Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/Fixed-20260708-184106.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Fixed
body: 'logging: escape DEL, C1 controls, and invalid UTF-8 in field values'
time: 2026-07-08T18:41:06.151703+02:00
79 changes: 66 additions & 13 deletions log.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

static int journal_mode = 0;

static const char hexd[] = "0123456789abcdef";

void
log_setup(void)
{
Expand All @@ -20,14 +22,56 @@ log_setup(void)
}
}

/* Return the length of the strict UTF-8 sequence at p (RFC 3629: no
* overlongs, no surrogates, max U+10FFFF) when it encodes a code point that
* may pass through unescaped (>= U+00A0, past the C1 controls), else 0. */
static int
utf8_passthrough(const unsigned char *p)
{
unsigned r;
int len, i;

if ((p[0] & 0xe0) == 0xc0) { len = 2; r = p[0] & 0x1f; }
else if ((p[0] & 0xf0) == 0xe0) { len = 3; r = p[0] & 0x0f; }
else if ((p[0] & 0xf8) == 0xf0) { len = 4; r = p[0] & 0x07; }
else
return 0;
for (i = 1; i < len; i++) {
if ((p[i] & 0xc0) != 0x80)
return 0;
r = (r << 6) | (p[i] & 0x3f);
}
if (len == 2 && r < 0xa0) return 0; /* overlong or C1 control */
if (len == 3 && r < 0x800) return 0; /* overlong */
if (len == 4 && r < 0x10000) return 0; /* overlong */
if (r >= 0xd800 && r <= 0xdfff)
return 0;
if (r > 0x10ffff)
return 0;
return len;
}

static int
needs_quote(const char *s)
{
if (!*s)
const unsigned char *p = (const unsigned char *)s;
int len;

if (!*p)
return 1;
for (const unsigned char *p = (const unsigned char *)s; *p; p++)
if (*p == ' ' || *p == '=' || *p == '"' || *p == '\\' || *p < 0x20)
while (*p) {
if (*p == ' ' || *p == '=' || *p == '"' || *p == '\\' ||
*p < 0x20 || *p == 0x7f)
return 1;
if (*p < 0x80) {
p++;
continue;
}
len = utf8_passthrough(p);
if (!len)
return 1;
p += len;
}
return 0;
}

Expand All @@ -47,19 +91,29 @@ log_enc(char *out, size_t outsz, const char *val)
}
/* quoted form */
#define PUT(c) do { if (n + 1 < outsz) out[n++] = (c); } while (0)
#define PUTX(c) do { PUT('\\'); PUT('x'); PUT(hexd[(c) >> 4]); PUT(hexd[(c) & 0xf]); } while (0)
PUT('"');
for (const unsigned char *p = (const unsigned char *)val; *p; p++) {
for (const unsigned char *p = (const unsigned char *)val; *p; ) {
unsigned char c = *p;
if (c == '"' || c == '\\') { PUT('\\'); PUT(c); }
else if (c == '\n') { PUT('\\'); PUT('n'); }
else if (c == '\t') { PUT('\\'); PUT('t'); }
else if (c == '\r') { PUT('\\'); PUT('r'); }
else if (c < 0x20) {
static const char hexd[] = "0123456789abcdef";
PUT('\\'); PUT('x'); PUT(hexd[c >> 4]); PUT(hexd[c & 0xf]);
} else PUT(c);
int len;

if (c == '"' || c == '\\') { PUT('\\'); PUT(c); p++; }
else if (c == '\n') { PUT('\\'); PUT('n'); p++; }
else if (c == '\t') { PUT('\\'); PUT('t'); p++; }
else if (c == '\r') { PUT('\\'); PUT('r'); p++; }
else if (c < 0x20 || c == 0x7f) { PUTX(c); p++; }
else if (c < 0x80) { PUT(c); p++; }
else if ((len = utf8_passthrough(p)) == 0) {
/* invalid UTF-8 or a C1 control: escape this
* byte, retry at the next */
PUTX(c);
p++;
} else {
while (len--) { PUT(*p); p++; }
}
}
PUT('"');
#undef PUTX
#undef PUT
out[n < outsz ? n : outsz - 1] = '\0';
return n;
Expand Down Expand Up @@ -190,7 +244,6 @@ static char hexbuf_buf[2 * HEXBUF_MAX_IN + 1];
const char *
log_hexbuf(const void *buf, size_t len)
{
static const char hexd[] = "0123456789abcdef";
const unsigned char *b = buf;
size_t i, n = len > HEXBUF_MAX_IN ? HEXBUF_MAX_IN : len;
for (i = 0; i < n; i++) {
Expand Down
22 changes: 21 additions & 1 deletion t/test_log.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ is_enc(const char *val, const char *expected)
{
char buf[256];
log_enc(buf, sizeof(buf), val);
if (!ok(strcmp(buf, expected) == 0, "enc(%s)", val))
if (!ok(strcmp(buf, expected) == 0, "enc -> %s", expected))
tap_diag("got: %s want: %s", buf, expected);
}

Expand Down Expand Up @@ -37,6 +37,26 @@ main(void)
is_enc("t\tend", "\"t\\tend\"");
is_enc("r\rend", "\"r\\rend\"");
is_enc("\x01", "\"\\x01\"");
/* DEL and C1 controls must not reach the output raw */
is_enc("\x7f", "\"\\x7f\"");
is_enc("a\x7f" "b", "\"a\\x7fb\"");
is_enc("\x9b", "\"\\x9b\""); /* bare continuation byte */
is_enc("\xc2\x9b", "\"\\xc2\\x9b\""); /* U+009B CSI */
is_enc("\xc2\x80", "\"\\xc2\\x80\""); /* U+0080 */
/* valid printable UTF-8 passes through, bare or quoted */
is_enc("bl\xc3\xa5" "b\xc3\xa6r", "bl\xc3\xa5" "b\xc3\xa6r");
is_enc("s\xc3\xb8 blue", "\"s\xc3\xb8 blue\"");
is_enc("\xc2\xa0", "\xc2\xa0"); /* U+00A0, first past C1 */
is_enc("\xe2\x82\xac", "\xe2\x82\xac"); /* U+20AC */
is_enc("\xf0\x9f\x98\x80", "\xf0\x9f\x98\x80"); /* U+1F600 */
/* malformed UTF-8 is escaped byte by byte */
is_enc("\xc0\x9b", "\"\\xc0\\x9b\""); /* overlong 2-byte */
is_enc("\xe0\x80\xa0", "\"\\xe0\\x80\\xa0\""); /* overlong 3-byte */
is_enc("\xed\xa0\x80", "\"\\xed\\xa0\\x80\""); /* surrogate U+D800 */
is_enc("\xf4\x90\x80\x80", "\"\\xf4\\x90\\x80\\x80\""); /* above U+10FFFF */
is_enc("\xc3", "\"\\xc3\""); /* truncated at end */
is_enc("\xe2\x80", "\"\\xe2\\x80\""); /* truncated at end */
is_enc("\xe2" "AB", "\"\\xe2AB\""); /* resync after bad lead */
ok(strcmp(log_u(42u), "42") == 0, "U(42)");
ok(strcmp(log_i(-7), "-7") == 0, "I(-7)");
ok(strcmp(log_hex(0xa2u), "0xa2") == 0, "HEX(0xa2)");
Expand Down
Loading