Skip to content

Fix tracing JIT reading the int result of ++/-- on a reference as a double - #25383

Closed
ChrisAbner wants to merge 1 commit into
php:PHP-8.4from
ChrisAbner:fix-jit-inc-dec-ref-result-guard
Closed

ChrisAbner wants to merge 1 commit into
php:PHP-8.4from
ChrisAbner:fix-jit-inc-dec-ref-result-guard

Conversation

@ChrisAbner

Copy link
Copy Markdown
Contributor

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 ASSIGN 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 caf102d (#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 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.

…ouble

After zend_jit_inc_dec(), the double case dropped the speculative guard
of the result when !(res_info & MAY_BE_STRING), which is always true
there. For a reference the trace does not know the type of the value,
so the result can be an int that the next ASSIGN then reads as a double.
Check op1_info, like the long case does.
@devnexen
devnexen requested a review from iliaal November 11, 2026 05:16

@LamentXU123 LamentXU123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this make sense. @ndossche ?

@iliaal iliaal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also checked foreach by-ref, a by-ref parameter, use (&$v) and a static property reference: all four return garbage floats before this and the right ints after. Keeping the long case's op1_info condition is fine by me.

@devnexen devnexen closed this in 1cb8a2f Nov 11, 2026
@devnexen

Copy link
Copy Markdown
Member

Thanks !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants