Skip to content

JIT compiled exit clobbers registers before saving #22915

Description

@arnaud-lb

Description

The following code:

<?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:

 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

PHP 8.4

Operating System

No response

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions