From 89246d2388159e30b9b05d47901c8040862394d1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 28 Sep 2026 09:56:16 +0200 Subject: [PATCH] ext/mbstring: Don't emit surrogates when encoding to UTF-8 The UTF-8 encoder turned surrogate codepoints into 3-byte sequences like "\xED\xA0\x80", which are not valid UTF-8. Such codepoints come from UCS-2, UCS-4 or HTML-ENTITIES input, or from mb_decode_numericentity(). Since mbstring marks the UTF-8 strings it returns as valid UTF-8, functions that trust that flag, like mb_check_encoding(), mb_scrub() or preg_match(), accepted them too. Surrogates are now replaced by the substitute character, as codepoints above U+10FFFF already are. The internal mode used by mb_strpos() and similar functions still encodes them, so that different surrogates don't match each other. --- NEWS | 2 + ext/mbstring/libmbfl/filters/mbfilter_utf8.c | 6 +++ ext/mbstring/tests/surrogates_to_utf8.phpt | 47 ++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 ext/mbstring/tests/surrogates_to_utf8.phpt diff --git a/NEWS b/NEWS index bde051dee660..686eedc46457 100644 --- a/NEWS +++ b/NEWS @@ -55,6 +55,8 @@ PHP NEWS - MBString: . Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in a truncated UTF-8 sequence). (Lazizbek Ergashev) + . Fixed mbstring functions emitting surrogates in UTF-8 output and flagging + it as valid UTF-8. (Nicolas Grekas) - MySQLi: . Fix GH-22854: Fixed failed assertion when accessing mysqli property after diff --git a/ext/mbstring/libmbfl/filters/mbfilter_utf8.c b/ext/mbstring/libmbfl/filters/mbfilter_utf8.c index b0a9b6f95a33..e097476178da 100644 --- a/ext/mbstring/libmbfl/filters/mbfilter_utf8.c +++ b/ext/mbstring/libmbfl/filters/mbfilter_utf8.c @@ -548,6 +548,12 @@ static void mb_wchar_to_utf8(uint32_t *in, size_t len, mb_convert_buf *buf, bool } else if (w < 0x800) { MB_CONVERT_BUF_ENSURE(buf, out, limit, len + 2); out = mb_convert_buf_add2(out, ((w >> 6) & 0x1F) | 0xC0, (w & 0x3F) | 0x80); + } else if (w >= 0xD800 && w <= 0xDFFF && buf->error_mode != MBFL_OUTPUTFILTER_ILLEGAL_MODE_BADUTF8) { + /* Surrogate codepoints (which may come from UCS-2, UCS-4 or numeric entities) are not valid in UTF-8. + * BADUTF8 mode is only used internally to search strings; there, encoding them keeps them distinct + * instead of turning them all into the same error marker. */ + MB_CONVERT_ERROR(buf, out, limit, w, mb_wchar_to_utf8); + MB_CONVERT_BUF_ENSURE(buf, out, limit, len); } else if (w < 0x10000) { MB_CONVERT_BUF_ENSURE(buf, out, limit, len + 3); out = mb_convert_buf_add3(out, ((w >> 12) & 0xF) | 0xE0, ((w >> 6) & 0x3F) | 0x80, (w & 0x3F) | 0x80); diff --git a/ext/mbstring/tests/surrogates_to_utf8.phpt b/ext/mbstring/tests/surrogates_to_utf8.phpt new file mode 100644 index 000000000000..9707c47f24df --- /dev/null +++ b/ext/mbstring/tests/surrogates_to_utf8.phpt @@ -0,0 +1,47 @@ +--TEST-- +Surrogate codepoints are not encoded as UTF-8 +--EXTENSIONS-- +mbstring +--FILE-- + +--EXPECT-- +UCS-4: 413f42 true +UCS-4LE: 3f true +UCS-2: 3f3f true +UCS-2LE: 3f true +HTML-ENTITIES: 3f3f true +mb_convert_variables: 3f true +mb_decode_numericentity: 3f3f true +mb_decode_mimeheader: 3f true +long: 552b44383030 true +int(3) +int(3) +int(1)