Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
27 changes: 25 additions & 2 deletions Zend/zend_vm_execute.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions Zend/zend_vm_gen.php
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,13 @@ function gen_halt_handler($f, $kind) {
out($f,"}\n\n");
}

function gen_interrupt_func($f, $kind, $spec) {
out($f, "static ZEND_COLD zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS) {\n");
out($f,"\tSAVE_OPLINE();\n");
out($f,"\tZEND_VM_TAIL_CALL(zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));\n");
out($f, "}\n");
}

function extra_spec_name($extra_spec) {
global $prefix;

Expand Down Expand Up @@ -1808,6 +1815,7 @@ function gen_executor_code($f, $spec, $kind, $prolog, &$switch_labels = array())
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");
Expand Down Expand Up @@ -1845,7 +1853,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() SAVE_OPLINE(); return &call_interrupt_op;\n");
Copy link
Copy Markdown
Member

@arnaud-lb arnaud-lb May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems equivalent to return zend_interrupt_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU). The advantage of this form (return zend_interrupt_TAILCALL(...)) is that it reduces code size slightly compared to an inline SAVE_OPLINE() + return.

out($f, $delayed_helpers);
out($f, "#pragma pop_macro(\"ZEND_VM_INTERRUPT\")\n");
out($f, "#pragma pop_macro(\"ZEND_VM_CONTINUE\")\n");
Expand Down Expand Up @@ -1901,6 +1909,9 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name)
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,"#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL\n\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");
Expand Down Expand Up @@ -2139,9 +2150,10 @@ 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");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the alternative would be returning instead of tail-calling here. But that might be slower.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts @arnaud-lb ?

Copy link
Copy Markdown
Member

@arnaud-lb arnaud-lb May 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this would work. Possibly this may result in worse branch prediction as we revert to the main loop's central dispatch point.

Another alternative would be to make ZEND_VM_INTERRUPT() return an opline whose handler is zend_interrupt_helper, like we do for halt_op in the HYBRID and TAILCALL VMs: https://github.com/php/php-src/compare/arnaud-lb:vm-interrupt-tailcall-repro-3. This probably requires special handling in zend_jit_trace_execute(), I haven't investigated.

I've benchmarked the 3 approaches:

  1. check ENTER_BIT
  2. return opline
  3. return interrupt_op

Results:

Symfony:

base:  mean:  0.4459;  stddev:  0.0004;  diff:  -0.00%                       
1   :  mean:  0.4439;  stddev:  0.0004;  diff:  -0.45%;  p-value:  0.001000  (strong)
2   :  mean:  0.4471;  stddev:  0.0003;  diff:  +0.25%;  p-value:  0.001000  (strong)
3   :  mean:  0.4436;  stddev:  0.0003;  diff:  -0.52%;  p-value:  0.001000  (strong)

Symfony (valgrind):

base:  diff:  +0.00%
1   :  diff:  +0.12%
2   :  diff:  +0.01%
3   :  diff:  -0.00%

bench.php:

base:  mean:  0.8044;  stddev:  0.0004;  diff:  -0.00%                       
1   :  mean:  0.8161;  stddev:  0.0005;  diff:  +1.45%;  p-value:  0.001000  (strong)
2   :  mean:  0.8142;  stddev:  0.0016;  diff:  +1.22%;  p-value:  0.001000  (strong)
3   :  mean:  0.8043;  stddev:  0.0006;  diff:  -0.01%;  p-value:  0.690382  (weak)

bench.php (valgrind):

base:  diff:  +0.00%
1   :  diff:  +1.03%
2   :  diff:  -0.50%
3   :  diff:  -0.00%

The Symfony results do not make sense to me, but these are stable.

Approach 3 seems better overall, speed-wise, assuming that we don't find issues with it.

Copy link
Copy Markdown
Member

@bwoebi bwoebi May 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh, that's a nice approach! Love it.

I don't see any issues, that approach will never have &call_interrupt_op show up in EX(opline) or the VM IP register so that's perfect.
I have no idea about the JIT code, so, I'll leave that to you :-D

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pulled in your changes in the 3rd approach to let CI run while I review it in more dept (first glance looks good).

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_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");
Expand All @@ -2152,6 +2164,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]);
Expand Down
43 changes: 43 additions & 0 deletions ext/zend_test/object_handlers.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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));
Expand Down
5 changes: 5 additions & 0 deletions ext/zend_test/object_handlers.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 25 additions & 1 deletion ext/zend_test/object_handlers_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions ext/zend_test/tests/observer_vm_interrupt_tailcall_helper.phpt
Original file line number Diff line number Diff line change
@@ -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
Loading