Skip to content

JIT: fix vm_interrupt resumption in TAILCALL/CALL VMs - #24793

Merged
arnaud-lb merged 4 commits into
php:PHP-8.5from
morrisonlevi:tailcall_vm_interrupt_jit
Oct 28, 2026
Merged

arnaud-lb merged 4 commits into
php:PHP-8.5from
morrisonlevi:tailcall_vm_interrupt_jit

Conversation

@morrisonlevi

@morrisonlevi morrisonlevi commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

This contains two JIT fixes for vm_interrupts on TAILCALL VMs (and the 2nd also affects the CALL VM too).


On the TAILCALL VM, the JIT (both function and tracing) can resume at the wrong opline after it handles a VM interrupt. This re-runs an opcode that has already executed. Depending on that opcode, you get a wrong result or a crash; for example, re-running a call opcode whose call frame has already been used up.

This is a follow-up to #21922.


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.

@morrisonlevi
morrisonlevi changed the base branch from master to PHP-8.5 October 27, 2026 15:41
@morrisonlevi
morrisonlevi marked this pull request as ready for review October 27, 2026 15:53
@morrisonlevi
morrisonlevi requested a review from dstogov as a code owner October 27, 2026 15:53
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.
@morrisonlevi morrisonlevi changed the title JIT: fix vm_interrupt on TAILCALL VM JIT: fix vm_interrupt resumption in CALL/TAILCALL VMs Oct 27, 2026
@morrisonlevi morrisonlevi changed the title JIT: fix vm_interrupt resumption in CALL/TAILCALL VMs JIT: fix vm_interrupt resumption in TAILCALL/CALL VMs Oct 27, 2026

@bwoebi bwoebi left a comment

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.

These fixes look right to me. Thanks!
Not very nice to have to &~ZEND_VM_ENTER bit in every JIT handler call, but I guess nothing we can avoid here.

Comment thread Zend/zend_vm_gen.php
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");

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.

Alternatively we can handle this in zend_interrupt_helper:

@@ -10538,8 +10538,11 @@ 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();
+       if (opline == &call_interrupt_op) {
+               LOAD_OPLINE();
+       } else {
+               SAVE_OPLINE();
+       }
 #else
        SAVE_OPLINE();
 #endif

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.

Does that win us anything? The current patch only adds the unconditional save when it's anyway tailcalling to the interrupt helper. And here you introduce a branch.

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.

It doesn't win us much, it just reduces code size slightly.

@arnaud-lb

arnaud-lb commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Not very nice to have to &~ZEND_VM_ENTER bit in every JIT handler call, but I guess nothing we can avoid here.

Yes it's sad that we have to do that. The bit is set in case the interrupt function switches frame, but I don't think we ever do that, and the JIT would break if we did. Maybe we could set the flag only when the interrupt function actually did switch frame? The JIT assumes it never does, so we could also assume that the flag is never set.

Not a change for this branch however.

Edit: could we at least do that only when there is an interrupt function?

@morrisonlevi

Copy link
Copy Markdown
Contributor Author

@arnaud-lb and @bwoebi please take another look! It now does a if (zend_interrupt_function) check at compile-time and avoids the ptr & ~ZEND_VM_ENTER unless an interrupt function has been set.

@arnaud-lb
arnaud-lb merged commit 8975c59 into php:PHP-8.5 Oct 28, 2026
18 checks passed
arnaud-lb added a commit that referenced this pull request Oct 28, 2026
* PHP-8.6:
  JIT: fix vm_interrupt resumption in TAILCALL/CALL VMs (#24793)
@arnaud-lb

Copy link
Copy Markdown
Member

Thank you!

opensource-fork-sync Bot pushed a commit to cla7aye15I4nd/opensource-php__php-src that referenced this pull request Oct 28, 2026
* PHP-8.5:
  JIT: fix vm_interrupt resumption in TAILCALL/CALL VMs (php#24793)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants