[php-src] Issue #24678: Tracing JIT with opcache.jit_max_polymorphic_calls=0: local variable undefined after a call in a side trace
| From: | ChrisOrlando | Date: | Sun, 25 Oct 2026 11:09:22 +0000 |
| Subject: | [php-src] Issue #24678: Tracing JIT with opcache.jit_max_polymorphic_calls=0: local variable undefined after a call in a side trace | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253176@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24678
Author: ChrisOrlando
### Description
With the tracing JIT and
opcache.jit_max_polymorphic_calls=0, a local that is always
assigned
becomes undefined after a method call, once a side trace has been compiled for that code path.
It shows up as Warning: Undefined variable $p (or, with typed parameters, as
TypeError: ... must be of type int, null given where the local is passed on).
The following code:
```php
<?php
// php -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.jit_buffer_size=64M \
// -d opcache.jit_max_polymorphic_calls=0 -d opcache.file_update_protection=0 repro.php
//
// The calls need to be straight-line code rather than a loop, so the script
// writes them out and includes the result.
$code = <<<'PHP'
<?php
final class Fast { public function get(int $x): int { return 1; } }
final class Slow { public function get(int $x): int { return 0; } }
final class Router
{
public function __construct(private Fast $fast = new Fast(), private Slow $slow = new Slow()) {}
public function route(int $addr): int
{
$p = $addr & 0x1FFFFFFF;
if ($p < 0x00800000) {
return $this->fast->get($p & 0x1FFFFF);
}
return $this->slow->get($p - 0x1F801070) + $p;
}
}
$r = new Router();
PHP;
// 127 fast-path calls make route() hot (opcache.jit_hot_func). The slow path then
// leaves the trace, and its 9th exit (opcache.jit_hot_side_exit = 8) is compiled.
$code .= str_repeat("\$r->route(0x801FFA70);\n", 127);
$code .= str_repeat("echo \$r->route(0x1F801070), \"\\n\";\n", 9);
file_put_contents(__DIR__ . '/generated.php', $code);
include __DIR__ . '/generated.php';
```
Resulted in this output (the 9th call):
```
528486512
... (7 more lines of 528486512)
Warning: Undefined variable $p in .../generated.php on line 15
0
```
But I expected this output instead (as with the JIT disabled, or
jit_max_polymorphic_calls 1, 2 or 8):
```
528486512
... (9 lines of 528486512 in total)
```
### Observations
- The failure starts exactly when the counts line up with the default thresholds: 127 calls on
the fast path make route() hot (opcache.jit_hot_func=127), and the slow
path fails on its
9th call, its first after its side exit becomes hot (opcache.jit_hot_side_exit=8).
With 126
fast-path calls it doesn't happen.
- Only opcache.jit_max_polymorphic_calls=0 triggers it; 1, 2 (the default) and 8 are
fine.
- Both calls need an *expression* of $p as their argument ($p &
0x1FFFFF, $p - 0x1F801070).
Passing $p itself to the fast path, or to the slow path, makes it go away. That
suggests
$p only exists in a register in the parent trace, and isn't stored back to its
slot before
the non-inlined call on the side trace.
- Found while reducing a crash in a PHP emulator: its bus read() method lost its
$p the same
way, through Bus::extract(): Argument #2 ($p) must be of type int, null given.
### 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)