From 153070b01b0424887db19c4cb296987f9ab0e9bf Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Tue, 13 Oct 2026 18:45:21 -0300 Subject: [PATCH 1/2] fix: t:utf8toUnicode drops the last hex digit of code points above U+FFFF Since the refactor in #3284 (2.9.9) the output pointer always advanced by six bytes after sprintf("%%u%04x"), so a code point that needs five hex digits (anything above U+FFFF) lost its last digit to the next write or to the terminating NUL: U+1F600 came out as "%u1f60". Advance by the number of bytes sprintf actually wrote. While there, fix two adjacent checks in the same function: bytes_left was never decremented, so the "enough bytes left" guards compared the total length instead of the remaining length and a truncated sequence at the end of the value was examined past input_len; and an 0xF4 lead byte with a second byte >= 0x90 (U+110000 and above, outside Unicode) was accepted as a valid four-byte sequence. Add four-byte, above-U+10FFFF and truncated-sequence cases to the utf8toUnicode unit tests, which had no four-byte input at all. Fixes #3684 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Cq9GqRpB4wzTFnEUvQaUJg --- apache2/msc_util.c | 11 +++++++---- tests/tfn/utf8toUnicode.t | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/apache2/msc_util.c b/apache2/msc_util.c index a939ddfe66..593cffdf15 100644 --- a/apache2/msc_util.c +++ b/apache2/msc_util.c @@ -120,7 +120,8 @@ char *utf8_unicode_inplace_ex(apr_pool_t *mp, unsigned char *input, long int inp data = rval = apr_palloc(mp, len); if (rval == NULL) return NULL; - for (i = 0; i < bytes_left;) { + for (i = 0; i < input_len;) { + bytes_left = input_len - i; unicode_len = 0; d = 0; utf = (unsigned char *)&input[i]; c = *utf; @@ -129,7 +130,7 @@ char *utf8_unicode_inplace_ex(apr_pool_t *mp, unsigned char *input, long int inp if ((c & 0x80) == 0) { if (c == 0) { unicode_len = 2; - d = utf[1]; + d = (bytes_left >= 2) ? utf[1] : 0; } } /* If first byte begins with binary 110 it is two byte encoding*/ @@ -178,6 +179,8 @@ char *utf8_unicode_inplace_ex(apr_pool_t *mp, unsigned char *input, long int inp } /* invalid UTF-8 character number range (RFC 3629) */ if ((d >= 0xD800) && (d <= 0xDFFF)) unicode_len = UNICODE_ERROR_RESTRICTED_CHARACTER; + /* restrict characters to UTF-8 range (U+0000 - U+10FFFF) */ + if ((unicode_len == 4) && (d > 0x10FFFF)) unicode_len = UNICODE_ERROR_RESTRICTED_CHARACTER; /* check for overlong */ if ((unicode_len == 4) && (d < 0x010000)) unicode_len = UNICODE_ERROR_OVERLONG_CHARACTER; /* three byte could be represented with less bytes */ @@ -187,8 +190,8 @@ char *utf8_unicode_inplace_ex(apr_pool_t *mp, unsigned char *input, long int inp if (unicode_len > 0) { i += unicode_len; - sprintf(data, "%%u%04x", d); - data += 6; + /* code points above U+FFFF need five hex digits */ + data += sprintf(data, "%%u%04x", d); *changed = 1; } else { diff --git a/tests/tfn/utf8toUnicode.t b/tests/tfn/utf8toUnicode.t index a39b8d7a34..ba0cf2ab66 100644 --- a/tests/tfn/utf8toUnicode.t +++ b/tests/tfn/utf8toUnicode.t @@ -54,3 +54,35 @@ output => " %u27f0 %u27f1 %u27f2 %u27f3 %u27f4 %u27f5 %u27f6 %u27f7 %u27f8 %u27f9 %u27fa %u27fb %u27fc %u27fd %u27fe %u27ff", ret => 1, }, +# Supplementary planes (four-byte sequences, five hex digits) +{ + type => "tfn", + name => "utf8toUnicode", + input => "😀 𝒜 𠀀 􏿿", + output => "%u1f600 %u1d49c %u20000 %u10ffff", + ret => 1, +}, +# Four-byte sequence followed by ASCII must not lose its last digit +{ + type => "tfn", + name => "utf8toUnicode", + input => "😀A", + output => "%u1f600A", + ret => 1, +}, +# Code point above U+10FFFF is not valid UTF-8 and is passed through +{ + type => "tfn", + name => "utf8toUnicode", + input => "\xf4\x90\x80\x80", + output => "\xf4\x90\x80\x80", + ret => 0, +}, +# Truncated sequence at the end of the input is passed through +{ + type => "tfn", + name => "utf8toUnicode", + input => "a\xe2\x82", + output => "a\xe2\x82", + ret => 0, +}, From dc8afb44a5077e629550e2a4d706ddd66c1feb41 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Wed, 14 Oct 2026 07:30:56 -0300 Subject: [PATCH 2/2] fix: bound the %u escape write with snprintf Sonar flagged the sprintf call added by the previous commit. The output buffer is sized for the worst case, so this only makes the bound explicit. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Mhspv9mxtMSiWC1U8mVH1H --- apache2/msc_util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apache2/msc_util.c b/apache2/msc_util.c index 593cffdf15..3d6491d02f 100644 --- a/apache2/msc_util.c +++ b/apache2/msc_util.c @@ -191,7 +191,7 @@ char *utf8_unicode_inplace_ex(apr_pool_t *mp, unsigned char *input, long int inp if (unicode_len > 0) { i += unicode_len; /* code points above U+FFFF need five hex digits */ - data += sprintf(data, "%%u%04x", d); + data += snprintf(data, len - (data - rval), "%%u%04x", d); *changed = 1; } else {