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
3 changes: 3 additions & 0 deletions Zend/zend_vm_execute.h

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

3 changes: 3 additions & 0 deletions Zend/zend_vm_gen.php
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,9 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name)
out($f,"# define ZEND_VM_DISPATCH_TO_HELPER(call) \\\n");
out($f," do { \\\n");
out($f," opline = call; \\\n");
out($f," if (UNEXPECTED(((uintptr_t)opline & ZEND_VM_ENTER_BIT))) { \\\n");
out($f," return opline; \\\n");
out($f," } \\\n");
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

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");
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