[php-src] Issue #22915: JIT compiled exit clobbers registers before saving
[email protected] (arnaud-lb) Wed, 29 Jul 2026 09:29:31 +0000
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <jS1DxVXnsCyGjdr77q7sbeuYwWEPePrahV2yckMtRyE@main.internal.php.net> |
Issue: https://github.com/php/php-src/issues/22915
Author: arnaud-lb
### Description
The following code:
```php
<?php
final class It implements Iterator {
public readonly array $values;
public int $position = 0;
public function __construct(array $values) {
$this->values = $values;
}
public function rewind(): void {}
public function valid(): bool {
return $this->position === 0;
}
public function current(): mixed {
if (!isset($this->values[$this->position])) {
throw new Exception();
}
return $this->values[$this->position];
}
public function key(): mixed {
return $this->position;
}
public function next(): void {
$this->position++;
}
}
function iter(It $it) {
foreach ($it as $value) {
var_dump($value);
if (!$value instanceof stdClass) {
continue;
}
}
}
echo "# First run\n";
for ($i = 0; $i < 5; $i++) {
getenv('F')(new It([getenv('F')])); // non-immutable, packed array
}
// Next side-exit should deoptimize
var_dump(ini_set('opcache.jit_max_side_traces', '0'));
var_dump(ini_set('opcache.jit_blacklist_side_trace', '0'));
echo "# Second run\n";
for ($i = 0; $i < 5; $i++) {
getenv('F')(new It([getenv('F'), 'map' => true])); // non-immutable, map, triggers exit
}
?>
```
When executed with
```
F=iter php -n -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.jit_hot_func=1 -d opcache.jit_hot_side_exit=1 test.php
Resulted in this output:
```
# First run
string(4) "iter"
string(4) "iter"
string(4) "iter"
string(4) "iter"
string(4) "iter"
string(3) "128"
string(1) "8"
# Second run
string(4) "iter"
Fatal error: Uncaught Exception in test.php:18
Stack trace:
#0 test.php(34): It->current()
#1 test.php(53): iter(Object(It))
#2 {main}
thrown in test.php on line 18
```
But I expected this output instead:
```
# First run
string(4) "iter"
string(4) "iter"
string(4) "iter"
string(4) "iter"
string(4) "iter"
string(3) "128"
string(1) "8"
# Second run
string(4) "iter"
string(4) "iter"
string(4) "iter"
string(4) "iter"
string(4) "iter"
```
Cause:
During the second run phase, the compiled trace for `It::current()` exits because `$this->values` is not a packed array anymore. The side exit is blacklisted, but the generated code clobbers registers before saving them:
```asm
ESCAPE-6-4:
cmpb $0, 0x69(%r12)
je .L1
; try_addref(T1)
movq 0x60(%r12), %rax
addl $1, (%rax)
.L1:
; save T2 (%rax), but it was clobbered above
movq %rax, 0x70(%r12)
movl $4, 0x78(%r12)
leaq -0xb01677d(%rip), %r13
addq $0x38, %rsp
jmpq *(%r13)
```
This causes `isset($this->values[$this->position])` to evaluate to `false`.
### PHP Version
```plain
PHP 8.4
```
### Operating System
_No response_