Commit de4edc82b83 for php
commit de4edc82b83021b3cd694cccfe312990e32a713a
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Tue Nov 3 09:25:53 2026 -0500
ext/opcache: Fix GH-25164 loop liveness across calls
A Phi in an ENTRY block now always moves through memory: its sources are
stored and the Phi is reloaded, because the VM may enter that block
without the caller's registers. The ENTRY walk in zend_jit_may_be_in_reg()
also follows the incoming edges of a loop-carried Phi use in the
definition block, except for a zero-length Pi interval. DO_UCALL,
DO_FCALL, and DO_FCALL_BY_NAME reproduce the bug; yield and include do
not.
Fixes GH-25164
Closes GH-25178
diff --git a/NEWS b/NEWS
index f42114aa991..578709a0a5d 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,8 @@ PHP NEWS
- Opcache:
. Fixed bug GH-25165 (Tracing JIT loops forever once opcache.jit_buffer_size
is exhausted while compiling side traces). (Ilia Alshanetsky)
+ . Fixed bug GH-25164 (Function JIT loop calling a user function runs once
+ or never terminates). (Ilia Alshanetsky)
19 Nov 2026, PHP 8.4.28
diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c
index 53c87ce16da..dbc7a37ca07 100644
--- a/ext/opcache/jit/zend_jit.c
+++ b/ext/opcache/jit/zend_jit.c
@@ -1073,7 +1073,8 @@ static void zend_jit_allocate_registers(zend_jit_ctx *ctx, const zend_op_array *
ra[src].flags |= ZREG_STORE;
}
} else {
- int need_move = 0;
+ /* The VM may enter an ENTRY block, so its Phi sources must go through memory */
+ int need_move = (ssa->cfg.blocks[phi->block].flags & (ZEND_BB_ENTRY|ZEND_BB_RECV_ENTRY)) != 0;
for (k = 0; k < ssa->cfg.blocks[phi->block].predecessors_count; k++) {
src = phi->sources[k];
diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 53959a536a4..1afdb79267c 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -17538,6 +17538,7 @@ static bool zend_jit_may_be_in_reg(const zend_op_array *op_array, zend_ssa *ssa,
zend_basic_block *bb;
zend_ssa_phi *p;
bool ret = 1;
+ bool def_is_pi = 0;
zend_worklist worklist;
ALLOCA_FLAG(use_heap)
@@ -17547,6 +17548,7 @@ static bool zend_jit_may_be_in_reg(const zend_op_array *op_array, zend_ssa *ssa,
} else {
ZEND_ASSERT(ssa->vars[var].definition_phi);
def_block = ssa->vars[var].definition_phi->block;
+ def_is_pi = ssa->vars[var].definition_phi->pi >= 0;
}
ZEND_WORKLIST_ALLOCA(&worklist, ssa->cfg.blocks_count, use_heap);
@@ -17564,9 +17566,14 @@ static bool zend_jit_may_be_in_reg(const zend_op_array *op_array, zend_ssa *ssa,
p = ssa->vars[var].phi_use_chain;
while (p) {
- use_block = p->block;
- if (use_block != def_block) {
- bb = &ssa->cfg.blocks[use_block];
+ bb = &ssa->cfg.blocks[p->block];
+ if (p->pi >= 0) {
+ /* A Pi uses the value on the single edge from its "pi" block */
+ if (p->pi != def_block) {
+ zend_worklist_push(&worklist, p->pi);
+ }
+ } else if (p->block != def_block || !def_is_pi) {
+ /* Live to the end of the predecessor; a same-block Pi is zero-length */
for (j = 0; j < bb->predecessors_count; j++) {
if (p->sources[j] == var) {
use_block = ssa->cfg.predecessors[bb->predecessor_offset + j];
diff --git a/ext/opcache/tests/jit/gh25164.phpt b/ext/opcache/tests/jit/gh25164.phpt
new file mode 100644
index 00000000000..1809b89775b
--- /dev/null
+++ b/ext/opcache/tests/jit/gh25164.phpt
@@ -0,0 +1,45 @@
+--TEST--
+GH-25164 (JIT: loop variable kept in a register across a user function call)
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit_buffer_size=64M
+opcache.jit=function
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+
+function leaf() {
+ static $calls = 0;
+ if (++$calls > 20) {
+ die("runaway loop\n");
+ }
+ echo "a";
+}
+
+function driver() {
+ $i = 0;
+ while ($i++ < 5) {
+ leaf();
+ }
+ return $i;
+}
+
+function driver2() {
+ $i = 0;
+ while ((($i = ($i + 3) & 15) | 0) != 9) {
+ leaf();
+ echo "-";
+ }
+ return $i;
+}
+
+var_dump(driver());
+var_dump(driver2());
+
+?>
+--EXPECT--
+aaaaaint(6)
+a-a-int(9)