[php-src] PR #23571: Fix GH-17626: JIT corrupts opline handler when blacklisting root trace

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

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