[php-src] PR #24333: Fix tracing JIT specializing calls to not persistent functions

From: Date: Wed, 14 Oct 2026 18:23:23 +0000
Subject: [php-src] PR #24333: Fix tracing JIT specializing calls to not persistent functions
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-39439@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/24333 Author: pavimus 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 under ZTS (#24276) we saw rare false ‚ > w€ÂÒ3Q“P¥©!TypeErrors > (Unsupported operand types: int|float + int in code that only > adds integers) and crashes in > i_free_compiled_variables() with a garbage > EX(func), only with > opcache.jit=tracing. They turned > out to be unrelated to threads and to restarts: a single-threaded php > -S (ZTS and NTS builds) with > a full OPcache SHM crashes the same way, without any restart. > > The tracer records the called function of INIT_METHOD_CALL and > friends, and the trace compiler > then guards on it and uses it to compute the call frame size > (zend_vm_calc_used_stack()) and to > decide how arguments are passed. For a user function that is not persistent (its > script is not cached, e.g. because SHM is full, the file is blacklisted, or the code comes from > eval()), zend_jit_func_guard() > compares op_array.opcodes, which is request memory. In a later > request another function may get the same opcodes address; the > guard passes and the trace > pushes a call frame of the wrong size. When it is too small, the frames of the calls made by > the > callee are pushed over its own CVs and temporaries: it then reads garbage types, and leaving a > nested call crashes on a garbage EX(func). > zend_jit_init_fcall_guard() already refuses such functions > ("op_array and op_array->opcodes are not persistent. We can't link."). > > The fix makes the tracer not record such functions, the same way as megamorphic calls: the call > is compiled without a known function, and recording stops at the > DO_FCALL. > > The test serves index.php with php > -S; work() is cached and calls > $o->run() in a hot loop, > and class X comes from a blacklisted file that alternates > between two versions with the same > number of opcodes but 17 and 26 CVs. After a warm-up request both versions get the same > opcodes > address in every request (checked with gdb), so the trace recorded with the smaller version is > entered with the bigger one. Without the fix the result for the second version is wrong on > every > such request (NTS debug and ZTS release builds, AArch64 Linux); with the fix it is correct. > > On the original workload (a generated application of 2520 classes, 32M SHM that fills up, > php -S, > one connection) the crash depends on the sequence of requests; replaying a recorded sequence of > 206 requests crashes in i_free_compiled_variables() on the same > request every time (3 of 3). > In the core, work() (cached) called > run() of a class compiled in that request; that frame > needs 38 slots, but the next frame starts 28 slots above it. With the fix the same sequence > passes (0 of 3), and random runs (318 000 requests) neither crash nor return wrong results. > make test for ext/opcache and > Zend/tests passes, also with > opcache.jit=tracing forced. > > We only have AArch64 hardware; nothing in the mechanism is specific to it. It may also explain > #22102. > > The change conflicts when merged up into PHP-8.5 (the recorder code moved); I have an adapted > version for master (same fix and test, passes ext/opcache and > Zend/tests with tracing JIT) and > can open it or push it here, whichever is preferred. 🤖 Generated with [Claude Code](https://claude.com/claude-code)

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