[GIT-PULLS] [php-src] PR #23241: JIT: Repeat FETCH_OBJ_IS in the VM when the property slot is IS_UNDEF

[email protected] (EdmondDantes)
Newsgroups php.git-pulls
Message-ID <[email protected]>
Pull Request: https://github.com/php/php-src/pull/23241
Author: EdmondDantes

Under `opcache.jit=tracing`, `isset($obj->prop[$key])` answers `false` for a key the array holds, when `prop` is a declared property that `unset()` removed and `__isset()`/`&__get()` serve it. `opcache.jit=function` and `opcache.jit=off` are both correct. Reproduces on the stock `php:8.4-cli` and `php:8.5-cli` images; 8.3 is unaffected.

```php
class Store { public array $marks = []; }

class Holder {
    public static Store $store;
    public array $marks = [];

    public function __construct() { unset($this->marks); }
    public function &__get(string $name) { return self::$store->$name; }
    public function __isset(string $name): bool { return isset(self::$store->$name); }
    public function mark(string $key): void { $this->marks[$key] = true; }
    public function has(string $key): bool { return isset($this->marks[$key]); }
}

Holder::$store = new Store();
$holder = new Holder();

for ($n = 0; $n < 10; $n++) {
    $key = "k{$n}";
    $holder->mark($key);
    var_dump($holder->has($key));   // true, true, then false eight times
}
```

`php -d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.jit_hot_func=2 t.php`

## Cause

With `prop_info` known, the trace for `Holder::has()` reads the property slot directly and leaves the `IS_UNDEF` check to the result type guard (`zend_jit_fetch_obj()`, `zend_jit_ir.c:14556`). The guard fails, because the slot of an `unset()` property is `IS_UNDEF` while the trace recorded an array, and the exit lands in `zend_jit_trace_exit()`:

```c
if (op->opcode == ZEND_FETCH_DIM_IS || op->opcode == ZEND_FETCH_OBJ_IS) {
    ZVAL_NULL(EX_VAR_NUM(i));
} else {
    ...
    repeat_last_opline = 1;
}
```

The exit opline is `FETCH_OBJ_IS + 1`, so the VM resumes at `ISSET_ISEMPTY_DIM_OBJ` with NULL in the result and never enters `zend_std_read_property()`. `unset()` on a declared property leaves the slot `IS_UNDEF` with `IS_PROP_UNINIT` clear, which is exactly the state where `zend_std_read_property()` calls `__isset()` and then `__get()` (`zend_object_handlers.c:929`).

`FETCH_DIM_IS` keeps the shortcut: an undefined array index has no magic behind it. `FETCH_OBJ_IS` joins the opcodes that re-run in the VM, which is what `FETCH_OBJ_R` already does for the same slot state.

The shortcut comes from ace18f49192 (#14298), so 8.4 and later are affected.

The trace stays wrong for as long as the exit is taken; once the side exit gets hot, the side trace is recorded from the deoptimized state and bakes the NULL in permanently.

## Testing

`ext/opcache/tests/jit/fetch_obj_is_unset_prop.phpt` fails without the change and passes with it. On this branch, built with `--enable-debug --enable-opcache`: `ext/opcache/tests` has no new failures (`gh18050.phpt` fails before and after), and `Zend/tests` under `opcache.jit=tracing` is 4970 tests with none failed.

I have not opened a tracking issue; say the word and I will file one and rename the test to `gh<N>.phpt`.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.