Commit 06bfe158a52 for php.net
commit 06bfe158a52fb79077e46328f4c1a44f0de4e8b0
Author: Levi Morrison <levi.morrison@datadoghq.com>
Date: Thu Apr 30 10:07:12 2026 -0600
Fix tailcall helper dispatch after VM interrupt
In the tailcall VM, helpers which have extra args return the next opline instead
of tailcalling it, relying on the parent to call it in
ZEND_VM_DISPATCH_TO_HELPER(). When an interrupt is handled in the helper the
opline may be tagged with ZEND_VM_ENTER_BIT, but ZEND_VM_DISPATCH_TO_HELPER()
assumes an untagged opline.
This PR changes ZEND_VM_INTERRUPT() so that such helpers return an opline whose
handler is zend_interrupt_helper, instead of executing it directly.
Closes GH-21922
Co-Authored-By: Arnaud Le Blanc <arnaud.lb@gmail.com>
diff --git a/NEWS b/NEWS
index 7d4f5cde9fe..0abc46c5907 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,8 @@ PHP NEWS
. Fixed bug GH-21746 (Segfault with tracing JIT). (Arnaud)
. Fixed bug GH-22004 (Assertion failure at ext/opcache/jit/zend_jit_trace.c).
(Arnaud)
+ . Fixed tailcall VM crash when a VM interrupt is handled from a VM helper.
+ (Levi Morrison, Arnaud)
- OpenSSL:
. Fix compatibility issues with OpenSSL 4.0. (jordikroon, Remi)
diff --git a/Zend/zend_vm.h b/Zend/zend_vm.h
index c9c41c75c72..0727fdb15c7 100644
--- a/Zend/zend_vm.h
+++ b/Zend/zend_vm.h
@@ -32,6 +32,7 @@ ZEND_API void ZEND_FASTCALL zend_serialize_opcode_handler(zend_op *op);
ZEND_API void ZEND_FASTCALL zend_deserialize_opcode_handler(zend_op *op);
ZEND_API const void* ZEND_FASTCALL zend_get_opcode_handler_func(const zend_op *op);
ZEND_API const zend_op *zend_get_halt_op(void);
+ZEND_API const zend_op *zend_get_interrupt_op(void);
ZEND_API int ZEND_FASTCALL zend_vm_call_opcode_handler(zend_execute_data *ex);
ZEND_API int zend_vm_kind(void);
ZEND_API bool zend_gcc_global_regs(void);
@@ -39,6 +40,10 @@ ZEND_API bool zend_gcc_global_regs(void);
void zend_vm_init(void);
void zend_vm_dtor(void);
+#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL
+const struct _zend_op *zend_vm_handle_interrupt(struct _zend_execute_data *execute_data, const struct _zend_op *opline);
+#endif
+
END_EXTERN_C()
#define ZEND_VM_SET_OPCODE_HANDLER(opline) zend_vm_set_opcode_handler(opline)
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 6712625e83b..fdfe48fc842 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 0d11a17ce78..0faa84444be 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ
diff --git a/Zend/zend_vm_execute.skl b/Zend/zend_vm_execute.skl
index 53b1ac6baf0..f7bbfc99b57 100644
--- a/Zend/zend_vm_execute.skl
+++ b/Zend/zend_vm_execute.skl
@@ -161,6 +161,22 @@ ZEND_API const zend_op *zend_get_halt_op(void)
#endif
}
+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
+ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_vm_handle_interrupt(ZEND_OPCODE_HANDLER_ARGS)
+{
+ return zend_interrupt_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);
+}
+#endif
+
ZEND_API int zend_vm_kind(void)
{
return ZEND_VM_KIND;
diff --git a/Zend/zend_vm_gen.php b/Zend/zend_vm_gen.php
index 38c20b24da2..1ffa80f718d 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");
@@ -2139,9 +2159,11 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name)
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");
@@ -2152,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.c b/ext/opcache/jit/zend_jit.c
index 41cb0176141..5de9c81484b 100644
--- a/ext/opcache/jit/zend_jit.c
+++ b/ext/opcache/jit/zend_jit.c
@@ -78,6 +78,7 @@ int zend_jit_profile_counter_rid = -1;
int16_t zend_jit_hot_counters[ZEND_HOT_COUNTERS_COUNT];
const zend_op *zend_jit_halt_op = NULL;
+const zend_op *zend_jit_interrupt_op = NULL;
#ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP
static int zend_write_protect = 1;
#endif
@@ -3777,6 +3778,7 @@ int zend_jit_check_support(void)
void zend_jit_startup(void *buf, size_t size, bool reattached)
{
zend_jit_halt_op = zend_get_halt_op();
+ zend_jit_interrupt_op = zend_get_interrupt_op();
zend_jit_profile_counter_rid = zend_get_op_array_extension_handle(ACCELERATOR_PRODUCT_NAME);
#ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP
diff --git a/ext/opcache/jit/zend_jit_internal.h b/ext/opcache/jit/zend_jit_internal.h
index 57c0dedb2fa..8732b424f00 100644
--- a/ext/opcache/jit/zend_jit_internal.h
+++ b/ext/opcache/jit/zend_jit_internal.h
@@ -177,6 +177,7 @@ typedef struct _zend_jit_op_array_hot_extension {
zend_jit_hash((op_array)->opcodes)
extern const zend_op *zend_jit_halt_op;
+extern const zend_op *zend_jit_interrupt_op;
#ifdef HAVE_GCC_GLOBAL_REGS
# define EXECUTE_DATA_D void
diff --git a/ext/opcache/jit/zend_jit_vm_helpers.c b/ext/opcache/jit/zend_jit_vm_helpers.c
index 44f46feb5f6..77035b93688 100644
--- a/ext/opcache/jit/zend_jit_vm_helpers.c
+++ b/ext/opcache/jit/zend_jit_vm_helpers.c
@@ -1072,6 +1072,11 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex,
if (UNEXPECTED(opline == zend_jit_halt_op)) {
#else
opline = handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);
+# if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL
+ while (UNEXPECTED(opline == zend_jit_interrupt_op)) {
+ opline = zend_vm_handle_interrupt(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);
+ }
+# endif
if (UNEXPECTED(((uintptr_t)opline & ~ZEND_VM_ENTER_BIT) == 0)) {
#endif
if (prev_opline->opcode == ZEND_YIELD || prev_opline->opcode == ZEND_YIELD_FROM) {
diff --git a/ext/zend_test/object_handlers.c b/ext/zend_test/object_handlers.c
index 15e362605f8..9163c2f416a 100644
--- a/ext/zend_test/object_handlers.c
+++ b/ext/zend_test/object_handlers.c
@@ -232,6 +232,44 @@ ZEND_METHOD(NumericCastableNoOperations, __construct)
ZVAL_COPY(OBJ_PROP_NUM(Z_OBJ_P(ZEND_THIS), 0), n);
}
+static zend_class_entry *vm_interrupt_comparable_ce;
+static zend_object_handlers vm_interrupt_comparable_object_handlers;
+
+static zend_object* vm_interrupt_comparable_object_create_ex(zend_class_entry* ce, zend_long l) {
+ zend_object *obj = zend_objects_new(ce);
+ object_properties_init(obj, ce);
+ obj->handlers = &vm_interrupt_comparable_object_handlers;
+ ZVAL_LONG(OBJ_PROP_NUM(obj, 0), l);
+ return obj;
+}
+
+static zend_object *vm_interrupt_comparable_object_create(zend_class_entry *ce)
+{
+ return vm_interrupt_comparable_object_create_ex(ce, 0);
+}
+
+static int vm_interrupt_comparable_compare(zval *op1, zval *op2)
+{
+ ZEND_COMPARE_OBJECTS_FALLBACK(op1, op2);
+
+ zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
+
+ return ZEND_THREEWAY_COMPARE(
+ Z_LVAL_P(OBJ_PROP_NUM(Z_OBJ_P(op1), 0)),
+ Z_LVAL_P(OBJ_PROP_NUM(Z_OBJ_P(op2), 0)));
+}
+
+ZEND_METHOD(VmInterruptComparable, __construct)
+{
+ zend_long l;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_LONG(l)
+ ZEND_PARSE_PARAMETERS_END();
+
+ ZVAL_LONG(OBJ_PROP_NUM(Z_OBJ_P(ZEND_THIS), 0), l);
+}
+
static zend_class_entry *dimension_handlers_no_ArrayAccess_ce;
static zend_object_handlers dimension_handlers_no_ArrayAccess_object_handlers;
@@ -302,6 +340,11 @@ void zend_test_object_handlers_init(void)
memcpy(&numeric_castable_no_operation_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
numeric_castable_no_operation_object_handlers.cast_object = numeric_castable_no_operation_cast_object;
+ vm_interrupt_comparable_ce = register_class_VmInterruptComparable();
+ vm_interrupt_comparable_ce->create_object = vm_interrupt_comparable_object_create;
+ memcpy(&vm_interrupt_comparable_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
+ vm_interrupt_comparable_object_handlers.compare = vm_interrupt_comparable_compare;
+
dimension_handlers_no_ArrayAccess_ce = register_class_DimensionHandlersNoArrayAccess();
dimension_handlers_no_ArrayAccess_ce->create_object = dimension_handlers_no_ArrayAccess_object_create;
memcpy(&dimension_handlers_no_ArrayAccess_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
diff --git a/ext/zend_test/object_handlers.stub.php b/ext/zend_test/object_handlers.stub.php
index a474908b109..8c8e7f9bfe9 100644
--- a/ext/zend_test/object_handlers.stub.php
+++ b/ext/zend_test/object_handlers.stub.php
@@ -23,6 +23,11 @@ final class NumericCastableNoOperations {
public function __construct(int|float $val) {}
}
+final class VmInterruptComparable {
+ private int $val;
+ public function __construct(int $val) {}
+}
+
class DimensionHandlersNoArrayAccess {
public bool $read = false;
public bool $write = false;
diff --git a/ext/zend_test/object_handlers_arginfo.h b/ext/zend_test/object_handlers_arginfo.h
index 370ad13894a..b46028a6e01 100644
Binary files a/ext/zend_test/object_handlers_arginfo.h and b/ext/zend_test/object_handlers_arginfo.h differ
diff --git a/ext/zend_test/tests/observer_vm_interrupt_tailcall_helper.phpt b/ext/zend_test/tests/observer_vm_interrupt_tailcall_helper.phpt
new file mode 100644
index 00000000000..d0178bdbf61
--- /dev/null
+++ b/ext/zend_test/tests/observer_vm_interrupt_tailcall_helper.phpt
@@ -0,0 +1,26 @@
+--TEST--
+Observer: VM interrupt during tailcall helper dispatch
+--DESCRIPTION--
+This exercises a VM interrupt raised while an opcode handler dispatches to an
+extra-argument helper. On the tailcall VM, the helper may return an opline
+tagged with ZEND_VM_ENTER_BIT; treating that tagged value as a zend_op * before
+tailcalling the next handler can crash.
+--EXTENSIONS--
+zend_test
+--INI--
+opcache.jit=0
+zend_test.observer.set_vm_interrupt_on_begin=1
+--FILE--
+<?php
+function trigger(VmInterruptComparable $left, VmInterruptComparable $right): object
+{
+ if ($left < $right) {
+ return new Exception();
+ }
+ return new stdClass();
+}
+
+echo get_class(trigger(new VmInterruptComparable(2), new VmInterruptComparable(1))), "\n";
+?>
+--EXPECT--
+stdClass