Commit e1a3a4c9a47 for php.net

commit e1a3a4c9a47bea89646d888692fe75b4aaac5831
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Mon Mar 16 14:31:49 2026 -0400

    Fix GH-21267: JIT infinite loop on FETCH_OBJ_R with IS_UNDEF property (#21368)

    When the JIT defers the IS_UNDEF check for FETCH_OBJ_R to the result
    type guard, the deoptimization escape path dispatches to opline->handler
    via the trace_escape stub. If opline->handler has been overwritten with
    JIT code (e.g. a function entry trace), this creates an infinite loop.

    Fix by dispatching to the original VM handler (orig_handler from the
    trace extension) instead of going through the trace_escape stub. This
    avoids the extra IS_UNDEF guard on every property read while correctly
    handling the rare IS_UNDEF case during deoptimization.

    Also set current_op_array in zend_jit_trace_exit_to_vm so that the
    blacklisted exit deoptimizer can resolve orig_handler, covering the
    case where side trace compilation is exhausted.

    Closes GH-21368.

diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 3ddaa027088..b87abad4361 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -8066,7 +8066,7 @@ static int zend_jit_defined(zend_jit_ctx *jit, const zend_op *opline, uint8_t sm
 	return 1;
 }

-static int zend_jit_escape_if_undef(zend_jit_ctx *jit, int var, uint32_t flags, const zend_op *opline, int8_t reg)
+static int zend_jit_escape_if_undef(zend_jit_ctx *jit, int var, uint32_t flags, const zend_op *opline, const zend_op_array *op_array, int8_t reg)
 {
 	zend_jit_addr reg_addr = ZEND_ADDR_REF_ZVAL(zend_jit_deopt_rload(jit, IR_ADDR, reg));
 	ir_ref if_def = ir_IF(jit_Z_TYPE(jit, reg_addr));
@@ -8089,7 +8089,20 @@ static int zend_jit_escape_if_undef(zend_jit_ctx *jit, int var, uint32_t flags,
 	}

 	jit_LOAD_IP_ADDR(jit, opline - 1);
-	ir_IJMP(jit_STUB_ADDR(jit, jit_stub_trace_escape));
+
+	/* We can't use trace_escape() because opcode handler may be overridden by JIT */
+	zend_jit_op_array_trace_extension *jit_extension =
+		(zend_jit_op_array_trace_extension*)ZEND_FUNC_INFO(op_array);
+	size_t offset = jit_extension->offset;
+	ir_ref ref = ir_CONST_ADDR(ZEND_OP_TRACE_INFO((opline - 1), offset)->orig_handler);
+	if (GCC_GLOBAL_REGS) {
+		ir_TAILCALL(IR_VOID, ref);
+	} else {
+#if defined(IR_TARGET_X86)
+		ref = ir_CAST_FC_FUNC(ref);
+#endif
+		ir_TAILCALL_1(IR_I32, ref, jit_FP(jit));
+	}

 	ir_IF_TRUE(if_def);

diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index 03b59eea615..b5d980ca5af 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -3603,7 +3603,7 @@ static int zend_jit_trace_deoptimization(

 		ZEND_ASSERT(STACK_FLAGS(parent_stack, check2) == ZREG_ZVAL_COPY);
 		ZEND_ASSERT(reg != ZREG_NONE);
-		if (!zend_jit_escape_if_undef(jit, check2, flags, opline, reg)) {
+		if (!zend_jit_escape_if_undef(jit, check2, flags, opline, exit_info->op_array, reg)) {
 			return 0;
 		}
 		if (!zend_jit_restore_zval(jit, EX_NUM_TO_VAR(check2), reg)) {
diff --git a/ext/opcache/tests/jit/gh21267.phpt b/ext/opcache/tests/jit/gh21267.phpt
new file mode 100644
index 00000000000..91c8de29c2b
--- /dev/null
+++ b/ext/opcache/tests/jit/gh21267.phpt
@@ -0,0 +1,35 @@
+--TEST--
+GH-21267 (JIT infinite loop on FETCH_OBJ_R with IS_UNDEF property in polymorphic context)
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=tracing
+opcache.jit_buffer_size=64M
+opcache.jit_hot_loop=0
+opcache.jit_hot_func=2
+opcache.jit_hot_return=0
+opcache.jit_hot_side_exit=1
+--FILE--
+<?php
+class C {
+    public $x = true;
+    public function __get($name) { return null; }
+    public function getX() { return $this->x; }
+}
+
+$o1 = new C;
+$o2 = new C;
+$o2->x = false;
+$o3 = new C;
+unset($o3->x);
+$a = [$o1, $o2, $o3];
+
+for ($i = 0; $i < 8; $i++) {
+    $m = $a[$i % 3];
+    $m->getX();
+    $m->getX();
+}
+?>
+OK
+--EXPECT--
+OK
diff --git a/ext/opcache/tests/jit/gh21267_blacklist.phpt b/ext/opcache/tests/jit/gh21267_blacklist.phpt
new file mode 100644
index 00000000000..3ec222dc425
--- /dev/null
+++ b/ext/opcache/tests/jit/gh21267_blacklist.phpt
@@ -0,0 +1,36 @@
+--TEST--
+GH-21267 (JIT infinite loop on FETCH_OBJ_R with IS_UNDEF via blacklisted trace exit)
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=tracing
+opcache.jit_buffer_size=64M
+opcache.jit_hot_loop=0
+opcache.jit_hot_func=2
+opcache.jit_hot_return=0
+opcache.jit_hot_side_exit=1
+opcache.jit_max_side_traces=0
+--FILE--
+<?php
+class C {
+    public $x = true;
+    public function __get($name) { return null; }
+    public function getX() { return $this->x; }
+}
+
+$o1 = new C;
+$o2 = new C;
+$o2->x = false;
+$o3 = new C;
+unset($o3->x);
+$a = [$o1, $o2, $o3];
+
+for ($i = 0; $i < 8; $i++) {
+    $m = $a[$i % 3];
+    $m->getX();
+    $m->getX();
+}
+?>
+OK
+--EXPECT--
+OK