[php-src] Issue #24206: Tracing JIT with global register allocation corrupts UTF-8 scanning on aarch64
| From: | mercierj | 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>