From f4bc650d6d3378c493bd61acc7bbe6e0646228eb Mon Sep 17 00:00:00 2001 From: christopher miller Date: Thu, 2 Jul 2026 17:07:07 +0100 Subject: [PATCH 1/2] JIT: Inline ZEND_BW_NOT Under the tracing JIT, ~ (ZEND_BW_NOT) was not inlined and fell back to the ZEND_BW_NOT_SPEC helper, whose result could be allocated to a stack slot aliasing a spilled loop-carried CV, clobbering it and producing a wrong result. Inline ZEND_BW_NOT for the LONG case (x ^ -1) like the other bitwise ops, removing the helper call and its temporary slot. --- ext/opcache/jit/zend_jit.c | 17 ++++++++++++ ext/opcache/jit/zend_jit_ir.c | 21 +++++++++++++++ ext/opcache/jit/zend_jit_trace.c | 17 ++++++++++++ ext/opcache/tests/jit/gh22558.phpt | 43 ++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 ext/opcache/tests/jit/gh22558.phpt diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index da73c98c4355..106d3c59c71d 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -1594,6 +1594,23 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op goto jit_failure; } goto done; + case ZEND_BW_NOT: + if (PROFITABILITY_CHECKS && (!ssa->ops || !ssa->var_info)) { + break; + } + op1_info = OP1_INFO(); + /* Only the definitely-LONG fast path; anything else falls + * through to the VM handler (its previous behaviour). */ + if ((op1_info & (MAY_BE_ANY|MAY_BE_UNDEF|MAY_BE_REF)) != MAY_BE_LONG) { + break; + } + res_addr = RES_REG_ADDR(); + if (!zend_jit_bw_not(&ctx, opline, + op1_info, OP1_REG_ADDR(), + RES_INFO(), res_addr)) { + goto jit_failure; + } + goto done; case ZEND_ADD: case ZEND_SUB: case ZEND_MUL: diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 2cc6a01c4107..717534ef1b48 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -6141,6 +6141,27 @@ static int zend_jit_long_math(zend_jit_ctx *jit, const zend_op *opline, uint32_t return 1; } +/* Inlined ZEND_BW_NOT for the definitely-LONG case. ~x == x ^ -1; using the + * inlined XOR path (rather than the ZEND_BW_NOT_SPEC helper) avoids a helper + * call whose result slot could alias a spilled loop-carried CV. */ +static int zend_jit_bw_not(zend_jit_ctx *jit, const zend_op *opline, uint32_t op1_info, zend_jit_addr op1_addr, uint32_t res_info, zend_jit_addr res_addr) +{ + ir_ref op1_lval_ref, ref; + + ZEND_ASSERT((op1_info & (MAY_BE_ANY|MAY_BE_UNDEF|MAY_BE_REF)) == MAY_BE_LONG); + + op1_lval_ref = jit_Z_LVAL(jit, op1_addr); + ref = ir_XOR_L(op1_lval_ref, ir_CONST_LONG(-1)); + jit_set_Z_LVAL(jit, res_addr, ref); + if (Z_MODE(res_addr) != IS_REG) { + jit_set_Z_TYPE_INFO(jit, res_addr, IS_LONG); + } + if (!zend_jit_store_var_if_necessary(jit, opline->result.var, res_addr, res_info)) { + return 0; + } + return 1; +} + static int zend_jit_concat_helper(zend_jit_ctx *jit, const zend_op *opline, uint8_t op1_type, diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index da97d102f202..2daf72d45838 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -2008,6 +2008,7 @@ static zend_ssa *zend_jit_trace_build_tssa(zend_jit_trace_rec *trace_buffer, uin case ZEND_JMPNZ_EX: case ZEND_BOOL: case ZEND_BOOL_NOT: + case ZEND_BW_NOT: ADD_OP1_TRACE_GUARD(); break; case ZEND_ISSET_ISEMPTY_CV: @@ -4465,6 +4466,22 @@ static const void *zend_jit_trace(zend_jit_trace_rec *trace_buffer, uint32_t par ssa->var_info[ssa_op->result_def].type &= ~MAY_BE_GUARD; } goto done; + case ZEND_BW_NOT: + op1_info = OP1_INFO(); + CHECK_OP1_TRACE_TYPE(); + /* Only the definitely-LONG fast path; otherwise fall back + * to the VM handler (its previous behaviour). */ + if ((op1_info & (MAY_BE_ANY|MAY_BE_UNDEF|MAY_BE_REF)) != MAY_BE_LONG) { + break; + } + res_info = RES_INFO(); + res_addr = RES_REG_ADDR(); + if (!zend_jit_bw_not(&ctx, opline, + op1_info, OP1_REG_ADDR(), + res_info, res_addr)) { + goto jit_failure; + } + goto done; case ZEND_BW_OR: case ZEND_BW_AND: case ZEND_BW_XOR: diff --git a/ext/opcache/tests/jit/gh22558.phpt b/ext/opcache/tests/jit/gh22558.phpt new file mode 100644 index 000000000000..14621a76e73d --- /dev/null +++ b/ext/opcache/tests/jit/gh22558.phpt @@ -0,0 +1,43 @@ +--TEST-- +GH-22558 (Tracing JIT: bitwise-NOT high bits leak past a mask into a typed int property) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit_buffer_size=64M +opcache.jit=tracing +; run-tests forces jit_hot_side_exit=1, which compiles side traces so eagerly +; that the buggy trace never forms — override back to the default so the bug shows. +opcache.jit_hot_side_exit=8 +--FILE-- +t[$i] = $i & 0xA8; } + public function add8(int $value, int $carry): void { + $a = $this->a; + $total = $a + $value + $carry; + $result = $total & 0xFF; + $this->f = $this->t[$result] + | (($total & 0x100) ? 0x01 : 0) + | (((($a & 0x0F) + ($value & 0x0F) + $carry) & 0x10) ? 0x10 : 0) + | ((~($a ^ $value) & ($a ^ $result) & 0x80) ? 0x04 : 0); + $this->a = $result; + } +} +$c = new C(); +for ($i = 0; $i < 100000; $i++) { + $c->a = $i & 0xFF; + $c->add8(($i >> 8) & 0xFF, 0); + if (($c->f & ~0xFF) !== 0) { + printf("MISCOMPILED at i=%d: \$f = %d (0x%X)\n", $i, $c->f, $c->f); + break; + } +} +echo "done\n"; +?> +--EXPECT-- +done From 64edd32bedc50f9bb6747bbc23ce5f133b43fab3 Mon Sep 17 00:00:00 2001 From: christopher miller Date: Fri, 3 Jul 2026 20:39:42 +0100 Subject: [PATCH 2/2] test name correction --- ext/opcache/tests/jit/{gh22558.phpt => gh22559.phpt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename ext/opcache/tests/jit/{gh22558.phpt => gh22559.phpt} (95%) diff --git a/ext/opcache/tests/jit/gh22558.phpt b/ext/opcache/tests/jit/gh22559.phpt similarity index 95% rename from ext/opcache/tests/jit/gh22558.phpt rename to ext/opcache/tests/jit/gh22559.phpt index 14621a76e73d..dc4654af0559 100644 --- a/ext/opcache/tests/jit/gh22558.phpt +++ b/ext/opcache/tests/jit/gh22559.phpt @@ -1,5 +1,5 @@ --TEST-- -GH-22558 (Tracing JIT: bitwise-NOT high bits leak past a mask into a typed int property) +GH-22559 (Tracing JIT: bitwise-NOT high bits leak past a mask into a typed int property) --EXTENSIONS-- opcache --INI--