Repository navigation
PHP 8.5+ Windows ZTS: random access violations with OPcache JIT in multi-process setups #24185
Copy link
Copy link
Open
Description
Description
Reproduce code:
child.ini (same directory):
opcache.enable=1
opcache.enable_cli=1
opcache.protect_memory=1
opcache.jit=1205
opcache.jit_buffer_size=128Mrepro-parent.php (parent should also use child.ini):
<?php
declare(strict_types=1);
$suppress = ($argv[1] ?? 'false') === 'true';
$runs = (int) ($argv[2] ?? 10);
const ACCESS_VIOLATION = -1073741819;
$crashes = 0;
for($i = 1; $i <= $runs; $i++){
$cmd = 'start "" /b /wait ' . PHP_BINARY . ' -n -c ' . __DIR__ . '\child.ini ' . __DIR__ . '\repro-child.php & exit';
$proc = proc_open($cmd, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, __DIR__, getenv(), ['suppress_errors' => $suppress]);
fclose($pipes[0]);
$out = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
$code = proc_close($proc);
if($code === ACCESS_VIOLATION){
$crashes++;
}
echo "run $i: exit $code, output: " . trim($out) . "\n";
}
echo "suppress_errors=" . var_export($suppress, true) . ": $crashes / $runs runs crashed\n";
exit($crashes > 0 ? 1 : 0);repro-child.php (must use child.ini):
<?php
declare(strict_types=1);
function fixedArrayRead(SplFixedArray $array, int $index) : mixed{
return $array->offsetGet($index);
}
function makeDate(string $value) : DateTimeImmutable{
return new DateTimeImmutable($value);
}
function iteratorSeek(ArrayIterator $iterator, int $position) : void{
$iterator->seek($position);
}
$caught = 0;
$array = new SplFixedArray(1);
$iterator = new ArrayIterator([1]);
for($i = 0; $i < 10; $i++){
try{
fixedArrayRead($array, 5);
}catch(RuntimeException $e){
$caught++;
}
try{
makeDate('not a date');
}catch(Exception $e){
$caught++;
}
try{
iteratorSeek($iterator, 5);
}catch(OutOfBoundsException $e){
$caught++;
}
}
echo "caught $caught\n";Run:
php -n -c child.ini repro-parent.php true 50
In theory, the child script's expected output should be caught 30 because exceptions should be caught by try-catch. But in PHP 8.5+ (ZTS, OPcache JIT enabled), this causes ACCESS_VIOLATION for some reason with OPcache JIT enabled.
Tests with GitHub actions runner: action log
Full code snippets can be found on this repo
PHP Version
PHP 8.5.11 (cli) (built: Sep 22 2026 13:50:46) (ZTS Visual C++ 2022 x64)
Copyright (c) The PHP Group
Built by The PHP Group
Zend Engine v4.5.11, Copyright (c) Zend Technologies
with Zend OPcache v8.5.11, Copyright (c), by Zend Technologies
PHP 8.6.0RC3 (cli) (built: Oct 6 2026 17:20:27) (ZTS Visual C++ 2026 x64)
Copyright © The PHP Group and Contributors
Built by The PHP Group
Zend Engine v4.6.0RC3, Copyright © Zend by Perforce
with Zend OPcache v8.6.0RC3, Copyright ©, by Zend by Perforce
Operating System
Windows 10 Pro 22H2 (10.0.19045), Windows Server 2025 (windows-2025-vs2026 runner image)
Reactions are currently unavailable
Activity
ndossche commentedon Oct 8, 2026
with a little tweak:
ndossche commentedon Oct 8, 2026
So it looks like EG() pointers etc are corrupted on ZTS because the TLS addresses are different between processes.
Add this in
zend_jit_resolve_tsrm_ls_cache_offsets():fprintf(stderr, "tls_index=%u offset=%zu\n", _tls_index, offset);The process that first JITs has
tls_index=8 offset=32, and the crashing process hastls_index=7 offset=32.Turns out that on Windows,
tls_indexis not fixed, so by baking the TLS since PHP 8.5 by making opcache bundled, this causes problems.Reason is that now when PHP starts up we get a TLS index attached, whereas previously we only got it when opcache got loaded, which was at the same address every time by design and so got the same TLS index.
ndossche commentedon Oct 8, 2026
The stupid&simple fix is to detach not only on ASLR mismatch but also TLS mismatch. But that's awkward: that means on some processes you get opcache but no JIT...
I believe the right fix is to load the offset at runtime rather than embedding it? That can't be fixed without upstream IR changes, and it makes TLS loads more expensive...
PoC:
cc @iluuu1994 @arnaud-lb @TimWolla since you were involved in #18961
arnaud-lb commentedon Oct 15, 2026
The IR version in 8.6 has
ir_TLS_ADDR(). We could use that to fetchNtCurrentTeb()withir_TLS_ADDR(-1, 0). In 8.5 we could fallback tozend_jit_get_tsrm_ls_cache().@ndossche do you want to handle this or shall I?
Also related: #20470
ndossche commentedon Oct 15, 2026
Your related points to this issue ;)
But feel free to handle this, I'm kinda busy rn
arnaud-lb commentedon Oct 15, 2026
Oops, wanted to link #20470.
Ok, I will handle this then.