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

From: 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.

« previous php.git-pulls (#37817) next »