Fix GH-17626: JIT corrupts opline handler when blacklisting root trace - #23571
Open
RV7PR wants to merge 1 commit into
Open
Fix GH-17626: JIT corrupts opline handler when blacklisting root trace#23571RV7PR wants to merge 1 commit into
RV7PR wants to merge 1 commit into
Conversation
…race When a trace exits through a ZEND_JIT_EXIT_INVALIDATE guard while ZEND_JIT_TRACE_NUM has already reached opcache.jit_max_root_traces, zend_jit_trace_exit() blacklists the root trace and restores the original VM handler. It wrote that handler to the exit opline (the INIT_* opline whose callee guard failed) instead of the root trace's start opline. The opcodes live in SHM, so every worker keeps executing the foreign handler until restart. For a root trace that starts at the entry of a function with typed parameters the copied handler is ZEND_RECV, which then runs on an INIT_FCALL / INIT_STATIC_METHOD_CALL opline and raises "Too few arguments to function X(), N passed ... and exactly N expected" from a frame that has all its arguments. Other root oplines lead to crashes instead. Introduced by 350af54 (phpGH-14475).
RV7PR
force-pushed
the
fix/gh-17626-trace-exit-invalidate
branch
from
September 4, 2026 20:03
49c3ba3 to
3fbb782
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.
Fixes #17626
The bug
zend_jit_trace_exit()handlesZEND_JIT_EXIT_INVALIDATEby walking up to the root tracet. IfZEND_JIT_TRACE_NUM >= JIT_G(max_root_traces)it blacklists the root and puts the original VM handler back, but writes it toopline(the exit opline of the trace that just exited) instead oft->opline(the root's start opline). The siblingelsebranch usest->opline.The opcodes live in shared memory, so the corruption is process-wide and permanent until restart. That matches the reports: it appears after long uptime (the 1024 root-trace limit has been reached), after deploys / composer updates (a recompiled callee makes an INVALIDATE guard fail), frequently in trait methods on 8.5 (classes using a trait share the opcodes and therefore the trace), and only goes away after restarting FPM / the container.
When the root trace starts at the entry of a function with typed parameters the root opline is
ZEND_RECV, so the RECV handler ends up on anINIT_*opline. It readsop1.numas the argument number (2 forparent::, the stack size forINIT_FCALL, a literal offset forFoo::bar()), callszend_missing_arg_error()on a frame that has all its arguments and produces the contradictory "N passed ... and exactly N expected" message from the issue. Other root oplines produce the SIGSEGVs mentioned there.Introduced by 350af54 (fix for GH-14475); present in PHP-8.3, PHP-8.4, PHP-8.5 and master.
The fix
Restore the handler on the root trace's opline,
t->opline, as the other branch already does.Tests
Both tests set
opcache.jit_max_root_traces=2so the limit is reached right after the first root trace, and use a typed function so the root trace starts at a RECV opcode.gh17626.phpt: namespacedcaller()calls an unqualified user function from another file (INIT_NS_FCALL_BY_NAME, guarded withZEND_JIT_EXIT_INVALIDATEbecause the callee is immutable). The callee's file is touched and invalidated at the end, so under--repeat 2(the CI repeat job; same approach asgh8591-001.phptandinit_fcall_003.phpt) the second run recompiles the callee and the guard fails. Without the fix:ArgumentCountError: Too few arguments to function GH17626\caller(), 1 passed ... and exactly 1 expected. This is the trigger 8.3/8.4 users hit after a deploy or composer update.gh17626_002.phpt: a typed trait method callingparent::m(), used by two classes with different parents (grandparent in another file sozend_jit_may_be_modified()keeps the guard).zend_jit_init_static_method_call()emits an INVALIDATE guard for this only on PHP-8.5 and master, so this test fails without the fix from 8.5 on without needing--repeat. It is the trigger behind the Carbon / trait reports on 8.5.Verified on macOS arm64: PHP-8.4 unfixed/fixed with and without
--repeat 2, master unfixed/fixed; fullext/opcache/tests/jitpasses with the fix (474/0 on 8.4, 487/0 on master).Disclaimer
I am the reporter of #17626. The root-cause analysis, the fix, the tests and this write-up were produced with Claude Fable 5.1 (Anthropic) at my direction; I ran the reproductions and test runs on my machine and verified the results, but I cannot review the C myself. I will relay any review questions and apply requested changes.