[php-src] PR #24333: Fix tracing JIT specializing calls to not persistent functions
| From: | pavimus | 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)