Commit 9c2acc343e5 for php.net

commit 9c2acc343e55dc59bd92cd5acfdadee4787772d7
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Wed Sep 16 08:35:00 2026 -0400

    Fix GH-23693: JIT guard branches on stale flags across basic blocks

    The x86 matcher folds v = BINOP(a, b); c = CMP(v, 0); GUARD(c) into
    IR_GUARD_JCC_INT, emitting the BINOP, dropping the CMP and branching on
    the flags the BINOP left, but it only required the BINOP to precede the
    CMP in the IR. Once GCM hoists a loop-invariant BINOP into a dominating
    block, the jcc reads flags the intervening code has clobbered and the
    guard fires on whatever is in EFLAGS. Require the BINOP to sit in the
    guard's block and allow only snapshots between the comparison and the
    guard, the way ir_match_fuse_load() pairs ir_in_same_block() with
    ir_match_has_mem_deps(). The sibling MEM_BINOP fold already checks the
    block, the IF side folds are pinned by full ref adjacency, and
    ir_aarch64.dasc has no guard fold.

    Mirrors the upstream fix dstogov/ir@51107a3.

    Fixes GH-23693
    Closes GH-23711

diff --git a/NEWS b/NEWS
index 2eef981bcf6..982945e5ebf 100644
--- a/NEWS
+++ b/NEWS
@@ -37,6 +37,8 @@ PHP                                                                        NEWS
 - Opcache:
   . Fixed OSS-Fuzz #546798343 (Heap-buffer-overflow in optimizer with
     FCCs and inlining). (ndossche)
+  . Fixed bug GH-23693 (Tracing JIT produces wrong results for a guard on a
+    loop-invariant addition). (Ilia Alshanetsky)

 - PDO:
   . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid
diff --git a/ext/opcache/jit/ir/ir_x86.dasc b/ext/opcache/jit/ir/ir_x86.dasc
index ca42001a881..f5efb66698d 100644
--- a/ext/opcache/jit/ir/ir_x86.dasc
+++ b/ext/opcache/jit/ir/ir_x86.dasc
@@ -1923,6 +1923,21 @@ static bool ir_match_has_mem_deps(ir_ctx *ctx, ir_ref ref, ir_ref root)
 	return 0;
 }

+/* A naive check if anything that emits code, and so clobbers the flags, is
+ * scheduled between the flags setting instruction and the fusion root */
+static bool ir_match_has_flags_deps(ir_ctx *ctx, ir_ref ref, ir_ref root)
+{
+	ir_ref pos = ctx->prev_ref[root];
+
+	while (pos > ref) {
+		if (ctx->ir_base[pos].op != IR_SNAPSHOT) {
+			return 1;
+		}
+		pos = ctx->prev_ref[pos];
+	}
+	return pos != ref;
+}
+
 static void ir_match_fuse_load(ir_ctx *ctx, ir_ref ref, ir_ref root)
 {
 	if (ir_in_same_block(ctx, ref) &&
@@ -3089,7 +3104,9 @@ store_int:
 						if (IR_IS_CONST_REF(op2_insn->op2)
 						 && !IR_IS_SYM_CONST(ctx->ir_base[op2_insn->op2].op)
 						 && ctx->ir_base[op2_insn->op2].val.i64 == 0) {
-							if (op2_insn->op1 == insn->op2 - 1) { /* previous instruction */
+							if (op2_insn->op1 == insn->op2 - 1 /* previous instruction */
+							 && ir_in_same_block(ctx, op2_insn->op1)
+							 && !ir_match_has_flags_deps(ctx, insn->op2, ref)) {
 								ir_insn *op1_insn = &ctx->ir_base[op2_insn->op1];

 								if ((op1_insn->op == IR_OR || op1_insn->op == IR_AND || op1_insn->op == IR_XOR) ||
diff --git a/ext/opcache/tests/jit/gh23693.phpt b/ext/opcache/tests/jit/gh23693.phpt
new file mode 100644
index 00000000000..1540f0c1368
--- /dev/null
+++ b/ext/opcache/tests/jit/gh23693.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23693: Tracing JIT reads stale flags for a guard on a hoisted addition
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit_buffer_size=64M
+opcache.jit=tracing
+opcache.jit_hot_func=1
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+function f(int $pos, int $n): int {
+    if ($pos < 0) {
+        return -1;
+    }
+    $y = $pos >> 2;
+    $s = 0;
+    for ($dy = -1; $dy <= 1; ++$dy) {
+        for ($i = 0; $i < $n; ++$i) {
+            $ny = $y + $dy;
+            if ($ny < 0) {
+                continue;
+            }
+            if ($ny >= 4) {
+                continue;
+            }
+            $s += ($ny << 2) | ($i & 3);
+        }
+    }
+    return $s;
+}
+for ($k = 0; $k < 30; ++$k) {
+    f(16, 200);
+}
+var_dump(f(16, 200));
+var_dump(f(0, 200));
+?>
+--EXPECT--
+int(2700)
+int(1400)