[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_