[php-src] Issue #24683: Tracing JIT: local keeps a stale constant after a trace links into a loop trace

From: Date: Sun, 25 Oct 2026 12:28:35 +0000
Subject: [php-src] Issue #24683: Tracing JIT: local keeps a stale constant after a trace links into a loop trace
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-253179@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/24683 Author: ChrisOrlando # Tracing JIT: local keeps a stale constant after a trace links into a loop trace ### Description With the tracing JIT, a local variable that is set to a constant and then overwritten by a loaded value keeps the *constant* after another trace links into the function's loop trace. The function then reads through the wrong pointer and returns the wrong value. In step() below, case 1 sets $ptr = 0x80080000, uses it to compute an address, loads the real pointer (0x1F8010F4) and assigns it to $ptr, then falls through to case 2. Once the JIT has compiled the loop, step() sometimes returns with $ptr still 0x80080000. The following code: ```php <?php // php -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.jit_buffer_size=64M repro.php // // step() is a small state machine. Case 1 sets $ptr to a constant, uses it to // load the real pointer, and stores that in $ptr; it then falls through to case 2, // which reads through $ptr. After the tracing JIT has compiled the loop in // step(), $ptr sometimes still holds the constant when step() returns. final class Bus { public int $value = 0; public function read(int $address): int { return $this->value; } } function step(Bus $bus, array &$state, array &$ram, int $pc): int { $ptr = $state['ptr']; $i = $state['i']; $n = $state['n']; $ret = $state['ret']; $c = 0; while (true) { switch ($pc) { case 1: $ptr = 0x80080000; $a = $ptr - 0x6260; if (($a & 3) !== 0) { goto trap; } $p = $a & 0x1FFFFFFF; $inRam = $p < 0x00800000; $x = $inRam ? $ram[$p >> 2] : $bus->read($a); $ptr = $x; // 0x1F8010F4 $t = $i !== $n; // unused, but needed to reproduce $c += 6; // unused, but needed to reproduce // fall through case 2: $a = $ptr; $p = $a & 0x1FFFFFFF; $inRam = $p < 0x00800000; $y = $inRam ? $ram[$p >> 2] : $bus->read($a); $pc = ($y & 0x7F000000) !== 0 ? 3 : 4; continue 2; case 3: if ($pc === 99) { goto trap; } $ret = 1; $pc = $ram[0]; // 5 // fall through default: goto leave; } } leave: $state['ptr'] = $ptr; $state['ret'] = $ret; return $pc; trap: $state['ptr'] = $ptr; } $bus = new Bus(); $ram = array_fill(0, 0x80000, 0); $ram[0] = 5; $ram[0x79DA0 >> 2] = 0x1F8010F4; $wrong = 0; for ($k = 0; $k < 10; $k++) { $bus->value = ($k % 128) << 24; $state = ['ptr' => 0x1F8010F4, 'i' => 0, 'n' => 0, 'ret' => 0]; $pc = 2; for ($m = 0; $m < 100; $m++) { $pc = step($bus, $state, $ram, $pc); if ($state['ptr'] !== 0x1F8010F4) { printf("k=%d m=%d: \$ptr is 0x%X, expected 0x1F8010F4\n", $k, $m, $state['ptr']); $wrong++; } if ($pc !== 5) { break; } $pc = $state['ret']; } } echo "wrong: $wrong\n"; ``` Resulted in this output: ``` k=1 m=29: $ptr is 0x80080000, expected 0x1F8010F4 k=2 m=0: $ptr is 0x80080000, expected 0x1F8010F4 ... (one line for each k up to 9) wrong: 9 ``` But I expected this output instead (as with opcache.jit=0 or opcache.jit=function): ``` wrong: 0 ``` ### Observations - With opcache.jit_debug set to trace start, stop and compiled, the log shows trace 1 as the while loop in step(), and trace 2 as a side trace of it. Trace 3 is the caller's loop: it inlines the call into step(), including case 1, then reports stop (link to 1). After that, $ptr comes back stale. So the overwritten $ptr doesn't seem to be stored back before entering trace 1, and trace 1 sees the earlier constant. - Only fails under tracing (tracing, 1254, 1255). function is fine. Still fails with opcache.optimization_level=0, so the opcache optimizer is probably not involved. opcache.jit_hot_loop=1 hides it. - Very sensitive to the shape of the code. Each of these makes it go away: - assigning the load to $ptr directly instead of via $x - removing the unused $t = $i !== $n; or $c += 6; - changing $c += 6 to $c++ ($c += 2 still fails) - comparing $i to a constant - removing either never-taken goto trap check - making trap: not read $ptr - replacing $bus->read() with a constant - using a sparse array instead of array_fill() for $ram - Found in a PHP emulator that compiles guest machine code to PHP. Booting a console BIOS hung, because the generated interrupt dispatcher polled the wrong address. There it only appeared after raising opcache.jit_max_exit_counters, because at the default limit that trace was rejected for running out of exit counters. The standalone script fails with default settings. - Fresh files need opcache.file_update_protection=0 (or a few seconds' wait) to be cached and JIT-compiled at all. ### PHP Version ``` PHP 8.5.12 (cli) (built: Oct 19 2026 17:02:46) (NTS) [Homebrew] ``` ### Operating System macOS (Darwin 27.0.0), Apple M1 (arm64)

« previous php.bugs (#253179) next »