[php-src] PR #24383: Fix AArch64 JIT stub handlers pointing at discarded veneers after opcache restart

From: Date: Sat, 17 Oct 2026 14:40:41 +0000
Subject: [php-src] PR #24383: Fix AArch64 JIT stub handlers pointing at discarded veneers after opcache restart
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-39519@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/24383 Author: austinderrick On AArch64, opcache_reset() with the tracing JIT leaves zend_jit_stub_handlers[] pointing at veneers in code that the restart discarded. Traces compiled afterwards overwrite those veneers, so a later branch to an affected stub jumps into unrelated machine code. PHP-FPM workers then crash with SIGSEGV or hang at 100% CPU until FPM is restarted. ### Cause 1. On AArch64, zend_jit_stub_handlers[] lives at the start of the JIT buffer. 2. When a trace is out of branch range of a stub, IR emits a veneer in that trace's code, and zend_jit_commit_veneers() replaces zend_jit_stub_handlers[i] with the veneer address. 3. zend_jit_restart() resets the buffer position (dasm_ptr[0] = dasm_ptr[1]) but keeps the table. In a Laravel application, a gdb dump of the table showed six entries (exception_handler, exception_handler_undef, interrupt_handler, leave_function_handler, icall_throw, leave_throw) moving from the stub area to about 1 MB into the buffer after warm-up. After opcache_reset(), they still pointed there while the buffer was back to 3.9 KB in use. The failure needs the code compiled after the reset to differ from the code before it. When the same code runs in the same order, IR rebuilds the same veneers at the same addresses. ### Fix Save the stub table at the end of zend_jit_startup() and restore it, with the pending-veneer slots cleared, in zend_jit_restart(). ### Reproduction Official php:8.4-fpm-trixie image (8.4.23, arm64) behind nginx, pm = static with 4 children: ```ini opcache.enable=1 opcache.jit=tracing opcache.jit_buffer_size=64M opcache.validate_timestamps=0 opcache.jit_max_root_traces=100000 opcache.jit_max_side_traces=100000 opcache.jit_max_exit_counters=65536 max_execution_time=0 ``` Two generated workloads: A is 1,500 classes with hot loops, and B is 3,000 classes with polymorphic branches, closures and caught exceptions (about 8 MB of JIT code). Page b.php runs B; page ab.php runs A and then B; reset.php calls opcache_reset(). | Sequence on a fresh FPM | Result | |---|---| | b.php x2, ab.php x2 | All 200, ab.php in 0.3 to 0.8 s | | b.php x2, reset.php, ab.php | ab.php never returns; workers at 100% CPU | | Same, opcache.jit=disable | All 200 | | Same, x86_64 | All 200 | | Same, arm64 with this patch | All 200 | In the Laravel application, the same sequence crashes every worker with SIGSEGV inside zend_jit_trace_exit or in JIT code calling zend_error_unchecked, also on 8.4.26. With this patch applied to 8.4.23, repeated resets return 200 with no crashes. I can share the generator scripts. This may be the same bug as GH-11609.

« previous php.git-pulls (#39519) next »