[php-src] PR #23571: Fix GH-17626: JIT corrupts opline handler when blacklisting root trace
| From: | RV7PR | Date: | Fri, 04 Sep 2026 20:02:02 +0000 |
| Subject: | [php-src] PR #23571: Fix GH-17626: JIT corrupts opline handler when blacklisting root trace | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-38310@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/23571
Author: RV7PR
Fixes #17626
### The bug
zend_jit_trace_exit() handles ZEND_JIT_EXIT_INVALIDATE by walking up to
the root trace t. If ZEND_JIT_TRACE_NUM >= JIT_G(max_root_traces) it
blacklists the root and puts the original VM handler back, but writes it to opline (the
exit opline of the trace that just exited) instead of t->opline (the root's
start opline). The sibling else branch uses t->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 an INIT_* opline. It reads
op1.num as the argument number (2 for parent::, the stack size for
INIT_FCALL, a literal offset for Foo::bar()), calls
zend_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=2 so 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: namespaced caller() calls an unqualified user function
from another file (INIT_NS_FCALL_BY_NAME, guarded with
ZEND_JIT_EXIT_INVALIDATE because 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 as gh8591-001.phpt and init_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 calling parent::m(), used by two
classes with different parents (grandparent in another file so
zend_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; full ext/opcache/tests/jit passes 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.