[php-src] Issue #24206: Tracing JIT with global register allocation corrupts UTF-8 scanning on aarch64

From: Date: Thu, 08 Oct 2026 21:07:37 +0000
Subject: [php-src] Issue #24206: Tracing JIT with global register allocation corrupts UTF-8 scanning on aarch64
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-252945@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/24206 Author: mercierj ### Description Disclosure: this report and its reduction were prepared with AI assistance. The commands and results below were executed locally. Tracing JIT on Alpine Linux aarch64 produces an incorrect UTF-8 character map for valid input. The reproducer below is standalone PHP: it needs no Composer packages, framework, PHPUnit, database, or network. This was reduced from Symfony Mime 7.4.13's CharacterStream after observing corrupted MIME Subject headers. The unmodified Symfony encoder produced imm=C3=3Fdiatdiate instead of imm=C3=A9diate for immédiate. The standalone reduction isolates the wrong character map before MIME encoding or decoding. All three literal input strings are valid UTF-8. The lookup table has been reduced to the bytes used in those inputs. Save the following as repro.php: ```php <?php // Reduced from Symfony Mime v7.4.13 CharacterStream (MIT). // Copyright (c) Fabien Potencier and Symfony contributors. // Standalone diagnostic: tracing JIT must not change valid UTF-8 into invalid input. final class Utf8Scanner { private const UTF8_LENGTH_MAP = [ "\x20" => 1, "\x46" => 1, "\x53" => 1, "\x61" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, "\x67" => 1, "\x68" => 1, "\x69" => 1, "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, "\x80" => 0, "\x93" => 0, "\xa7" => 0, "\xa8" => 0, "\xa9" => 0, "\xc3" => 2, "\xe2" => 3, ]; /** @var array{p: array<int, int>, i: array<int, true>} */ public array $map = ['p' => [], 'i' => []]; public function scan(string $string, int $startOffset, string &$ignoredChars): int { $strlen = \strlen($string); $charPos = \count($this->map['p']); $foundChars = 0; $invalid = false; for ($i = 0; $i < $strlen; ++$i) { $char = $string[$i]; $size = self::UTF8_LENGTH_MAP[$char]; if (0 == $size) { /* char is invalid, we must wait for a resync */ $invalid = true; continue; } if ($invalid) { /* We mark the chars as invalid and start a new char */ $this->map['p'][$charPos + $foundChars] = $startOffset + $i; $this->map['i'][$charPos + $foundChars] = true; ++$foundChars; $invalid = false; } if (($i + $size) > $strlen) { $ignoredChars = substr($string, $i); break; } for ($j = 1; $j < $size; ++$j) { $char = $string[$i + $j]; if ($char > "\x7F" && $char < "\xC0") { // Valid - continue parsing } else { /* char is invalid, we must wait for a resync */ $invalid = true; continue 2; } } /* Ok we got a complete char here */ $this->map['p'][$charPos + $foundChars] = $startOffset + $i + $size; $i += $j - 1; ++$foundChars; } return $foundChars; } } $inputs = ['règlement', 'échéance – Société Française', 'régularisation immédiate']; foreach ($inputs as $input) { $scanner = new Utf8Scanner(); $ignored = ''; $scanner->scan($input, 0, $ignored); if ($scanner->map['i']) { echo "VALID UTF8 MARKED INVALID: ", json_encode($scanner->map), "\n"; exit(1); } } echo "PASS\n"; ``` Run: ```sh php -d opcache.enable_cli=1 \ -d opcache.file_update_protection=0 \ -d opcache.jit_buffer_size=128M \ -d opcache.jit=1255 \ -d opcache.jit_hot_loop=41 repro.php ``` opcache.file_update_protection=0 avoids skipping compilation of a freshly saved reproducer. The explicit hot-loop threshold makes the failure reproducible independently of preceding application/test execution. Expected output and exit status: ```text PASS ``` Exit status: 0. Actual output: ```text VALID UTF8 MARKED INVALID: {"p":[1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26],"i":{"19":true}} ``` Exit status: 1. The invalid marker appears while scanning the third string, régularisation immédiate. ### Controls and repeatability Using the same script, PHP build, and command, changing only opcache.jit: | JIT setting | Fresh processes tested | Result | | --- | --- | --- | | disable | 20 | 20 pass | | 1255 | 20 | 20 fail | | 1205 (function JIT) | 20 | 20 pass | Additional individual checks: 1254 also fails; 1055 and 1155 pass. This points toward tracing JIT with global register allocation, but I have not identified the faulty compiler instruction. Replacing $i += $j - 1 with the equivalent $i += $size - 1 after successful continuation-byte validation did not remove the failure. Possibly related: #22115 and its open fix #22132. I have not tested that patch or established that this is the same defect. Please treat this as an additional concrete reproducer if it is a duplicate. ### PHP Version ```text PHP 8.5.11 (cli) (built: Sep 25 2026 15:57:29) (NTS) Copyright (c) The PHP Group Built by Alpine Linux aports Zend Engine v4.5.11, Copyright (c) Zend Technologies with Zend OPcache v8.5.11, Copyright (c), by Zend Technologies ``` The same corruption was also reproduced with PHP 8.5.10 on aarch64. Other architectures and an upstream development build have not been tested. ### Operating System Alpine Linux 3.24.2, aarch64, in a local Docker container. The original symptom was first observed on a Linux ARM64 CI runner. ### Reproducer attribution Reduced from [Symfony Mime v7.4.13 CharacterStream](https://github.com/symfony/mime/blob/v7.4.13/CharacterStream.php), MIT licensed. The diagnosis and reduction used AI assistance; the commands and results above were executed locally. <details> <summary>Original MIT license notice</summary> ```text Copyright (c) 2010-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` </details>

« previous php.bugs (#252945) next »