ext/mbstring: mb_regex_substitute() reads past the replacement string's end on a truncated trailing multibyte lead byte
メンテナーはふだん 1 日以内に返信
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 74/100
調査の方向性
ext/mbstring/php_mbregex.cを読み、mb_regex_substitute()のメインループと近くにある長さを制限した呼び出し箇所を調べてください。まず、提供されたmb_ereg_replace()の再現コードを実行し、次に関連するmbstringのテストでカバレッジを確認してください。切り詰められた末尾のマルチバイトバイトが置換文字列の末尾までだけコピーされ、再現コードが期待される2バイトの結果を返せば完了です。
索引モデルが issue の本文から書いたものです。
説明
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:
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
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:
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.
- 主要言語
- C
- スター
- 40.7k
- フォーク
- 8.3k
- 平均マージ
- 2日 17時間
- マージ済み PR(30日)
- 159
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
php/php-src のほかの issue
-
Bug Status: Needs Triage
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
メンテナーはふだん 1 日以内に返信
-
zend_set_local_var() overwrites existing compiled variable (CV) zval without calling zval_ptr_dtor()対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンBug Status: Needs Triage
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
master: part of the mbstring bailout fix got lost in a merge対応中かも @lazerg が 6 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
Bug Status: Needs Triage
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
メンテナーはふだん 1 日以内に返信
-
quoted_printable_encode() produces lines longer than 76 characters for bytes 0xF5 to 0xFF対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
メンテナーはふだん 1 日以内に返信
-
area:buffer good first issue type:documentation
難易度 2/5 1〜3時間 初心者へのやさしさ 86/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
sudo-project/sudo#586 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 2 日以内に返信
-
Abort (SIGABRT) when stderr is a closed pipe: uncaught fmt::system_error in ccache_main's catch対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 3 日以内に返信