[php-src] PR #25383: Fix tracing JIT reading the int result of ++/-- on a reference as a double

From: Date: Wed, 11 Nov 2026 02:54:16 +0000
Subject: [php-src] PR #25383: Fix tracing JIT reading the int result of ++/-- on a reference as a double
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-40526@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/25383 Author: ChrisAbner > With the tracing JIT, ++/-- on > a reference returns an int that the trace then reads as a double. It happens once the trace was > recorded with a float in the variable. function f($a) { $x = &$a; for ($i = 0; > $i < 4; $i++) { $r = ++$x; } return $r; }, called 20 times with > 1.5 and then with 5, returns > float(4.4E-323) instead of int(9). > > global and static variables, > $x = &$arr[0], foreach (... as > &$x), [&$x] = $arr, use > (&$v), a by-reference parameter and references to properties and static > properties do the same. The variable itself is right. Only the result of the operation is wrong. > With a string in the reference, the string pointer is read as a float. > > The trace does not record the type behind a reference, so > zend_jit_inc_dec() takes the generic path and can write a result of > any type. The result still has a speculative double guard, because the next > ASSIGN was recorded with a float. Right after > zend_jit_inc_dec(), zend_jit_trace() > drops that guard in the double case if !(res_info & > MAY_BE_STRING). That is always true there, because the same condition already > requires res_info to be exactly double plus guard. The long case > just above checks !(op1_info & MAY_BE_STRING). This PR does the > same in the double case. The guard now stays for a reference, and the ÈCÒÛ¬�L > ­êþ§!9ASSIGN leaves the trace when the value is not a double. > > I kept the long case's condition instead of adding a separate > MAY_BE_REF check. Every CV that is not a reference is narrowed to > its recorded type before this point. A reference is not narrowed, and > UPDATE_SSA_TYPE gives it MAY_BE_ANY, > so its op1_info always has > MAY_BE_STRING. If you prefer it explicit, I can switch the four > conditions in this block to has_concrete_type(op1_info), as the > binary ops and ASSIGN_OP do. > > The double case came with the IR JIT in caf102dfae (#12079), so 8.4.0 is the first release with > it. Reproduced on 8.4.27 (x86-64 and aarch64), 8.5.12 and the PHP-8.6 head. 8.1, 8.2 and 8.3 return > int(9). The line is the same in PHP-8.5, PHP-8.6 and master. > > **Verification** > > New test ext/opcache/tests/jit/inc_025.phpt. PHP-8.4 debug > build, opcache.jit=tracing, each function called 20 times with > 1.5 and then once with the value shown: > > | Case | Before | After | > |---|---|---| > | ++$x, $x = &$v, with > 5 | > [3.0e-323,3.5e-323,4.0e-323,4.4e-323] | > [6,7,8,9] | > | $x++, $x = &$v, with > 5 | > [2.5e-323,3.0e-323,3.5e-323,4.0e-323] | > [5,6,7,8] | > | --$x, $x = &$v, with > 5 | > [2.0e-323,1.5e-323,1.0e-323,5.0e-324] | > [4,3,2,1] | > | ++$g, global $g, with > 5 | > [3.0e-323,3.5e-323,4.0e-323,4.4e-323] | > [6,7,8,9] | > | ++$s, static $s, with > 5 | > [3.0e-323,3.5e-323,4.0e-323,4.4e-323] | > [6,7,8,9] | > | $x++, $x = &$o->t with > float\|string $t, with > "2.5" | first value is a garbage float > (9.27e-310 in one run) | > ["2.5",3.5,4.5,5.5] | > > Without the fix the test fails on all six lines. With the fix it passes, and so does the rest > of ext/opcache/tests/jit (537 tests, 0 failures, including the 24 > existing inc_0* tests). > > Zend/tests and > ext/opcache/tests with the fix, 5906 tests run, with  > Љ³j„ðP U†‰Ò(opcache.jit=tracing and with > opcache.jit=function: 3 failures in both modes, > gh17733, > preload_restart_function_jit and > preload_restart_hot_counters. They start a sub-process with their > own OPcache settings, and they fail the same way on the base commit in my environment.

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