From e3df31248c65bdf8eb6257e1c8c2ae5da2065829 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 9 Jun 2026 14:33:36 -0600 Subject: [PATCH 1/5] Add regression test for tailcall VM crash --- ...observer_vm_interrupt_tailcall_return.phpt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 ext/zend_test/tests/observer_vm_interrupt_tailcall_return.phpt diff --git a/ext/zend_test/tests/observer_vm_interrupt_tailcall_return.phpt b/ext/zend_test/tests/observer_vm_interrupt_tailcall_return.phpt new file mode 100644 index 000000000000..95af2681c253 --- /dev/null +++ b/ext/zend_test/tests/observer_vm_interrupt_tailcall_return.phpt @@ -0,0 +1,29 @@ +--TEST-- +Observer: VM interrupt during tailcall return to caller +--DESCRIPTION-- +This exercises a VM interrupt raised immediately before a user function returns +to a caller that invoked it through DO_FCALL. On the tailcall VM, the caller's +saved opline must point to the opcode after DO_FCALL before a pending interrupt +is handled. +--EXTENSIONS-- +zend_test +--INI-- +opcache.jit=0 +zend_test.observer.set_vm_interrupt_on_begin=1 +--FILE-- + +--EXPECT-- +ok From a0f68b1db6ea77e2ca6bae700a7c80f9812ad3b6 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 9 Jun 2026 15:28:30 -0600 Subject: [PATCH 2/5] Fix vm_interrupt for tailcall VM --- Zend/zend_vm_def.h | 5 --- Zend/zend_vm_execute.h | 44 ++------------------------- Zend/zend_vm_execute.skl | 4 --- Zend/zend_vm_gen.php | 37 +++------------------- ext/opcache/jit/zend_jit_vm_helpers.c | 2 +- 5 files changed, 8 insertions(+), 84 deletions(-) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index fdfe48fc8421..6712625e83b1 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -10523,12 +10523,7 @@ ZEND_VM_DEFINE_OP(137, ZEND_OP_DATA); ZEND_VM_HELPER(zend_interrupt_helper, ANY, ANY) { zend_atomic_bool_store_ex(&EG(vm_interrupt), false); -#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL - /* opline is &call_interrupt_op. Load orig opline. */ - LOAD_OPLINE(); -#else SAVE_OPLINE(); -#endif if (zend_atomic_bool_load_ex(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 0faa84444be3..21c666aabebd 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -331,11 +331,6 @@ static zend_op hybrid_halt_op; static zend_vm_opcode_handler_func_t const * zend_opcode_handler_funcs; #endif - -#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL -static const zend_op call_interrupt_op; -#endif - #if (ZEND_VM_KIND != ZEND_VM_KIND_HYBRID && ZEND_VM_KIND != ZEND_VM_KIND_TAILCALL) || !ZEND_VM_SPEC static zend_vm_opcode_handler_t zend_vm_get_opcode_handler(uint8_t opcode, const zend_op* op); #endif @@ -4016,12 +4011,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_J static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS) { zend_atomic_bool_store_ex(&EG(vm_interrupt), false); -#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL - /* opline is &call_interrupt_op. Load orig opline. */ - LOAD_OPLINE(); -#else SAVE_OPLINE(); -#endif if (zend_atomic_bool_load_ex(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { @@ -56508,12 +56498,6 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NULL_HANDLER( ZEND_VM_NEXT_OPCODE(); /* Never reached */ } -#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL -static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt(ZEND_OPCODE_HANDLER_ARGS) { - SAVE_OPLINE(); - return &call_interrupt_op; -} -#endif #if (ZEND_VM_KIND == ZEND_VM_KIND_HYBRID) # undef ZEND_VM_TAIL_CALL @@ -56536,17 +56520,11 @@ static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_F # define ZEND_VM_TAIL_CALL(call) ZEND_MUSTTAIL return call # define ZEND_VM_CONTINUE() ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) # define ZEND_VM_RETURN() opline = &call_halt_op; ZEND_VM_CONTINUE() -# define ZEND_VM_DISPATCH_TO_HELPER(call) \ - do { \ - opline = call; \ - ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); \ - } while (0) +# define ZEND_VM_DISPATCH_TO_HELPER(call) return call # define ZEND_VM_DISPATCH_TO_LEAVE_HELPER(helper) opline = &call_leave_op; SAVE_OPLINE(); ZEND_VM_CONTINUE() -# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) +# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS); -static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt(ZEND_OPCODE_HANDLER_ARGS); -static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS); static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NULL_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS); static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_HALT_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS); static zend_never_inline const zend_op *ZEND_OPCODE_HANDLER_CCONV zend_leave_helper_SPEC_TAILCALL(zend_execute_data *ex, const zend_op *opline); @@ -56557,9 +56535,6 @@ static const zend_op call_halt_op = { static const zend_op call_leave_op = { .handler = zend_leave_helper_SPEC_TAILCALL, }; -static const zend_op call_interrupt_op = { - .handler = zend_interrupt_helper_SPEC_TAILCALL, -}; static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV_EX zend_add_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_EX zval *op_1, zval *op_2); static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV_EX zend_sub_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_EX zval *op_1, zval *op_2); @@ -59695,12 +59670,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_JMP_FO static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS) { zend_atomic_bool_store_ex(&EG(vm_interrupt), false); -#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL - /* opline is &call_interrupt_op. Load orig opline. */ - LOAD_OPLINE(); -#else SAVE_OPLINE(); -#endif if (zend_atomic_bool_load_ex(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { @@ -111888,17 +111858,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_HALT_TAILCALL_HAND return (zend_op*) ZEND_VM_ENTER_BIT; } -static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS) { - SAVE_OPLINE(); - ZEND_VM_TAIL_CALL(zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); -} /* The following helpers can not tailcall due to signature mismatch. Redefine some macros so they do not enforce tailcall. */ #pragma push_macro("ZEND_VM_CONTINUE") #undef ZEND_VM_CONTINUE #pragma push_macro("ZEND_VM_INTERRUPT") #undef ZEND_VM_INTERRUPT #define ZEND_VM_CONTINUE(handler) return opline -#define ZEND_VM_INTERRUPT() return zend_interrupt(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU) +#define ZEND_VM_INTERRUPT() return zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU) static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV_EX zend_add_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_EX zval *op_1, zval *op_2) { USE_OPLINE @@ -129294,11 +129260,7 @@ ZEND_API const zend_op *zend_get_halt_op(void) ZEND_API const zend_op *zend_get_interrupt_op(void) { -#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL - return &call_interrupt_op; -#else return NULL; -#endif } #if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL diff --git a/Zend/zend_vm_execute.skl b/Zend/zend_vm_execute.skl index f7bbfc99b578..45ca3c07144e 100644 --- a/Zend/zend_vm_execute.skl +++ b/Zend/zend_vm_execute.skl @@ -163,11 +163,7 @@ ZEND_API const zend_op *zend_get_halt_op(void) ZEND_API const zend_op *zend_get_interrupt_op(void) { -#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL - return &call_interrupt_op; -#else return NULL; -#endif } #if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL diff --git a/Zend/zend_vm_gen.php b/Zend/zend_vm_gen.php index 1ffa80f718dc..4f802d37557e 100755 --- a/Zend/zend_vm_gen.php +++ b/Zend/zend_vm_gen.php @@ -1594,19 +1594,6 @@ function gen_halt_handler($f, $kind) { out($f,"}\n\n"); } -function gen_interrupt_func($f, $kind, $spec) { - $cconv = $kind === ZEND_VM_KIND_TAILCALL ? 'ZEND_OPCODE_HANDLER_CCONV' : 'ZEND_OPCODE_HANDLER_FUNC_CCONV'; - $variant = $kind === ZEND_VM_KIND_TAILCALL ? '_TAILCALL' : ''; - out($f, "static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET {$cconv} zend_interrupt{$variant}(ZEND_OPCODE_HANDLER_ARGS) {\n"); - out($f,"\tSAVE_OPLINE();\n"); - if ($kind === ZEND_VM_KIND_TAILCALL) { - out($f,"\tZEND_VM_TAIL_CALL(zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));\n"); - } else { - out($f, "\treturn &call_interrupt_op;\n"); - } - out($f, "}\n"); -} - function extra_spec_name($extra_spec) { global $prefix; @@ -1817,14 +1804,10 @@ function gen_executor_code($f, $spec, $kind, $prolog, &$switch_labels = array()) switch ($kind) { case ZEND_VM_KIND_CALL: gen_null_handler($f, $kind); - out($f, "#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL\n"); - gen_interrupt_func($f, $kind, $spec); - out($f, "#endif\n"); break; case ZEND_VM_KIND_TAILCALL: gen_null_handler($f, $kind); gen_halt_handler($f, $kind); - gen_interrupt_func($f, $kind, $spec); break; case ZEND_VM_KIND_SWITCH: out($f,"default: ZEND_NULL_LABEL:\n"); @@ -1862,7 +1845,7 @@ function gen_executor_code($f, $spec, $kind, $prolog, &$switch_labels = array()) out($f, "#pragma push_macro(\"ZEND_VM_INTERRUPT\")\n"); out($f, "#undef ZEND_VM_INTERRUPT\n"); out($f, "#define ZEND_VM_CONTINUE(handler) return opline\n"); - out($f, "#define ZEND_VM_INTERRUPT() return zend_interrupt(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)\n"); + out($f, "#define ZEND_VM_INTERRUPT() return zend_interrupt_helper".($spec?"_SPEC":"")."(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)\n"); out($f, $delayed_helpers); out($f, "#pragma pop_macro(\"ZEND_VM_INTERRUPT\")\n"); out($f, "#pragma pop_macro(\"ZEND_VM_CONTINUE\")\n"); @@ -1917,10 +1900,7 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) if ($kind == ZEND_VM_KIND_HYBRID || $kind == ZEND_VM_KIND_CALL) { out($f,"#if ZEND_VM_KIND == ZEND_VM_KIND_HYBRID || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL\n\n"); out($f,"static zend_vm_opcode_handler_func_t const * zend_opcode_handler_funcs;\n"); - out($f,"#endif\n\n"); - out($f,"#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL\n"); - out($f,"static const zend_op call_interrupt_op;\n"); - out($f,"#endif\n\n"); + out($f,"#endif\n"); } out($f,"#if (ZEND_VM_KIND != ZEND_VM_KIND_HYBRID && ZEND_VM_KIND != ZEND_VM_KIND_TAILCALL) || !ZEND_VM_SPEC\n"); out($f,"static zend_vm_opcode_handler_t zend_vm_get_opcode_handler(uint8_t opcode, const zend_op* op);\n"); @@ -2153,17 +2133,11 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) out($f,"# define ZEND_VM_TAIL_CALL(call) ZEND_MUSTTAIL return call\n"); out($f,"# define ZEND_VM_CONTINUE() ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))\n"); out($f,"# define ZEND_VM_RETURN() opline = &call_halt_op; ZEND_VM_CONTINUE()\n"); - out($f,"# define ZEND_VM_DISPATCH_TO_HELPER(call) \\\n"); - out($f," do { \\\n"); - out($f," opline = call; \\\n"); - out($f," ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); \\\n"); - out($f," } while (0)\n"); + out($f,"# define ZEND_VM_DISPATCH_TO_HELPER(call) return call\n"); out($f,"# define ZEND_VM_DISPATCH_TO_LEAVE_HELPER(helper) opline = &call_leave_op; SAVE_OPLINE(); ZEND_VM_CONTINUE()\n"); - out($f,"# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))\n"); + out($f,"# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))\n"); out($f,"\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS);\n"); - out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt(ZEND_OPCODE_HANDLER_ARGS);\n"); - out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS);\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NULL_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS);\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_HALT_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS);\n"); out($f,"static zend_never_inline const zend_op *ZEND_OPCODE_HANDLER_CCONV zend_leave_helper_SPEC_TAILCALL(zend_execute_data *ex, const zend_op *opline);\n"); @@ -2174,9 +2148,6 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) out($f,"static const zend_op call_leave_op = {\n"); out($f," .handler = zend_leave_helper_SPEC_TAILCALL,\n"); out($f,"};\n"); - out($f,"static const zend_op call_interrupt_op = {\n"); - out($f," .handler = zend_interrupt_helper_SPEC_TAILCALL,\n"); - out($f,"};\n"); out($f,"\n"); gen_executor_code($f, $spec, ZEND_VM_KIND_TAILCALL, $m[1]); diff --git a/ext/opcache/jit/zend_jit_vm_helpers.c b/ext/opcache/jit/zend_jit_vm_helpers.c index 77035b936885..270e1f07388d 100644 --- a/ext/opcache/jit/zend_jit_vm_helpers.c +++ b/ext/opcache/jit/zend_jit_vm_helpers.c @@ -1073,7 +1073,7 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex, #else opline = handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); # if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL - while (UNEXPECTED(opline == zend_jit_interrupt_op)) { + while (UNEXPECTED(zend_jit_interrupt_op && opline == zend_jit_interrupt_op)) { opline = zend_vm_handle_interrupt(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); } # endif From 445c541d5a40dfa3d7c37bb0e71964f02ac8b01b Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 9 Jun 2026 17:02:04 -0600 Subject: [PATCH 3/5] SAVE_OPLINE before ZEND_VM_LOOP_INTERRUPT --- Zend/zend_execute.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 99e4cb131439..a90ba05c5fbd 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4304,6 +4304,7 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_fcall_interrupt(zend_execute_data *ca #define ZEND_VM_LOOP_INTERRUPT_CHECK() do { \ if (UNEXPECTED(zend_atomic_bool_load_ex(&EG(vm_interrupt)))) { \ + SAVE_OPLINE(); \ ZEND_VM_LOOP_INTERRUPT(); \ } \ } while (0) From 6d5f17316b4d74d7e5116bb8873f742595c4cf10 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 9 Jun 2026 17:02:47 -0600 Subject: [PATCH 4/5] Revert "Fix vm_interrupt for tailcall VM" This reverts commit a0f68b1db6ea77e2ca6bae700a7c80f9812ad3b6. --- Zend/zend_vm_def.h | 5 +++ Zend/zend_vm_execute.h | 44 +++++++++++++++++++++++++-- Zend/zend_vm_execute.skl | 4 +++ Zend/zend_vm_gen.php | 37 +++++++++++++++++++--- ext/opcache/jit/zend_jit_vm_helpers.c | 2 +- 5 files changed, 84 insertions(+), 8 deletions(-) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 6712625e83b1..fdfe48fc8421 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -10523,7 +10523,12 @@ ZEND_VM_DEFINE_OP(137, ZEND_OP_DATA); ZEND_VM_HELPER(zend_interrupt_helper, ANY, ANY) { zend_atomic_bool_store_ex(&EG(vm_interrupt), false); +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + /* opline is &call_interrupt_op. Load orig opline. */ + LOAD_OPLINE(); +#else SAVE_OPLINE(); +#endif if (zend_atomic_bool_load_ex(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 21c666aabebd..0faa84444be3 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -331,6 +331,11 @@ static zend_op hybrid_halt_op; static zend_vm_opcode_handler_func_t const * zend_opcode_handler_funcs; #endif + +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL +static const zend_op call_interrupt_op; +#endif + #if (ZEND_VM_KIND != ZEND_VM_KIND_HYBRID && ZEND_VM_KIND != ZEND_VM_KIND_TAILCALL) || !ZEND_VM_SPEC static zend_vm_opcode_handler_t zend_vm_get_opcode_handler(uint8_t opcode, const zend_op* op); #endif @@ -4011,7 +4016,12 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_J static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS) { zend_atomic_bool_store_ex(&EG(vm_interrupt), false); +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + /* opline is &call_interrupt_op. Load orig opline. */ + LOAD_OPLINE(); +#else SAVE_OPLINE(); +#endif if (zend_atomic_bool_load_ex(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { @@ -56498,6 +56508,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NULL_HANDLER( ZEND_VM_NEXT_OPCODE(); /* Never reached */ } +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL +static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt(ZEND_OPCODE_HANDLER_ARGS) { + SAVE_OPLINE(); + return &call_interrupt_op; +} +#endif #if (ZEND_VM_KIND == ZEND_VM_KIND_HYBRID) # undef ZEND_VM_TAIL_CALL @@ -56520,11 +56536,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NULL_HANDLER( # define ZEND_VM_TAIL_CALL(call) ZEND_MUSTTAIL return call # define ZEND_VM_CONTINUE() ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) # define ZEND_VM_RETURN() opline = &call_halt_op; ZEND_VM_CONTINUE() -# define ZEND_VM_DISPATCH_TO_HELPER(call) return call +# define ZEND_VM_DISPATCH_TO_HELPER(call) \ + do { \ + opline = call; \ + ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); \ + } while (0) # define ZEND_VM_DISPATCH_TO_LEAVE_HELPER(helper) opline = &call_leave_op; SAVE_OPLINE(); ZEND_VM_CONTINUE() -# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) +# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS); +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt(ZEND_OPCODE_HANDLER_ARGS); +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS); static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NULL_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS); static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_HALT_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS); static zend_never_inline const zend_op *ZEND_OPCODE_HANDLER_CCONV zend_leave_helper_SPEC_TAILCALL(zend_execute_data *ex, const zend_op *opline); @@ -56535,6 +56557,9 @@ static const zend_op call_halt_op = { static const zend_op call_leave_op = { .handler = zend_leave_helper_SPEC_TAILCALL, }; +static const zend_op call_interrupt_op = { + .handler = zend_interrupt_helper_SPEC_TAILCALL, +}; static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV_EX zend_add_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_EX zval *op_1, zval *op_2); static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV_EX zend_sub_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_EX zval *op_1, zval *op_2); @@ -59670,7 +59695,12 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_JMP_FO static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS) { zend_atomic_bool_store_ex(&EG(vm_interrupt), false); +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + /* opline is &call_interrupt_op. Load orig opline. */ + LOAD_OPLINE(); +#else SAVE_OPLINE(); +#endif if (zend_atomic_bool_load_ex(&EG(timed_out))) { zend_timeout(); } else if (zend_interrupt_function) { @@ -111858,13 +111888,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_HALT_TAILCALL_HAND return (zend_op*) ZEND_VM_ENTER_BIT; } +static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS) { + SAVE_OPLINE(); + ZEND_VM_TAIL_CALL(zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); +} /* The following helpers can not tailcall due to signature mismatch. Redefine some macros so they do not enforce tailcall. */ #pragma push_macro("ZEND_VM_CONTINUE") #undef ZEND_VM_CONTINUE #pragma push_macro("ZEND_VM_INTERRUPT") #undef ZEND_VM_INTERRUPT #define ZEND_VM_CONTINUE(handler) return opline -#define ZEND_VM_INTERRUPT() return zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU) +#define ZEND_VM_INTERRUPT() return zend_interrupt(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU) static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV_EX zend_add_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_EX zval *op_1, zval *op_2) { USE_OPLINE @@ -129260,7 +129294,11 @@ ZEND_API const zend_op *zend_get_halt_op(void) ZEND_API const zend_op *zend_get_interrupt_op(void) { +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + return &call_interrupt_op; +#else return NULL; +#endif } #if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL diff --git a/Zend/zend_vm_execute.skl b/Zend/zend_vm_execute.skl index 45ca3c07144e..f7bbfc99b578 100644 --- a/Zend/zend_vm_execute.skl +++ b/Zend/zend_vm_execute.skl @@ -163,7 +163,11 @@ ZEND_API const zend_op *zend_get_halt_op(void) ZEND_API const zend_op *zend_get_interrupt_op(void) { +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL + return &call_interrupt_op; +#else return NULL; +#endif } #if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL diff --git a/Zend/zend_vm_gen.php b/Zend/zend_vm_gen.php index 4f802d37557e..1ffa80f718dc 100755 --- a/Zend/zend_vm_gen.php +++ b/Zend/zend_vm_gen.php @@ -1594,6 +1594,19 @@ function gen_halt_handler($f, $kind) { out($f,"}\n\n"); } +function gen_interrupt_func($f, $kind, $spec) { + $cconv = $kind === ZEND_VM_KIND_TAILCALL ? 'ZEND_OPCODE_HANDLER_CCONV' : 'ZEND_OPCODE_HANDLER_FUNC_CCONV'; + $variant = $kind === ZEND_VM_KIND_TAILCALL ? '_TAILCALL' : ''; + out($f, "static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET {$cconv} zend_interrupt{$variant}(ZEND_OPCODE_HANDLER_ARGS) {\n"); + out($f,"\tSAVE_OPLINE();\n"); + if ($kind === ZEND_VM_KIND_TAILCALL) { + out($f,"\tZEND_VM_TAIL_CALL(zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));\n"); + } else { + out($f, "\treturn &call_interrupt_op;\n"); + } + out($f, "}\n"); +} + function extra_spec_name($extra_spec) { global $prefix; @@ -1804,10 +1817,14 @@ function gen_executor_code($f, $spec, $kind, $prolog, &$switch_labels = array()) switch ($kind) { case ZEND_VM_KIND_CALL: gen_null_handler($f, $kind); + out($f, "#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL\n"); + gen_interrupt_func($f, $kind, $spec); + out($f, "#endif\n"); break; case ZEND_VM_KIND_TAILCALL: gen_null_handler($f, $kind); gen_halt_handler($f, $kind); + gen_interrupt_func($f, $kind, $spec); break; case ZEND_VM_KIND_SWITCH: out($f,"default: ZEND_NULL_LABEL:\n"); @@ -1845,7 +1862,7 @@ function gen_executor_code($f, $spec, $kind, $prolog, &$switch_labels = array()) out($f, "#pragma push_macro(\"ZEND_VM_INTERRUPT\")\n"); out($f, "#undef ZEND_VM_INTERRUPT\n"); out($f, "#define ZEND_VM_CONTINUE(handler) return opline\n"); - out($f, "#define ZEND_VM_INTERRUPT() return zend_interrupt_helper".($spec?"_SPEC":"")."(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)\n"); + out($f, "#define ZEND_VM_INTERRUPT() return zend_interrupt(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)\n"); out($f, $delayed_helpers); out($f, "#pragma pop_macro(\"ZEND_VM_INTERRUPT\")\n"); out($f, "#pragma pop_macro(\"ZEND_VM_CONTINUE\")\n"); @@ -1900,7 +1917,10 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) if ($kind == ZEND_VM_KIND_HYBRID || $kind == ZEND_VM_KIND_CALL) { out($f,"#if ZEND_VM_KIND == ZEND_VM_KIND_HYBRID || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL\n\n"); out($f,"static zend_vm_opcode_handler_func_t const * zend_opcode_handler_funcs;\n"); - out($f,"#endif\n"); + out($f,"#endif\n\n"); + out($f,"#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL\n"); + out($f,"static const zend_op call_interrupt_op;\n"); + out($f,"#endif\n\n"); } out($f,"#if (ZEND_VM_KIND != ZEND_VM_KIND_HYBRID && ZEND_VM_KIND != ZEND_VM_KIND_TAILCALL) || !ZEND_VM_SPEC\n"); out($f,"static zend_vm_opcode_handler_t zend_vm_get_opcode_handler(uint8_t opcode, const zend_op* op);\n"); @@ -2133,11 +2153,17 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) out($f,"# define ZEND_VM_TAIL_CALL(call) ZEND_MUSTTAIL return call\n"); out($f,"# define ZEND_VM_CONTINUE() ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))\n"); out($f,"# define ZEND_VM_RETURN() opline = &call_halt_op; ZEND_VM_CONTINUE()\n"); - out($f,"# define ZEND_VM_DISPATCH_TO_HELPER(call) return call\n"); + out($f,"# define ZEND_VM_DISPATCH_TO_HELPER(call) \\\n"); + out($f," do { \\\n"); + out($f," opline = call; \\\n"); + out($f," ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); \\\n"); + out($f," } while (0)\n"); out($f,"# define ZEND_VM_DISPATCH_TO_LEAVE_HELPER(helper) opline = &call_leave_op; SAVE_OPLINE(); ZEND_VM_CONTINUE()\n"); - out($f,"# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))\n"); + out($f,"# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))\n"); out($f,"\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS);\n"); + out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_interrupt(ZEND_OPCODE_HANDLER_ARGS);\n"); + out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS);\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NULL_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS);\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_HALT_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS);\n"); out($f,"static zend_never_inline const zend_op *ZEND_OPCODE_HANDLER_CCONV zend_leave_helper_SPEC_TAILCALL(zend_execute_data *ex, const zend_op *opline);\n"); @@ -2148,6 +2174,9 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) out($f,"static const zend_op call_leave_op = {\n"); out($f," .handler = zend_leave_helper_SPEC_TAILCALL,\n"); out($f,"};\n"); + out($f,"static const zend_op call_interrupt_op = {\n"); + out($f," .handler = zend_interrupt_helper_SPEC_TAILCALL,\n"); + out($f,"};\n"); out($f,"\n"); gen_executor_code($f, $spec, ZEND_VM_KIND_TAILCALL, $m[1]); diff --git a/ext/opcache/jit/zend_jit_vm_helpers.c b/ext/opcache/jit/zend_jit_vm_helpers.c index 270e1f07388d..77035b936885 100644 --- a/ext/opcache/jit/zend_jit_vm_helpers.c +++ b/ext/opcache/jit/zend_jit_vm_helpers.c @@ -1073,7 +1073,7 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex, #else opline = handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); # if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL - while (UNEXPECTED(zend_jit_interrupt_op && opline == zend_jit_interrupt_op)) { + while (UNEXPECTED(opline == zend_jit_interrupt_op)) { opline = zend_vm_handle_interrupt(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); } # endif From c1599b567fe466ebf607f82a8907392d11622196 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 9 Jun 2026 17:17:31 -0600 Subject: [PATCH 5/5] Update NEWS --- NEWS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS b/NEWS index f09487c5da06..08d381209f8e 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,9 @@ PHP NEWS LXB_API as __declspec(dllimport) when linked statically into PHP. (Luther Monson) +- Opcache: + . Fixed bug GH-22265 (Another tailcall vm_interrupt bug). (Levi Morrison) + - Phar: . Fixed a bypass of the magic ".phar" directory protection in Phar::addEmptyDir() for paths starting with "/.phar", while allowing