[php-src] PR #23241: JIT: Repeat FETCH_OBJ_IS in the VM when the property slot is IS_UNDEF
| From: | EdmondDantes | Date: | Thu, 13 Aug 2026 07:08:08 +0000 |
| Subject: | [php-src] PR #23241: JIT: Repeat FETCH_OBJ_IS in the VM when the property slot is IS_UNDEF | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-37817@lists.php.net to get a copy of this message | ||
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.