Repository navigation
Description
Description
php.ini
zend_extension=opcache.so
opcache.enable_cli=on
opcache.jit=tracing
opcache.jit_buffer_size=32M
Reproducer code:
<?php
function test()
{
$iterations = 1000000;
$counter = 0;
yield true;
while (true) {
$counter++;
if ($counter == $iterations) break;
if (mt_rand(0, 10000000) > 9999999) { echo("$counter\n"); return; } # take note iteration count is lower!
yield true;
}
echo($counter."\n");
}
foreach (test() as $v) { }
Bug description:
Results in loop mostly (but not always!) terminating by injected mt_rand() based termination condition at fixed counter value of "65".
It seems counter variable somehow stops increasing at generator invocations. In Windows build it does stop at "65" as well.
If mt_rand() termination is commented out, script expectedly never completes at wrong ("65") runs, so most of the time, while it should always complete at 1000000 iterations. I do not know why it completes sometimes, maybe due to address space randomization, maybe due to something else.
Disabling JIT resolves the issue, the script always works correctly.
Confirmed in both 8.4.10 for Linux x86-64 NTS built from source and 8.4.11 NTS Windows ZIP build downloaded from PHP site.
Correct output for multiple consecutive runs (random values and "1000000" completions as expected):
963792
956411
1000000
209645
1000000
75357
1000000
842156
512774
1000000
...
Problem occurence output (take note how much "65" appears there):
822760
65
65
1000000
65
65
65
65
1000000
65
...
Code workaround found (should help to debug it):
<?php
function test()
{
$iterations = 1000000;
$counter = 0;
yield true;
while (true) {
if ($counter == $iterations) break;
$counter++;
if (mt_rand(0, 10000000) > 9999999) { echo("$counter\n"); return; } # take note iteration count is lower!
yield true;
}
echo($counter."\n");
}
foreach (test() as $v) { }
(basically, swapping if() clause and counter increment)
makes the script always work properly.
Please tell if I can add anything else to that report.
PHP Version
PHP 8.4.10 (cli) (built: Jul 11 2025 10:12:19) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.4.10, Copyright (c) Zend Technologies
with Zend OPcache v8.4.10, Copyright (c), by Zend Technologies
PHP 8.4.11 (cli) (built: Jul 29 2025 18:02:29) (NTS Visual C++ 2022 x64)
Copyright (c) The PHP Group
Zend Engine v4.4.11, Copyright (c) Zend Technologies
with Zend OPcache v8.4.11, Copyright (c), by Zend Technologies
Operating System
Oracle Linux 10 x86-64, Windows 11 x86-64
Activity
AlexAT commentedon Aug 15, 2025
Apparently the count of "65" iterations before issue occurence is not random.
Changing
opcache.jit_hot_loopsetting from64to128made this number "129".Setting it to
0(avoiding hot loop compilation) resolves the issue.So somehow related to hot loops compilation.
ndossche commentedon Aug 16, 2025
I made a slight modification to trigger it more easily:
On 8.3 we get the following JIT trace checks:
On 8.4 we get this:
In the 8.4 assembly the value of %rbx is never stored back and so the increment is lost.
EDIT: forcing the variable to be stored by modifying
zend_jit_store_var_if_necessary_exworks around the issueEDIT 2: Dumping register allocation shows that the "store" flag is not set for
$countereven though I think it should. Although on 8.3 it also doesn't have ZREG_STORE set...AlexAT commentedon Aug 16, 2025
Could it be it considers RBX to stay allocated inside the whole loop (register allocated for variable inside the function/loop context) and does not take it into account this is a generator loop and so allocated registers will all be lost on yield?
Not sure how it's done internally but it feels sane to terminate register allocation context by yield.
Basically issuing yield should store all register-allocated variables back to their respective locations and reset all register allocations.
arnaud-lb commentedon Aug 16, 2025
Something like this should fix the issue:
Currently YIELD ends the trace with ZEND_JIT_TRACE_STOP_RETURN and is handled mostly like RETURN. Changing to ZEND_JIT_TRACE_STOP_INTERPRETER causes JIT to consider that variables are live after the trace returns E.g. in
php-src/ext/opcache/jit/zend_jit_trace.c
Lines 3299 to 3303 in 18276a8
AlexAT commentedon Aug 16, 2025
Oh yeah, reverse cause, it considers local variables are not needed anymore then like with return. Seems like so.
Fit JIT variable not stored before YIELD
arnaud-lb commentedon Aug 19, 2025
Thank you @AlexAT!