Skip to content

Fix JIT stub veneers being reused after an OPcache restart on AArch64 - #24273

Open
pavimus wants to merge 1 commit into
php:PHP-8.4from
pavimus:fix-jit-aarch64-restart-veneers
Open

pavimus wants to merge 1 commit into
php:PHP-8.4from
pavimus:fix-jit-aarch64-restart-veneers

Conversation

@pavimus

@pavimus pavimus commented Oct 12, 2026

Copy link
Copy Markdown

Prepared with an AI assistant (Claude), which also ran the reproduction and the tests on my machine; I reviewed the result. The text below is AI-generated (quoted, as asked in CONTRIBUTING.md).

While testing OPcache restarts with tracing JIT on AArch64 we found crashes caused by a JIT issue that is independent of threading.

On AArch64 a branch from JIT code to a stub can be out of range (conditional branches reach
±1 MB), and IR emits a veneer next to the new code. zend_jit_set_veneer() /
zend_jit_commit_veneers() then replace the address of the stub in zend_jit_stub_handlers[]
by the address of that veneer, so that later code reuses it.

These veneers live after the JIT restart point (dasm_ptr[1]). zend_jit_restart() rewinds the
buffer to that point but leaves zend_jit_stub_handlers[] pointing to the old veneers. Code
compiled after the restart branches to those addresses, which are overwritten by new traces as the
buffer fills up again, and jumps into the middle of unrelated code when the stub is taken
(exception handling, leaving a function, ...).

Observed with opcache.jit=tracing, more than 1 MB of JIT code and repeated OOM restarts under
FrankenPHP (ZTS): crash in JIT code in 8 of 8 runs. At the crash, gdb shows e.g.
zend_jit_stub_handlers[JIT$$leave_function_handler] == $pc, and that address holds an ldr
of a trace, not the b of a veneer. In a single-process NTS server the same stale entries are
present at zend_jit_restart() (JIT$$exception_handler, JIT$$leave_function_handler,
JIT$$icall_throw, JIT$$leave_throw pointing past the restart point); we did not manage to turn
that into a deterministic crash in NTS, so there is no test yet.

The fix keeps the original stub addresses in a third block after the pending veneers (allocated in
the same place as the existing two) and restores them in zend_jit_restart(). x86 and Windows
are not affected (no veneers).

With this change (and the GH-8739 fix, which is needed to have restarts at all under ZTS): 0 crashes
in 40 runs of the FrankenPHP scenario, and 9 of 10 runs without a crash on Apache event (the remaining
crash has a different signature and is not understood yet). make test for ext/opcache and
Zend/tests passes.

🤖 Generated with Claude Code

On AArch64 a branch from JIT code to a stub may be out of range, and IR
emits a veneer next to the new code. zend_jit_commit_veneers() then
replaces the address of the stub in zend_jit_stub_handlers[] by the
address of the veneer, so that later code reuses it. These veneers live
after the JIT restart point: zend_jit_restart() rewinds the buffer below
them, but zend_jit_stub_handlers[] keeps pointing to them. Code compiled
after the restart branches to the old addresses, which are overwritten
by new traces, and crashes (e.g. on an exception or when leaving a
function through a stub). This needs more than 1MB of JIT code (the
range of conditional branches) before the restart.

Keep the original stub addresses after the pending veneers and restore
them in zend_jit_restart().
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.

1 participant