[php-src] Issue #24769: ext/mbstring: mb_regex_substitute() reads past the replacement string's end on a truncated trailing multibyte lead byte

From: Date: Tue, 27 Oct 2026 03:02:36 +0000
Subject: [php-src] Issue #24769: ext/mbstring: mb_regex_substitute() reads past the replacement string's end on a truncated trailing multibyte lead byte
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-253228@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/24769 Author: kunwaratulhax0r ### Description 509e27bae1 (GH-23499, fixed August 2026) corrected two places in mb_regex_substitute() (ext/mbstring/php_mbregex.c, the replacement-string interpolation behind mb_ereg_replace()/mb_eregi_replace()) where php_mb_mbchar_bytes()'s claimed character width was used to advance a pointer without checking it against the buffer's actual remaining length -- both were clamped to MIN(p + clen, eos). f6852befa3 (GH-24498, fixed October 2026) separately fixed the same bug class in a different file, php_mb_rfc1867_getword(). Neither fix touched the *first* php_mb_mbchar_bytes() call in mb_regex_substitute()'s main loop -- the check that runs on every character, before any backslash/backref handling, to decide whether it's an ordinary character or the start of a backreference: ```c while (p < eos) { ... clen = (int) php_mb_mbchar_bytes(p, enc); if (clen != 1 || p == eos || p[0] != '\\') { /* skip anything that's not an ascii backslash */ smart_str_appendl(pbuf, p, clen); p += clen; continue; } ``` If the character at p is not a backslash, and its lead byte (looked up in the current mbregex encoding's mblen_table) claims a width of 2-4 bytes but fewer than clen - 1 bytes actually remain before eos, smart_str_appendl(pbuf, p, clen) copies clen bytes starting at p regardless -- reading past the replacement string's real content. This is the identical shape as the two already-fixed call sites a few lines later in the same function, which use MIN(p + clen, eos) instead. Minimal reproducer: ```php <?php mb_regex_encoding('UTF-8'); $replacement = "x" . "\xF0"; // ends in a UTF-8 4-byte lead byte with 0 continuation bytes present var_dump(mb_ereg_replace('a', $replacement, 'a')); ``` Resulted in this output: ``` string(5) "x..." ``` (5 bytes: the input is only 2 bytes, but php_mb_mbchar_bytes() claims the trailing \xF0 byte is a 4-byte character, so 4 bytes get copied starting from it -- 3 of them past the replacement string's actual end.) But I expected this output instead: a 2-byte result ("x\xF0"), i.e. the same MIN(p + clen, eos)-style clamping already applied to the other two call sites in this function. This was repeatable across 20 differently-sized/content inputs ending the same way -- always exactly claimed_width - 1 extra bytes beyond the logical input length. In shorter variants the extra bytes were non-zero, unpredictable content rather than a fixed pattern, consistent with genuine adjacent heap memory being copied into the return value. **Suggested fix:** apply the same clamp already used nearby in the same function: ```c clen = (int) php_mb_mbchar_bytes(p, enc); if (clen != 1 || p == eos || p[0] != '\\') { size_t avail = MIN((size_t) clen, (size_t) (eos - p)); smart_str_appendl(pbuf, p, avail); p += avail; continue; } ``` Confirmed present, unmodified, on PHP-8.3, PHP-8.4, and PHP-8.5, and master. ### PHP Version ``` PHP 8.7.0-dev (cli) (built: Oct 26 2026 09:57:01) (NTS DEBUG) Copyright © The PHP Group and Contributors Zend Engine v4.7.0-dev, Copyright © Zend by Perforce with Zend OPcache v8.7.0-dev, Copyright ©, by Zend by Perforce ``` Also confirmed present on the PHP-8.3, PHP-8.4, and PHP-8.5 branches via direct content check. ### Operating System Ubuntu 24.04 (Docker), but this is pure string-handling logic, not OS-specific.

« previous php.bugs (#253228) next »