Commit 7050d57e0eb for php
commit 7050d57e0eb28865a6263ea24e49c8e31ce18513
Author: RV7PR <17615589+RV7PR@users.noreply.github.com>
Date: Fri Sep 4 21:21:27 2026 +0200
Fix GH-17626: JIT corrupts opline handler when blacklisting root trace
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 (GH-14475).
Closes GH-23571.
diff --git a/NEWS b/NEWS
index 64c94bbbebd..b2941f680eb 100644
--- a/NEWS
+++ b/NEWS
@@ -9,6 +9,9 @@ PHP NEWS
- Opcache:
. Fixed bug GH-20890 (Segfault in zval_undefined_cv with non-simple property
hook with minimal tracing JIT). (ndossche)
+ . Fixed bug GH-17626 (JIT corrupts an opline handler when blacklisting a
+ root trace at the opcache.jit_max_root_traces limit, causing spurious
+ "Too few arguments" errors and crashes). (RV7PR)
- SOAP:
. Fixed use of uninitialized func in do_request() on OOM bailout.
diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index 5aa62d2e087..41136bcd805 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -8788,7 +8788,7 @@ int ZEND_FASTCALL zend_jit_trace_exit(uint32_t exit_num, zend_jit_registers_buf
SHM_UNPROTECT();
zend_jit_unprotect();
- ((zend_op*)opline)->handler =
+ ((zend_op*)(t->opline))->handler =
ZEND_OP_TRACE_INFO(t->opline, jit_extension->offset)->orig_handler;
ZEND_OP_TRACE_INFO(t->opline, jit_extension->offset)->trace_flags &= ~ZEND_JIT_TRACE_JITED;
diff --git a/ext/opcache/tests/jit/gh17626.inc b/ext/opcache/tests/jit/gh17626.inc
new file mode 100644
index 00000000000..307a47ad294
--- /dev/null
+++ b/ext/opcache/tests/jit/gh17626.inc
@@ -0,0 +1,2 @@
+<?php
+function gh17626_callee(string $s) { return strtoupper($s); }
diff --git a/ext/opcache/tests/jit/gh17626.phpt b/ext/opcache/tests/jit/gh17626.phpt
new file mode 100644
index 00000000000..dbd917935c5
--- /dev/null
+++ b/ext/opcache/tests/jit/gh17626.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-17626: Opline handler corrupted when a root trace is blacklisted at the max_root_traces limit (fails with --repeat 2)
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.revalidate_freq=0
+opcache.jit=tracing
+opcache.jit_buffer_size=16M
+opcache.jit_hot_func=2
+opcache.jit_hot_loop=255
+opcache.jit_hot_return=255
+opcache.jit_hot_side_exit=255
+opcache.jit_max_root_traces=2
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+namespace GH17626;
+
+// In --repeat 2 the callee is recompiled after the first run, so the function
+// guard in the trace compiled for caller() fails with ZEND_JIT_EXIT_INVALIDATE.
+
+require __DIR__ . '/gh17626.inc';
+
+function caller(string $s) {
+ return gh17626_callee($s) . $s;
+}
+
+caller('a');
+caller('a');
+caller('a');
+echo caller('a'), "\n";
+echo caller('b'), "\n";
+
+touch(__DIR__ . '/gh17626.inc');
+opcache_invalidate(__DIR__ . '/gh17626.inc', true);
+?>
+--EXPECT--
+Aa
+Bb
diff --git a/ext/opcache/tests/jit/gh17626_002.inc b/ext/opcache/tests/jit/gh17626_002.inc
new file mode 100644
index 00000000000..8a12c7ae9fb
--- /dev/null
+++ b/ext/opcache/tests/jit/gh17626_002.inc
@@ -0,0 +1,2 @@
+<?php
+class GH17626GrandParent {}
diff --git a/ext/opcache/tests/jit/gh17626_002.phpt b/ext/opcache/tests/jit/gh17626_002.phpt
new file mode 100644
index 00000000000..a0f4792f540
--- /dev/null
+++ b/ext/opcache/tests/jit/gh17626_002.phpt
@@ -0,0 +1,47 @@
+--TEST--
+GH-17626: Opline handler corrupted when a root trace is blacklisted at the max_root_traces limit
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=tracing
+opcache.jit_buffer_size=16M
+opcache.jit_hot_func=2
+opcache.jit_hot_loop=255
+opcache.jit_hot_return=255
+opcache.jit_hot_side_exit=255
+opcache.jit_max_root_traces=2
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+require __DIR__ . '/gh17626_002.inc';
+
+class ParentA extends GH17626GrandParent { public static function m() { return 'A'; } }
+class ParentB extends GH17626GrandParent { public static function m() { return 'B'; } }
+
+trait T {
+ public function run(string $s) {
+ return parent::m() . $s;
+ }
+}
+
+class A extends ParentA { use T; }
+class B extends ParentB { use T; }
+
+$a = new A;
+$b = new B;
+
+$a->run('x');
+$a->run('x');
+$a->run('x');
+echo $a->run('x'), "\n";
+echo $b->run('y'), "\n";
+echo $b->run('y'), "\n";
+echo $a->run('x'), "\n";
+?>
+--EXPECT--
+Ax
+By
+By
+Ax