[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`.