From f73fa2b3166dcfd4db8d8ad91848a036165a750d Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 29 Aug 2026 08:00:19 -0400 Subject: [PATCH] [mbstring] Fix mb_ereg_replace() emitting NUL/garbage for unterminated \k When a \\k backref in an mb_ereg_replace() replacement string lacks its closing delimiter, the error path computed p = name_end + 1 past eos and copied the trailing NUL terminator into the output; the multibyte sibling path (p += clen after \\k) could overshoot eos and read heap bytes past the string. Both paths are now clamped to eos so verbatim fallback never copies beyond the parsed name. Sibling audit found no other copies past eos in this loop. --- NEWS | 4 ++++ ext/mbstring/php_mbregex.c | 5 +++-- ...mb_ereg_replace_kname_unterminated_nul.phpt | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 ext/mbstring/tests/mb_ereg_replace_kname_unterminated_nul.phpt diff --git a/NEWS b/NEWS index 519b0ccaf053..3c2c43d760c8 100644 --- a/NEWS +++ b/NEWS @@ -44,6 +44,10 @@ PHP NEWS . Fixed a use-after-free when IntlRuleBasedBreakIterator is constructed from compiled rules. (iliaal) +- MBString: + . Fixed mb_ereg_replace() emitting a NUL or out-of-bounds bytes in the + replacement when a \\k backref has no closing delimiter. (iliaal) + - Opcache: . Fixed opcache.protect_memory race under ZTS. (realFlowControl) . Fixed bug GH-23288 (Crash on restart when opcache.interned_strings_buffer diff --git a/ext/mbstring/php_mbregex.c b/ext/mbstring/php_mbregex.c index e823b5529818..3d887aab490b 100644 --- a/ext/mbstring/php_mbregex.c +++ b/ext/mbstring/php_mbregex.c @@ -771,7 +771,7 @@ static inline void mb_regex_substitute( clen = (int) php_mb_mbchar_bytes(++p, enc); if (clen != 1 || p == eos || (p[0] != '<' && p[0] != '\'')) { /* not a backref delimiter */ - p += clen; + p = MIN(p + clen, eos); smart_str_appendl(pbuf, sp, p - sp); continue; } @@ -791,12 +791,13 @@ static inline void mb_regex_substitute( if (maybe_num && !isdigit((unsigned char)name_end[0])) maybe_num = 0; name_end++; } - p = name_end + 1; if (name_end - name < 1 || name_end >= eos) { /* the backref was empty or we failed to find the end delimiter */ + p = MIN(name_end + 1, eos); smart_str_appendl(pbuf, sp, p - sp); continue; } + p = name_end + 1; /* we have either a name or a number */ if (maybe_num) { if (!onig_noname_group_capture_is_active(regexp)) { diff --git a/ext/mbstring/tests/mb_ereg_replace_kname_unterminated_nul.phpt b/ext/mbstring/tests/mb_ereg_replace_kname_unterminated_nul.phpt new file mode 100644 index 000000000000..67a39dc9f95c --- /dev/null +++ b/ext/mbstring/tests/mb_ereg_replace_kname_unterminated_nul.phpt @@ -0,0 +1,18 @@ +--TEST-- +mb_ereg_replace() with unterminated \k backref must not embed a NUL byte +--EXTENSIONS-- +mbstring +--FILE-- + +--EXPECT-- +string(12) "5c6b3c6e756d" +string(12) "5c6b276e756d" +string(10) "615c6b3c6e" +string(8) "615c6bf0" +string(10) "5c5c6be282"