From 4cebe12b83ba3d16aec4b5b87af3aa704ed16b58 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 16 Mar 2026 09:22:08 -0400 Subject: [PATCH] Fix GH-21267: JIT infinite loop on FETCH_OBJ_R with IS_UNDEF property 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. --- ext/opcache/jit/zend_jit_ir.c | 17 +++++++-- ext/opcache/jit/zend_jit_trace.c | 2 +- ext/opcache/tests/jit/gh21267.phpt | 35 +++++++++++++++++++ ext/opcache/tests/jit/gh21267_blacklist.phpt | 36 ++++++++++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 ext/opcache/tests/jit/gh21267.phpt create mode 100644 ext/opcache/tests/jit/gh21267_blacklist.phpt diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 3ddaa0270881e..b87abad436183 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 03b59eea61535..b5d980ca5afc6 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 0000000000000..91c8de29c2bfa --- /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-- +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 0000000000000..3ec222dc42509 --- /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-- +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