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"