From 66f72ccedd60ba655413078a2d232d94eda13af8 Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 24 Aug 2026 21:52:27 +0000 Subject: [PATCH] AOT: fix mb_trim NestedJIT rtrim wrong output leftover of #34396 MbTrimJitHelper used substr() and index-subtraction patterns that thin AOT miscompiles; rewrite rtrim with byte indexing, copyPrefix, and C2-hold NBSP deferral so runtime haystack trim matches VM/Zend. Co-authored-by: Cursor --- ext/mbstring/MbTrimJitHelper.php | 73 ++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/ext/mbstring/MbTrimJitHelper.php b/ext/mbstring/MbTrimJitHelper.php index b6b2bd5f211..68a0465d668 100644 --- a/ext/mbstring/MbTrimJitHelper.php +++ b/ext/mbstring/MbTrimJitHelper.php @@ -11,6 +11,10 @@ * Private bodies: ascending for-loops only, no `break`/`continue`/strrev (those * SIGSEGV under thin AOT when several leaves share a module). * + * Byte access via $value[$i] — NestedJIT substr() mis-fires under thin AOT (#34338). + * Rtrim NBSP uses a C2-hold deferral — `$last = $i - 1` / `$last === $n - 1` miscompile + * under thin AOT (#34396 leftover). + * * Default charset: ASCII ws + U+00A0 (C2 A0). php-src: ext/mbstring/mbstring.c */ final class MbTrimJitHelper @@ -58,7 +62,7 @@ private static function trimLeftBody(string $value): string $started = 0; $prev = ''; for ($i = 0; $i < $n; ++$i) { - $c = \substr($value, $i, 1); + $c = $value[$i]; $ws = 0; if (' ' === $c || "\t" === $c || "\n" === $c || "\r" === $c || "\0" === $c || "\x0B" === $c) { @@ -100,33 +104,43 @@ private static function trimRightBody(string $value): string { $n = \strlen($value); $last = -1; - $prev = ''; + $pendingC2 = -1; for ($i = 0; $i < $n; ++$i) { - $c = \substr($value, $i, 1); - $ws = 0; - if (' ' === $c || "\t" === $c || "\n" === $c || "\r" === $c - || "\0" === $c || "\x0B" === $c) { - $ws = 1; - } elseif ("\xA0" === $c && "\xC2" === $prev) { - $ws = 1; - // previous C2 was start of NBSP — retract last if it pointed at C2 - if ($last === $i - 1) { - $last = $i - 2; + $c = $value[$i]; + if ($pendingC2 >= 0) { + if ("\xA0" === $c) { + $pendingC2 = -1; + } else { + $last = $pendingC2; + $pendingC2 = -1; + if (0 === self::defaultWsFlag($c)) { + $last = $i; + } } - } - if (0 === $ws) { + } elseif ("\xC2" === $c) { + $pendingC2 = $i; + } elseif (0 === self::defaultWsFlag($c)) { $last = $i; } - $prev = $c; } - if ($last === $n - 1) { - return $value; + if ($pendingC2 >= 0) { + $last = $pendingC2; } if ($last < 0) { return ''; } - return \substr($value, 0, $last + 1); + return self::copyPrefix($value, $last + 1); + } + + private static function defaultWsFlag(string $c): int + { + if (' ' === $c || "\t" === $c || "\n" === $c || "\r" === $c + || "\0" === $c || "\x0B" === $c) { + return 1; + } + + return 0; } private static function trimCharsLeftBody(string $value, string $what): string @@ -136,11 +150,11 @@ private static function trimCharsLeftBody(string $value, string $what): string $out = ''; $started = 0; for ($i = 0; $i < $n; ++$i) { - $c = \substr($value, $i, 1); + $c = $value[$i]; if (0 === $started) { $hit = 0; for ($k = 0; $k < $wlen; ++$k) { - if (\substr($what, $k, 1) === $c) { + if ($what[$k] === $c) { $hit = 1; } } @@ -162,10 +176,10 @@ private static function trimCharsRightBody(string $value, string $what): string $wlen = \strlen($what); $last = -1; for ($i = 0; $i < $n; ++$i) { - $c = \substr($value, $i, 1); + $c = $value[$i]; $hit = 0; for ($k = 0; $k < $wlen; ++$k) { - if (\substr($what, $k, 1) === $c) { + if ($what[$k] === $c) { $hit = 1; } } @@ -173,13 +187,20 @@ private static function trimCharsRightBody(string $value, string $what): string $last = $i; } } - if ($last === $n - 1) { - return $value; - } if ($last < 0) { return ''; } - return \substr($value, 0, $last + 1); + return self::copyPrefix($value, $last + 1); + } + + private static function copyPrefix(string $value, int $len): string + { + $out = ''; + for ($i = 0; $i < $len; ++$i) { + $out .= $value[$i]; + } + + return $out; } }