From 8c9c679446d2ebbb68de51185c8ebfc6b727c376 Mon Sep 17 00:00:00 2001 From: aasul Date: Sat, 31 Oct 2026 14:48:20 +0100 Subject: [PATCH] opcache: preserve loop-carried cv values across trace guards --- ext/opcache/jit/zend_jit_ir.c | 14 ++++ ext/opcache/tests/jit/gh24683_b.phpt | 80 +++++++++++++++++++++++ ext/opcache/tests/jit/gh24683_b_loop.phpt | 47 +++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 ext/opcache/tests/jit/gh24683_b.phpt create mode 100644 ext/opcache/tests/jit/gh24683_b_loop.phpt diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 53959a536a4a..aa8a1ec25966 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -17476,6 +17476,20 @@ static bool zend_jit_opline_supports_reg(const zend_op_array *op_array, zend_ssa static bool zend_jit_var_supports_reg(zend_ssa *ssa, int var) { + /* A transient assignment to a live loop-carried CV needs an explicit, + * control-dependent store. Its spill must not be hoisted before the + * loop guards, where the incoming value is still observable. */ + if (ssa->vars[var].definition >= 0 + && !ssa->vars[var].phi_use_chain + && ssa->ops[ssa->vars[var].definition].op1_def == var) { + int use = ssa->ops[ssa->vars[var].definition].op1_use; + + if (use >= 0 && !ssa->vars[use].no_val + && ssa->vars[use].definition_phi + && (ssa->cfg.blocks[ssa->vars[use].definition_phi->block].flags & ZEND_BB_LOOP_HEADER)) { + return 0; + } + } if (ssa->vars[var].no_val) { /* we don't need the value */ return 0; diff --git a/ext/opcache/tests/jit/gh24683_b.phpt b/ext/opcache/tests/jit/gh24683_b.phpt new file mode 100644 index 000000000000..02221c33b2ed --- /dev/null +++ b/ext/opcache/tests/jit/gh24683_b.phpt @@ -0,0 +1,80 @@ +--TEST-- +GH-24683 (B): tracing JIT must preserve overwritten runtime CV values +--EXTENSIONS-- +opcache +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=64M +opcache.jit_hot_loop=61 +opcache.jit_hot_func=127 +opcache.jit_hot_return=8 +opcache.jit_hot_side_exit=8 +opcache.file_update_protection=0 +--FILE-- +> 2; $ram[$k] = ($ram[$k] & ~(65535 << (($p & 3) << 3))) | (($value & 65535) << (($p & 3) << 3)); } + } + $pc = 2; + continue 2; + case 2: + $x = 0x800E0000 | $zero; + $a = ($x + 0xFFFFEDED) & 0xFFFFFFFF; + $p = $a & 0x1FFFFFFF; + $byte = ((($inRam ? ($ram[($p & 0x1FFFFF) >> 2] >> (($p & 3) << 3)) & 0xFF : ($inSp ? ($sp[($p - 0x1F800000) >> 2] >> (($p & 3) << 3)) & 0xFF : $bus->read8($a))) ^ 0x80) - 0x80) & 0xFFFFFFFF; + $x = $byte; + $pc = 3; + continue 2; + default: + goto leave; + } + } + leave: + $r[12] = $x; + return $pc; +}; + +$bus = new Bus(); +$ram = array_fill(0, 0x80000, 0); +$ram[0xDEDED >> 2] = 0x0003047F; // the byte at 0xDEDED is 4 +$sp = []; +$wrong = 0; +for ($k = 0; $k < 300; $k++) { + $r = array_fill(0, 32, 0); + $r[4] = 2; + $fn($bus, $r, $ram, $sp, 1); + if ($r[12] !== 4) { + if ($wrong++ === 0) { + printf("call %d: \$x is 0x%X, expected 4\n", $k, $r[12]); + } + } +} +echo "wrong: $wrong\n"; +--EXPECT-- +wrong: 0 diff --git a/ext/opcache/tests/jit/gh24683_b_loop.phpt b/ext/opcache/tests/jit/gh24683_b_loop.phpt new file mode 100644 index 000000000000..c8fee335a5c0 --- /dev/null +++ b/ext/opcache/tests/jit/gh24683_b_loop.phpt @@ -0,0 +1,47 @@ +--TEST-- +GH-24683 (B): loop guards must observe the incoming CV value +--EXTENSIONS-- +opcache +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=64M +opcache.jit_hot_loop=61 +opcache.jit_hot_func=127 +opcache.jit_hot_return=8 +opcache.jit_hot_side_exit=8 +opcache.file_update_protection=0 +--FILE-- +> (($p & 3) << 3)) & 0xFF) ^ 0x80) - 0x80; + $x = $byte; + $pc = 3; + continue 2; + default: goto leave; + } + } + leave: return $x; +} +$wrong = 0; +for ($i = 0; $i < 1000; $i++) { + $r = [0, 0x0003047F, 4]; + $pc = $i < 500 ? 2 : 3; + if (test($r, $pc) !== 4) { + $wrong++; + } +} +echo "wrong: $wrong\n"; +--EXPECT-- +wrong: 0