Repository navigation
Conversation
The typed-reference branch spilled OP_DATA to its stack slot and then set op3_addr to that slot, so the code emitted after the branch also read OP_DATA from the slot, which is only written on the typed-reference path. Pass the slot to zend_jit_assign_op_to_typed_ref() only, as zend_jit_assign_op() does. Assisted-by: Claude
ndossche
approved these changes
Oct 20, 2026
ndossche
left a comment
Member
There was a problem hiding this comment.
Thanks.
It's interesting because it's one of those cases where the code looks right, but due to a later code path it actually isn't right.
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.
In
zend_jit_assign_dim_op(), when the array element may be a reference, the cold branch for typed references spills the OP_DATA register to its stack slot and then setsop3_addr = real_addr;. That reassignment also applies to the code emitted after the branch, so the normal+=/%=/... path then reads the right-hand operand from a stack slot that is only written on the typed-reference path. With the tracing JIT,$a[0] += $i + 1on an array parameter adds a stale value (printsint(3)instead ofint(6)), and the%=variant from the issue can divide by a stale zero and dies with SIGFPE. A CV on the right ($a[0] += $i) is affected too, also with the default JIT counters.The fix passes
real_addrto the helper call only, the same wayzend_jit_assign_op()already does for its typed reference branch, and leavesop3_addralone for the rest of the function.The new test
ext/opcache/tests/jit/gh24437.phptcovers the undefined-key case, an element that is a reference, an element that is a typed reference, and the%=case. Without the fix the first two cases print 3 and 13 instead of 6 and 16, and the%=case crashes with an FPE; with it, the test passes. The reproducers from the issue now matchopcache.jit=off.Here's the PR, as suggested. It is close to #24387: there a register wasn't loaded on all paths around the typed reference check; here the stack slot is written only on the typed-reference path but read on the normal one.
zend_jit_assign_dim_op()already force-loads OP_DATA before the reference check, so the change from #24387 isn't needed here.Targets PHP-8.4, where I built and tested it (debug + ASan/UBSan build);
ext/opcache/tests/jitpasses in tracing and function mode.Fixes #24437.
Found with an LLM-based testing tool; I wrote this fix with help from Claude and reviewed it myself.