From 83effd7d1a8ab4cd72fee6de960ebc0d1d244558 Mon Sep 17 00:00:00 2001 From: Anton Berezin Date: Wed, 8 Jul 2026 18:13:28 +0200 Subject: [PATCH 1/3] fix: escape DEL, C1 controls, and invalid UTF-8 in log output --- log.c | 83 ++++++++++++++++++++++++++++++++++++++++++++-------- t/test_log.c | 22 +++++++++++++- 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/log.c b/log.c index 7d24210..6ce795a 100644 --- a/log.c +++ b/log.c @@ -20,14 +20,59 @@ log_setup(void) } } +/* Decode one strict UTF-8 sequence at p (RFC 3629: no overlongs, no + * surrogates, max U+10FFFF). Returns the sequence length and stores the + * code point in *rune, or returns 0 if p does not start a valid sequence. */ +static int +utf8_decode(const unsigned char *p, unsigned *rune) +{ + unsigned r; + int len, i; + + if (p[0] < 0x80) { *rune = p[0]; return 1; } + 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 < 0x80) return 0; + if (len == 3 && r < 0x800) return 0; + if (len == 4 && r < 0x10000) return 0; + if (r >= 0xd800 && r <= 0xdfff) + return 0; + if (r > 0x10ffff) + return 0; + *rune = r; + return len; +} + static int needs_quote(const char *s) { - if (!*s) + const unsigned char *p = (const unsigned char *)s; + unsigned rune; + 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_decode(p, &rune); + if (!len || rune <= 0x9f) /* invalid UTF-8 or a C1 control */ return 1; + p += len; + } return 0; } @@ -46,20 +91,34 @@ log_enc(char *out, size_t outsz, const char *val) return n; } /* quoted form */ + static const char hexd[] = "0123456789abcdef"; #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); + unsigned rune; + 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_decode(p, &rune)) == 0) { + /* invalid UTF-8: escape this byte, retry at the next */ + PUTX(c); + p++; + } else if (rune <= 0x9f) { + /* C1 control, validly encoded */ + while (len--) { PUTX(*p); p++; } + } else { + while (len--) { PUT(*p); p++; } + } } PUT('"'); +#undef PUTX #undef PUT out[n < outsz ? n : outsz - 1] = '\0'; return n; diff --git a/t/test_log.c b/t/test_log.c index d651e6b..2b5e8eb 100644 --- a/t/test_log.c +++ b/t/test_log.c @@ -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); } @@ -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)"); From c2f5ff35dff9c4cf1e5f7d03eaf541a67096c526 Mon Sep 17 00:00:00 2001 From: Anton Berezin Date: Wed, 8 Jul 2026 18:25:46 +0200 Subject: [PATCH 2/3] refactor: fold C1 rejection into the UTF-8 helper, share the hex table --- log.c | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/log.c b/log.c index 6ce795a..7856f8e 100644 --- a/log.c +++ b/log.c @@ -4,6 +4,8 @@ static int journal_mode = 0; +static const char hexd[] = "0123456789abcdef"; + void log_setup(void) { @@ -20,17 +22,16 @@ log_setup(void) } } -/* Decode one strict UTF-8 sequence at p (RFC 3629: no overlongs, no - * surrogates, max U+10FFFF). Returns the sequence length and stores the - * code point in *rune, or returns 0 if p does not start a valid sequence. */ +/* 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_decode(const unsigned char *p, unsigned *rune) +utf8_passthrough(const unsigned char *p) { unsigned r; int len, i; - if (p[0] < 0x80) { *rune = p[0]; return 1; } - if ((p[0] & 0xe0) == 0xc0) { len = 2; r = p[0] & 0x1f; } + 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 @@ -40,14 +41,13 @@ utf8_decode(const unsigned char *p, unsigned *rune) return 0; r = (r << 6) | (p[i] & 0x3f); } - if (len == 2 && r < 0x80) return 0; - if (len == 3 && r < 0x800) return 0; - if (len == 4 && r < 0x10000) return 0; + 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; - *rune = r; return len; } @@ -55,7 +55,6 @@ static int needs_quote(const char *s) { const unsigned char *p = (const unsigned char *)s; - unsigned rune; int len; if (!*p) @@ -68,8 +67,8 @@ needs_quote(const char *s) p++; continue; } - len = utf8_decode(p, &rune); - if (!len || rune <= 0x9f) /* invalid UTF-8 or a C1 control */ + len = utf8_passthrough(p); + if (!len) return 1; p += len; } @@ -91,13 +90,11 @@ log_enc(char *out, size_t outsz, const char *val) return n; } /* quoted form */ - static const char hexd[] = "0123456789abcdef"; #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; ) { unsigned char c = *p; - unsigned rune; int len; if (c == '"' || c == '\\') { PUT('\\'); PUT(c); p++; } @@ -106,13 +103,11 @@ log_enc(char *out, size_t outsz, const char *val) 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_decode(p, &rune)) == 0) { - /* invalid UTF-8: escape this byte, retry at the next */ + 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 if (rune <= 0x9f) { - /* C1 control, validly encoded */ - while (len--) { PUTX(*p); p++; } } else { while (len--) { PUT(*p); p++; } } @@ -249,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++) { From 0fa368e016b96ab38955966c6c89a68ea1159286 Mon Sep 17 00:00:00 2001 From: Anton Berezin Date: Wed, 8 Jul 2026 18:41:14 +0200 Subject: [PATCH 3/3] chore: changelog fragment for log escaping fix --- .changes/unreleased/Fixed-20260708-184106.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changes/unreleased/Fixed-20260708-184106.yaml diff --git a/.changes/unreleased/Fixed-20260708-184106.yaml b/.changes/unreleased/Fixed-20260708-184106.yaml new file mode 100644 index 0000000..c1a68f4 --- /dev/null +++ b/.changes/unreleased/Fixed-20260708-184106.yaml @@ -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