Commit a4c01896fd8 for php

commit a4c01896fd8d926e1bc39c5574b7d0b4eab1c07d
Author: ndossche <7771979+ndossche@users.noreply.github.com>
Date:   Thu Nov 5 22:58:45 2026 +0100

    Fix GH-25037: Function JIT ENTRY live var assertion

    IR: Backport fix for loop-invariant hoisting across external entries.
    Mark loops reached from an ENTRY that bypasses their immediate dominator
    with IR_BB_LOOP_WITH_ENTRY_IN_PREDS, so GCM cannot hoist unbound
    invariants across those entries.

    Cherry-picks https://github.com/dstogov/ir/commit/a38079ef81c2b37c1acba090d600c38855139323
    adapted to the older IR in this branch: the check is placed in the
    sorted_blocks loop of ir_find_loops().

    Fixes GH-25037
    Closes GH-25193.

diff --git a/NEWS b/NEWS
index 026237984b4..5f038904389 100644
--- a/NEWS
+++ b/NEWS
@@ -30,6 +30,8 @@ PHP                                                                        NEWS
   . Fixed memory leak in JIT ASSIGN_DIM when the assignment fails. (Jorg Sowa)
   . Fixed bug GH-16396 (Failed to invoke user error handler during index
     conversion in JIT). (Jorg Sowa)
+  . Fixed bug GH-25037 (Function JIT ENTRY live var assertion).
+    (Ilia Alshanetsky)

 - SOAP:
   . Fixed bug GH-25267 (NULL dereference in to_xml_any() with integer key).
diff --git a/ext/opcache/jit/ir/ir_cfg.c b/ext/opcache/jit/ir/ir_cfg.c
index 92042ea8cbb..0d6eeb8b84d 100644
--- a/ext/opcache/jit/ir/ir_cfg.c
+++ b/ext/opcache/jit/ir/ir_cfg.c
@@ -1085,6 +1085,33 @@ static IR_NEVER_INLINE void ir_collect_irreducible_loops(ir_ctx *ctx, uint32_t *
 	}
 }

+static IR_NEVER_INLINE void ir_check_loop_entry_in_preds(ir_ctx *ctx, uint32_t hdr, ir_worklist *work)
+{
+	ir_block *blocks = ctx->cfg_blocks;
+	uint32_t *edges = ctx->cfg_edges;
+
+	ir_worklist_clear(work);
+	ir_bitset_incl(work->visited, blocks[hdr].idom);
+	ir_worklist_push(work, hdr);
+	while (ir_worklist_len(work)) {
+		ir_block *bb = &blocks[ir_worklist_pop(work)];
+
+		if (bb->flags & IR_BB_ENTRY) {
+			blocks[hdr].flags |= IR_BB_LOOP_WITH_ENTRY_IN_PREDS;
+			break;
+		}
+
+		uint32_t *p = &edges[bb->predecessors];
+		uint32_t count = bb->predecessors_count;
+
+		for (; count > 0; p++, count--) {
+			if (!ir_dominates(blocks, hdr, *p)) {
+				ir_worklist_push(work, *p);
+			}
+		}
+	}
+}
+
 int ir_find_loops(ir_ctx *ctx)
 {
 	uint32_t b, j, n;
@@ -1284,6 +1311,12 @@ int ir_find_loops(ir_ctx *ctx)
 					}
 				}
 			}
+			bb = &blocks[b];
+			if (ctx->entries_count
+			 && (bb->flags & (IR_BB_LOOP_HEADER|IR_BB_LOOP_WITH_ENTRY)) == IR_BB_LOOP_HEADER
+			 && (bb->predecessors_count > 2 || (bb->flags & IR_BB_IRREDUCIBLE_LOOP))) {
+				ir_check_loop_entry_in_preds(ctx, b, &work);
+			}
 		}
 	}

diff --git a/ext/opcache/jit/ir/ir_gcm.c b/ext/opcache/jit/ir/ir_gcm.c
index b194eeb8177..5bec546129a 100644
--- a/ext/opcache/jit/ir/ir_gcm.c
+++ b/ext/opcache/jit/ir/ir_gcm.c
@@ -100,7 +100,7 @@ static uint32_t ir_gcm_select_best_block(ir_ctx *ctx, ir_ref ref, uint32_t lca)
 #endif

 	flags = (bb->flags & IR_BB_LOOP_HEADER) ? bb->flags : ctx->cfg_blocks[bb->loop_header].flags;
-	if ((flags & IR_BB_LOOP_WITH_ENTRY)
+	if ((flags & (IR_BB_LOOP_WITH_ENTRY|IR_BB_LOOP_WITH_ENTRY_IN_PREDS))
 	 && !(ctx->binding && ir_binding_find(ctx, ref))) {
 		/* Don't move loop invariant code across an OSR ENTRY if we can't restore it */
 		return lca;
@@ -135,7 +135,7 @@ static uint32_t ir_gcm_select_best_block(ir_ctx *ctx, ir_ref ref, uint32_t lca)
 				break;
 			}
 			flags = (bb->flags & IR_BB_LOOP_HEADER) ? bb->flags : ctx->cfg_blocks[bb->loop_header].flags;
-			if ((flags & IR_BB_LOOP_WITH_ENTRY)
+			if ((flags & (IR_BB_LOOP_WITH_ENTRY|IR_BB_LOOP_WITH_ENTRY_IN_PREDS))
 			 && !(ctx->binding && ir_binding_find(ctx, ref))) {
 				break;
 			}
diff --git a/ext/opcache/jit/ir/ir_private.h b/ext/opcache/jit/ir/ir_private.h
index 6d8f31a8b7e..25edb4bc41a 100644
--- a/ext/opcache/jit/ir/ir_private.h
+++ b/ext/opcache/jit/ir/ir_private.h
@@ -1125,6 +1125,7 @@ void ir_iter_cleanup(ir_ctx *ctx);
 #define IR_BB_PREV_EMPTY_ENTRY (1<<7)
 #define IR_BB_OSR_ENTRY_LOADS  (1<<8) /* OSR Entry-point with register LOADs   */
 #define IR_BB_LOOP_WITH_ENTRY  (1<<9) /* set together with LOOP_HEADER if there is an ENTRY in the loop */
+#define IR_BB_LOOP_WITH_ENTRY_IN_PREDS (1<<18) /* set together with LOOP_HEADER if an ENTRY reaches the loop bypassing its immediate dominator */

 /* The following flags are set by GCM */
 #define IR_BB_HAS_PHI          (1<<10)
diff --git a/ext/opcache/tests/jit/gh25037.phpt b/ext/opcache/tests/jit/gh25037.phpt
new file mode 100644
index 00000000000..cfb3b47cf5c
--- /dev/null
+++ b/ext/opcache/tests/jit/gh25037.phpt
@@ -0,0 +1,31 @@
+--TEST--
+GH-25037 (Function JIT: "ENTRY - live var" for a namespaced function call with a recursive self:: call argument)
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit_buffer_size=32M
+opcache.jit=1205
+--EXTENSIONS--
+opcache
+mbstring
+--FILE--
+<?php
+namespace Reproduce;
+
+class Entry
+{
+    public static function chr(int $ord, string $encoding = 'UTF-8'): string
+    {
+        if ($encoding === 'UCS-4BE') {
+            return pack('N', $ord);
+        }
+
+        return mb_convert_encoding(self::chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE');
+    }
+}
+
+var_dump(bin2hex(Entry::chr(0x1F35E)));
+?>
+--EXPECT--
+string(8) "f09f8d9e"