Skip to content

JIT: fix lost register-allocated CVs on interrupt at the end of a loop trace - #24610

Merged
arnaud-lb merged 4 commits into
php:PHP-8.4from
morrisonlevi:jit-loop-interrupt-deopt-8.4
Oct 27, 2026
Merged

arnaud-lb merged 4 commits into
php:PHP-8.4from
morrisonlevi:jit-loop-interrupt-deopt-8.4

Conversation

@morrisonlevi

@morrisonlevi morrisonlevi commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

AI disclosure: I hit this bug in the Datadog PHP profiler CI when running ext/opcache/tests/jit/gh20838.phpt, and I used AI to debug it, write the patch, and the test. I am certainly not a JIT expert, but I have reviewed the code to the best of my ability.


When closing a loop trace that uses the initial IP, the interrupt check either jumps to a deoptimizing exit at the loop header or directly to jit_stub_interrupt_handler. The choice was made with zend_jit_trace_stack_needs_deoptimization, which only checks STACK_REG and the slot flags. A register-allocated variable is tracked during compilation by its IR ref only, and STACK_REG is still ZREG_NONE, so the stub was chosen even when a CV lived only in a register. If EG(vm_interrupt) was raised during the iteration without a function call (e.g. by a signal handler or another thread), the VM resumed the loop with a stale CV value.

Also check for refs that were not stored to memory, using the same criterion as the link-to-root path.

VmInterruptComparable is backported to zend_test from 06bfe15 to raise the interrupt deterministically. This is going to cause a merge-conflict when merging forward to PHP 8.5.

When closing a loop trace that uses the initial IP, the interrupt check
either jumps to a deoptimizing exit at the loop header or directly to
jit_stub_interrupt_handler. The choice was made with
zend_jit_trace_stack_needs_deoptimization(), which only checks
STACK_REG() and the slot flags. A register-allocated variable is
tracked during compilation by its IR ref only, and STACK_REG() is still
ZREG_NONE, so the stub was chosen even when a CV lived only in a
register. If EG(vm_interrupt) was raised during the iteration without a
function call (e.g. by a signal handler or another thread), the VM
resumed the loop with a stale CV value.

Also check for refs that were not stored to memory, using the same
criterion as the link-to-root path.

VmInterruptComparable is backported to zend_test from 06bfe15 to
raise the interrupt deterministically.
@morrisonlevi
morrisonlevi marked this pull request as ready for review October 23, 2026 17:54
@morrisonlevi morrisonlevi changed the title Fix lost register-allocated CVs on interrupt at the end of a loop trace JIT: fix lost register-allocated CVs on interrupt at the end of a loop trace Oct 24, 2026
@arnaud-lb

Copy link
Copy Markdown
Member

This looks right and the fix does look necessary, but I wasn't able to reproduce the issue and the test passes without the fix. Does it rely on a specific VM/version?

@morrisonlevi

Copy link
Copy Markdown
Contributor Author

I hit this in CI on arm64 nodes (I think AWS Graviton?) and reproduced it on my Apple M5. There should be an issue on x86_64 but there are difference in calling conventions and such so maybe this specific test won't reproduce it. I'll see if I can get my agent to alter the test to get it failing on both architectures, or possibly write two tests.

@morrisonlevi

Copy link
Copy Markdown
Contributor Author

@arnaud-lb I've altered the test to make it fail on x86_64 as well and updated its description. I've also added a NEWS entry for PHP 8.4. You should be able to reproduce the issue more easily now!

@arnaud-lb
arnaud-lb merged commit 89f1fda into php:PHP-8.4 Oct 27, 2026
18 checks passed
arnaud-lb added a commit that referenced this pull request Oct 27, 2026
* PHP-8.6:
  JIT: fix lost register-allocated CVs on interrupt at the end of a loop trace (#24610)
@arnaud-lb

Copy link
Copy Markdown
Member

Thank you!

Truta446 pushed a commit to Truta446/php-src that referenced this pull request Oct 27, 2026
* PHP-8.4:
  JIT: fix lost register-allocated CVs on interrupt at the end of a loop trace (php#24610)
Truta446 pushed a commit to Truta446/php-src that referenced this pull request Oct 27, 2026
* PHP-8.5:
  JIT: fix lost register-allocated CVs on interrupt at the end of a loop trace (php#24610)
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.

2 participants