Commit 509e27bae1c for php.net
commit 509e27bae1cd8b5db255cde69a7fc847a85cc6e6
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Sat Aug 29 08:00:19 2026 -0400
[mbstring] Fix mb_ereg_replace() emitting NUL/garbage for unterminated \k<name>
When a \k<name> 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.
Closes GH-23499
diff --git a/NEWS b/NEWS
index ee0cf1b9c1d..ac08278a8a8 100644
--- a/NEWS
+++ b/NEWS
@@ -49,6 +49,11 @@ 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<name> backref has no closing delimiter.
+ (Ilia Alshanetsky)
+
- 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 e823b552981..3d887aab490 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 00000000000..67a39dc9f95
--- /dev/null
+++ b/ext/mbstring/tests/mb_ereg_replace_kname_unterminated_nul.phpt
@@ -0,0 +1,18 @@
+--TEST--
+mb_ereg_replace() with unterminated \k<name> backref must not embed a NUL byte
+--EXTENSIONS--
+mbstring
+--FILE--
+<?php
+var_dump(bin2hex(mb_ereg_replace('(\d+)', '\k<num', '123')));
+var_dump(bin2hex(mb_ereg_replace('(\d+)', "\\k'num", '123')));
+var_dump(bin2hex(mb_ereg_replace('(x)(y)', 'a\k<n', 'xy')));
+var_dump(bin2hex(mb_ereg_replace('(\d+)', "a\\k\xF0", '123')));
+var_dump(bin2hex(mb_ereg_replace('(\d+)', "\\\\k\xE2\x82", '123')));
+?>
+--EXPECT--
+string(12) "5c6b3c6e756d"
+string(12) "5c6b276e756d"
+string(10) "615c6b3c6e"
+string(8) "615c6bf0"
+string(10) "5c5c6be282"