From 349fc2c71c1c5cecf514440567493fec97105460 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 27 Oct 2026 09:00:50 -0600 Subject: [PATCH 1/3] JIT: fix vm_interrupt on TAILCALL VM --- NEWS | 2 + Zend/zend_vm_execute.h | 2 +- Zend/zend_vm_gen.php | 2 +- .../jit/tailcall_vm_interrupt_opline_001.phpt | 39 +++++++++++++++++++ .../jit/tailcall_vm_interrupt_opline_002.phpt | 29 ++++++++++++++ 5 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 ext/opcache/tests/jit/tailcall_vm_interrupt_opline_001.phpt create mode 100644 ext/opcache/tests/jit/tailcall_vm_interrupt_opline_002.phpt diff --git a/NEWS b/NEWS index 3fa71b513345..53ad7284bb24 100644 --- a/NEWS +++ b/NEWS @@ -122,6 +122,8 @@ PHP NEWS map_ptr offset). (prateekbhujel) . Fixed tracing JIT losing register-allocated CVs when an interrupt is handled at the end of a loop trace. (Levi Morrison) + . Fixed JIT crashes when handling a VM interrupt with a stale saved opline + in the tailcall VM. (Levi Morrison) - PDO: . Fixed invalid ATTR_ORACLE_NULLS values being accepted and truncated. diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index df733f525d50..2ffa11f9ec3f 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -445,7 +445,7 @@ static zend_vm_opcode_handler_func_t zend_vm_get_opcode_handler_func(uint8_t opc # define ZEND_VM_ENTER() execute_data = EG(current_execute_data); LOAD_OPLINE(); ZEND_VM_ENTER_EX() # define ZEND_VM_LEAVE() return (zend_op*)((uintptr_t)opline | ZEND_VM_ENTER_BIT) #endif -#define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); +#define ZEND_VM_INTERRUPT() ZEND_VM_KIND_TAILCALL_SAVE_OPLINE(); ZEND_VM_TAIL_CALL(zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); #ifdef ZEND_VM_FP_GLOBAL_REG #define ZEND_VM_LOOP_INTERRUPT() zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); #else diff --git a/Zend/zend_vm_gen.php b/Zend/zend_vm_gen.php index 0b9c47a330ef..0101dc8ad628 100755 --- a/Zend/zend_vm_gen.php +++ b/Zend/zend_vm_gen.php @@ -2045,7 +2045,7 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) out($f,"# define ZEND_VM_ENTER() execute_data = EG(current_execute_data); LOAD_OPLINE(); ZEND_VM_ENTER_EX()\n"); out($f,"# define ZEND_VM_LEAVE() return (zend_op*)((uintptr_t)opline | ZEND_VM_ENTER_BIT)\n"); out($f,"#endif\n"); - out($f,"#define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_helper".($spec?"_SPEC":"")."(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));\n"); + out($f,"#define ZEND_VM_INTERRUPT() ZEND_VM_KIND_TAILCALL_SAVE_OPLINE(); ZEND_VM_TAIL_CALL(zend_interrupt_helper".($spec?"_SPEC":"")."(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));\n"); out($f,"#ifdef ZEND_VM_FP_GLOBAL_REG\n"); out($f,"#define ZEND_VM_LOOP_INTERRUPT() zend_interrupt_helper".($spec?"_SPEC":"")."(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);\n"); out($f,"#else\n"); diff --git a/ext/opcache/tests/jit/tailcall_vm_interrupt_opline_001.phpt b/ext/opcache/tests/jit/tailcall_vm_interrupt_opline_001.phpt new file mode 100644 index 000000000000..ed9bef7e10eb --- /dev/null +++ b/ext/opcache/tests/jit/tailcall_vm_interrupt_opline_001.phpt @@ -0,0 +1,39 @@ +--TEST-- +Tracing JIT: preserve the next opline when handling a VM interrupt +--DESCRIPTION-- +The object comparison raises an interrupt; the observer INI setting installs +an interrupt callback without enabling observer events. While recording the +caller, the backward jump must save its target before the interrupt helper +reloads the opline. Otherwise it revisits the already executed call opcode, +whose pending call frame has been consumed. + +At the time of writing this was failing on the TAILCALL VM, but this should +pass on all VMs. +--EXTENSIONS-- +opcache +zend_test +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit=tracing +opcache.jit_buffer_size=64M +opcache.jit_hot_func=1 +opcache.jit_hot_loop=1 +zend_test.observer.set_vm_interrupt_on_begin=1 +--FILE-- + +--EXPECT-- +Done diff --git a/ext/opcache/tests/jit/tailcall_vm_interrupt_opline_002.phpt b/ext/opcache/tests/jit/tailcall_vm_interrupt_opline_002.phpt new file mode 100644 index 000000000000..0e13b0ca3bbd --- /dev/null +++ b/ext/opcache/tests/jit/tailcall_vm_interrupt_opline_002.phpt @@ -0,0 +1,29 @@ +--TEST-- +Function JIT: preserve the next opline when handling a VM interrupt +--DESCRIPTION-- +The object comparison raises an interrupt while the minimal function JIT +executes a callable opcode handler. The interrupt helper must resume at the +branch target, not reload the saved comparison opcode. No interrupt callback +is installed, isolating the saved-opline bug from callback resume-tag handling. +--EXTENSIONS-- +opcache +zend_test +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit=1201 +opcache.jit_buffer_size=64M +--FILE-- + +--EXPECT-- +int(22) From 33e8eeb7862bc76ec9b76f07b6b91730ffb302e3 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 27 Oct 2026 10:08:56 -0600 Subject: [PATCH 2/3] Function JIT: clear VM-enter bit after interrupt In the CALL and TAILCALL VMs, opcode handlers called by function JIT can return the next opline tagged with ZEND_VM_ENTER_BIT after invoking a VM interrupt callback. Function JIT used that result directly as an opcode pointer, causing invalid accesses when execution continued. --- NEWS | 2 ++ ext/opcache/jit/zend_jit_ir.c | 4 +-- .../jit/function_vm_interrupt_enter_001.phpt | 25 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 ext/opcache/tests/jit/function_vm_interrupt_enter_001.phpt diff --git a/NEWS b/NEWS index 53ad7284bb24..a35e2de030e0 100644 --- a/NEWS +++ b/NEWS @@ -124,6 +124,8 @@ PHP NEWS handled at the end of a loop trace. (Levi Morrison) . Fixed JIT crashes when handling a VM interrupt with a stale saved opline in the tailcall VM. (Levi Morrison) + . Fixed function JIT misinterpreting VM-enter results after an interrupt + callback in the CALL and TAILCALL VMs. (Levi Morrison) - PDO: . Fixed invalid ATTR_ORACLE_NULLS values being accepted and truncated. diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 52bd527d9d50..8fa6e96b9572 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -4152,11 +4152,11 @@ static int zend_jit_handler(zend_jit_ctx *jit, const zend_op *opline, int may_th } else if (ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL) { zend_vm_opcode_handler_func_t handler = (zend_vm_opcode_handler_func_t)zend_get_opcode_handler_func(opline); ir_ref ip = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(handler), jit_FP(jit), jit_IP(jit)); - jit_STORE_IP(jit, ip); + jit_STORE_IP(jit, ir_AND_A(ip, ir_CONST_ADDR(~ZEND_VM_ENTER_BIT))); } else { zend_vm_opcode_handler_t handler = opline->handler; ir_ref ip = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(handler), jit_FP(jit), jit_IP(jit)); - jit_STORE_IP(jit, ip); + jit_STORE_IP(jit, ir_AND_A(ip, ir_CONST_ADDR(~ZEND_VM_ENTER_BIT))); } if (may_throw) { zend_jit_check_exception(jit); diff --git a/ext/opcache/tests/jit/function_vm_interrupt_enter_001.phpt b/ext/opcache/tests/jit/function_vm_interrupt_enter_001.phpt new file mode 100644 index 000000000000..2651180fde8f --- /dev/null +++ b/ext/opcache/tests/jit/function_vm_interrupt_enter_001.phpt @@ -0,0 +1,25 @@ +--TEST-- +Function JIT: decode the VM-enter bit after an interrupt callback +--EXTENSIONS-- +opcache +zend_test +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit=1201 +opcache.jit_buffer_size=64M +zend_test.observer.set_vm_interrupt_on_begin=1 +--FILE-- + +--EXPECT-- +int(22) From 1ceb87b26ddfbb2e3a7a7dcbb65ffd65c562da2b Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 27 Oct 2026 16:20:33 -0600 Subject: [PATCH 3/3] perf: avoid checking the enter bit when there is no interrupt function callback --- ext/opcache/jit/zend_jit_ir.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 8fa6e96b9572..f1127094f278 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -4152,11 +4152,17 @@ static int zend_jit_handler(zend_jit_ctx *jit, const zend_op *opline, int may_th } else if (ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL) { zend_vm_opcode_handler_func_t handler = (zend_vm_opcode_handler_func_t)zend_get_opcode_handler_func(opline); ir_ref ip = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(handler), jit_FP(jit), jit_IP(jit)); - jit_STORE_IP(jit, ir_AND_A(ip, ir_CONST_ADDR(~ZEND_VM_ENTER_BIT))); + if (zend_interrupt_function) { + ip = ir_AND_A(ip, ir_CONST_ADDR(~ZEND_VM_ENTER_BIT)); + } + jit_STORE_IP(jit, ip); } else { zend_vm_opcode_handler_t handler = opline->handler; ir_ref ip = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(handler), jit_FP(jit), jit_IP(jit)); - jit_STORE_IP(jit, ir_AND_A(ip, ir_CONST_ADDR(~ZEND_VM_ENTER_BIT))); + if (zend_interrupt_function) { + ip = ir_AND_A(ip, ir_CONST_ADDR(~ZEND_VM_ENTER_BIT)); + } + jit_STORE_IP(jit, ip); } if (may_throw) { zend_jit_check_exception(jit);