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"