From ab6e4fbd120c5a6b7be79f8ea24679a3915be92e Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Sun, 25 Oct 2026 20:57:30 +0100 Subject: [PATCH 1/2] Fix GH-24286: Guard unknown internal calls in tracing JIT --- NEWS | 2 + ext/opcache/jit/zend_jit_ir.c | 26 ++++++++++-- ext/opcache/tests/jit/gh24286.phpt | 34 +++++++++++++++ ext/opcache/tests/jit/gh24286_002.phpt | 47 +++++++++++++++++++++ ext/opcache/tests/jit/gh24286_003.phpt | 57 ++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 ext/opcache/tests/jit/gh24286.phpt create mode 100644 ext/opcache/tests/jit/gh24286_002.phpt create mode 100644 ext/opcache/tests/jit/gh24286_003.phpt diff --git a/NEWS b/NEWS index b4cbb913c925..fb738868529f 100644 --- a/NEWS +++ b/NEWS @@ -75,6 +75,8 @@ PHP NEWS . Fix exception checks due to NAN warnings and object empty()'s. (ndossche) . Fixed bug GH-24270 (JIT loses the called scope of self:: and parent:: calls in closures). (RV7PR) + . Fixed bug GH-24286 (Tracing JIT loses dynamic call results after + Closure::__invoke). (ndossche, Alessio Attilio) . Fixed bug GH-24337 (JIT: segfault when decrementing a non-numeric string property). (Lazizbek Ergashev) . Fixed bug GH-24185 (Windows ZTS: random access violations with OPcache JIT diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 52bd527d9d50..c34aa54b2669 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -10068,6 +10068,7 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen const void *exit_addr = NULL; const zend_op *prev_opline; ir_ref rx, func_ref = IR_UNUSED, if_user = IR_UNUSED, user_path = IR_UNUSED; + bool unknown_internal = 0; prev_opline = opline - 1; while (prev_opline->opcode == ZEND_EXT_FCALL_BEGIN || prev_opline->opcode == ZEND_TICKS) { @@ -10151,14 +10152,31 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen if (opline->opcode == ZEND_DO_FCALL || opline->opcode == ZEND_DO_FCALL_BY_NAME) { if (!func) { if (trace) { - uint32_t exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_TO_VM); + uint32_t exit_point; + func_ref = ir_LOAD_A(jit_CALL(rx, func)); + + if (trace->op == ZEND_JIT_TRACE_DO_ICALL) { + exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_POLYMORPHISM); + exit_addr = zend_jit_trace_get_exit_addr(exit_point); + if (!exit_addr) { + return 0; + } + + ir_GUARD( + ir_EQ( + ir_LOAD_U8(ir_ADD_OFFSET(func_ref, offsetof(zend_function, type))), + ir_CONST_U8(ZEND_INTERNAL_FUNCTION)), + ir_CONST_ADDR(exit_addr)); + unknown_internal = 1; + } + + exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_TO_VM); exit_addr = zend_jit_trace_get_exit_addr(exit_point); if (!exit_addr) { return 0; } - func_ref = ir_LOAD_A(jit_CALL(rx, func)); ir_GUARD_NOT( ir_AND_U32( ir_LOAD_U32(ir_ADD_OFFSET(func_ref, offsetof(zend_op_array, fn_flags))), @@ -10232,6 +10250,7 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen } if (!func + && !unknown_internal && opline->opcode != ZEND_DO_UCALL && opline->opcode != ZEND_DO_ICALL) { ir_ref type_ref = ir_LOAD_U8(ir_ADD_OFFSET(func_ref, offsetof(zend_function, type))); @@ -10240,6 +10259,7 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen } if ((!func || func->type == ZEND_USER_FUNCTION) + && !unknown_internal && opline->opcode != ZEND_DO_ICALL) { bool recursive_call_through_jmp = 0; uint32_t num_args = 0; @@ -10546,7 +10566,7 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen if ((!func || func->type == ZEND_INTERNAL_FUNCTION) && (opline->opcode != ZEND_DO_UCALL)) { - if (!func && (opline->opcode != ZEND_DO_ICALL)) { + if (!func && !unknown_internal && (opline->opcode != ZEND_DO_ICALL)) { ir_IF_FALSE(if_user); } diff --git a/ext/opcache/tests/jit/gh24286.phpt b/ext/opcache/tests/jit/gh24286.phpt new file mode 100644 index 000000000000..1fe41d281884 --- /dev/null +++ b/ext/opcache/tests/jit/gh24286.phpt @@ -0,0 +1,34 @@ +--TEST-- +GH-24286 (Tracing JIT preserves dynamic call results after Closure::__invoke) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit_buffer_size=64M +opcache.jit=tracing +opcache.jit_hot_loop=61 +opcache.jit_hot_func=127 +opcache.jit_hot_return=8 +opcache.jit_hot_side_exit=8 +--FILE-- +{$method}(); +} +function every($items, $method) { + foreach ($items as $item) { + if (!call($item, $method)) return false; + } + return true; +} +$closure = function () { return false; }; +for ($i = 0; $i < 2000; $i++) every([$closure], '__invoke'); +var_dump(call(new A, 'm')); +?> +--EXPECT-- +bool(false) diff --git a/ext/opcache/tests/jit/gh24286_002.phpt b/ext/opcache/tests/jit/gh24286_002.phpt new file mode 100644 index 000000000000..18e70bdaa5ad --- /dev/null +++ b/ext/opcache/tests/jit/gh24286_002.phpt @@ -0,0 +1,47 @@ +--TEST-- +GH-24286 (Tracing JIT preserves unpacked dynamic call results during recursion) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit_buffer_size=64M +opcache.jit=tracing +opcache.jit_hot_loop=61 +opcache.jit_hot_func=127 +opcache.jit_hot_return=8 +opcache.jit_hot_side_exit=8 +--FILE-- +{$method}(...$args); +} +function numberText(mixed $value): string { + if (!is_int($value) && !is_float($value)) return ''; + return (string) $value; +} +function walk($items, $method, $value, int $depth): string { + $text = ''; + foreach ($items as $item) { + $result = invokeCall($item, $method, $value); + $text .= numberText($result); + if ($result === false) return ''; + if ($depth > 0) $text .= walk([$item], $method, $value + 0.5, $depth - 1); + } + return $text; +} +$closure = function (float $value): mixed { return false; }; +for ($i = 0; $i < 2000; $i++) walk([$closure], '__invoke', 0.5, 1); +for ($i = 0; $i < 500; $i++) { + walk([new Box()], 'run', -184013402102942.84 + $i, 2); +} +echo walk([new Box()], 'run', 15.0, 2), "\n"; +var_dump(invokeCall(new Box(), 'run', value: 15.5)); +?> +--EXPECT-- +1515.516 +float(15.5) diff --git a/ext/opcache/tests/jit/gh24286_003.phpt b/ext/opcache/tests/jit/gh24286_003.phpt new file mode 100644 index 000000000000..a98ddc2c78b0 --- /dev/null +++ b/ext/opcache/tests/jit/gh24286_003.phpt @@ -0,0 +1,57 @@ +--TEST-- +GH-24286 (Tracing JIT preserves polymorphic call results and exceptions after Closure::__invoke) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit_buffer_size=64M +opcache.jit=tracing +opcache.jit_hot_loop=61 +opcache.jit_hot_func=127 +opcache.jit_hot_return=8 +opcache.jit_hot_side_exit=8 +--FILE-- +value; } + function fail() { throw new RuntimeException('expected'); } + function __call($method, $args) { return $this->value; } +} +function call($object, $method) { + return $object->{$method}(); +} +function every($items, $method) { + foreach ($items as $item) { + if (!call($item, $method)) return false; + } + return true; +} +$closure = function () { return false; }; +for ($i = 0; $i < 2000; $i++) every([$closure], '__invoke'); +$object = new Value; +foreach ([null, false, true, 42, 1.5, str_repeat('x', 3), [42], (object) ['n' => 42]] as $value) { + $object->value = $value; + for ($i = 0; $i < 100; $i++) { + if (call($object, 'get') !== $value) { + throw new RuntimeException('Incorrect result'); + } + } +} +$object->value = 'magic'; +var_dump(call($object, 'missing')); +var_dump(call($closure, '__invoke')); +try { + call($object, 'fail'); +} catch (RuntimeException $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} +echo "Done\n"; +?> +--EXPECT-- +string(5) "magic" +bool(false) +RuntimeException: expected +Done From cb60d8c80e090646736978d93f8f5669dfb15044 Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Mon, 26 Oct 2026 00:15:05 +0100 Subject: [PATCH 2/2] Restore explanation for unknown internal call guard --- ext/opcache/jit/zend_jit_ir.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index c34aa54b2669..55d317f0e16b 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -10157,6 +10157,14 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen func_ref = ir_LOAD_A(jit_CALL(rx, func)); if (trace->op == ZEND_JIT_TRACE_DO_ICALL) { + /* The trace recorded a call to an unknown internal function (e.g. a trampoline) + * and continues in the caller, so the callee must be internal. + * + * If a user function is called instead, exit and let a side trace handle it. + * This is better than emitting a user function code path: + * - the trace code after the call has a single known state, + * - the trace doesn't contain code for the user call that is rarely used, + * - a side trace knows the called function and can inline it. */ exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_POLYMORPHISM); exit_addr = zend_jit_trace_get_exit_addr(exit_point); if (!exit_addr) {