JIT: Do not read an undefined property slot as NULL in FETCH_OBJ_IS - #23241
Open
EdmondDantes wants to merge 1 commit into
Open
JIT: Do not read an undefined property slot as NULL in FETCH_OBJ_IS#23241EdmondDantes wants to merge 1 commit into
EdmondDantes wants to merge 1 commit into
Conversation
isset($obj->prop[$key]) answered false for a key the array holds, when prop was a declared property removed by unset() and served by __isset()/__get(), and it answered from the IS_UNDEF slots of a lazy object instead of running its initializer. A trace reads the slot of a known property directly and left IS_UNDEF to the result type guard, which admits it whenever the recorded type is NULL. That holds only while nothing else answers for the slot, so FETCH_OBJ_IS now checks the slot before the guard: a lazy object and a property the accessors answer for leave the trace, while a typed property that was never assigned still reads as NULL inline. The side exit taken on any other recorded type stored NULL in the result and resumed after the fetch. FETCH_OBJ_IS now re-runs in the VM, as FETCH_OBJ_R already does.
EdmondDantes
force-pushed
the
fix-jit-fetch-obj-is-unset-prop
branch
from
August 13, 2026 09:29
7217e3b to
5764c14
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Under
opcache.jit=tracing,isset($obj->prop[$key])reads the property slot of a known property directly and treatsIS_UNDEFas NULL. Two things answer for such a slot and are skipped: a lazy object, whose initializer never runs, and a declared property removed byunset(), which__isset()/__get()answer for.opcache.jit=functionandopcache.jit=offare correct. Reproduces on the stockphp:8.4-cliandphp:8.5-cliimages; 8.3 predates the shortcut.php -d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.jit_hot_func=2 t.phpCause
unset()on a declared property leaves the slotIS_UNDEFwithIS_PROP_UNINITclear, andzend_std_read_property()answers through__isset()and then__get()in that state (zend_object_handlers.c:929). Every slot of an uninitialized lazy ghost or of a proxy isIS_UNDEFtoo, and reading one runs the initializer. Only a typed property that was never assigned reads as NULL without side effects.zend_jit_fetch_obj()skips theIS_UNDEFcheck whenprop_infois known and the result carriesMAY_BE_GUARD(zend_jit_ir.c:14394), leaving it tozend_jit_guard_fetch_result_type(). That guard sortsIS_UNDEFout on two paths, and both are wrong for a property:ir_ULE(Z_TYPE, IS_NULL), andIS_UNDEFpasses it inline. Nothing deoptimizes; the wrong answer comes straight out of the trace, and a lazy object is left uninitialized.zend_jit_trace_exit()stores NULL in the result and resumes atFETCH_OBJ_IS + 1, so the VM never re-runs the fetch either.Change
FETCH_OBJ_ISnow checks the slot before the guard: anIS_UNDEFslot leaves the trace when the object is lazy, or when the accessors answer for the property andIS_PROP_UNINITis clear. A never-assigned typed property still reads as NULL inline, so the shortcut keeps working where it is sound.The deoptimization path stops materializing NULL for
FETCH_OBJ_ISand repeats the opcode in the VM, asFETCH_OBJ_Ralready does.FETCH_DIM_ISkeeps both shortcuts: an undefined array index has nothing behind it.Testing
Three tests, one per path; all three fail on
PHP-8.4without the change:ext/opcache/tests/jit/fetch_obj_is_unset_prop.phpt— the side exit,ext/opcache/tests/jit/fetch_obj_is_unset_prop_null_guard.phpt— the inline NULL guard,ext/opcache/tests/jit/fetch_obj_is_lazy_object.phpt— the lazy ghost.Built on this branch with
--enable-debug --enable-opcache:ext/opcache/testshas no new failures (gh18050.phptfails before and after),Zend/testsunderopcache.jit=tracingis 4970 tests with none failed, andZend/tests/lazy_objectsunder the same is 225 of 225.Timings on that debug build, median of five runs, 3M iterations of
isset($obj->prop['k']), before → after:__isset()/__get()unset(), accessors answer true (300k iterations)The shortcut comes from ace18f4 (#14298), so 8.4 and later are affected.