[php-src] PR #25383: Fix tracing JIT reading the int result of ++/-- on a reference as a double
| From: | ChrisAbner | 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.