Skip to content

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

Merged
dstogov merged 1 commit intophp:PHP-8.4from
iliaal:fix/gh-21267-jit-fetch-obj-undef-loop
Mar 16, 2026
Merged

Fix GH-21267: JIT infinite loop on FETCH_OBJ_R with IS_UNDEF property#21368
dstogov merged 1 commit intophp:PHP-8.4from
iliaal:fix/gh-21267-jit-fetch-obj-undef-loop

Conversation

@iliaal
Copy link
Copy Markdown
Contributor

@iliaal iliaal commented Mar 7, 2026

Fixes #21267

When the JIT defers the IS_UNDEF check for FETCH_OBJ_R to the result type guard (the MAY_BE_GUARD + current_frame path), the deoptimization escape dispatches to opline->handler via the trace_escape stub. If opline->handler has been overwritten with JIT code (e.g. a function entry trace for a zero-param method where FETCH_OBJ_R is the first opline), this creates an infinite loop since no exit counters are bumped and no blacklisting occurs.

The fix changes zend_jit_escape_if_undef() to dispatch directly to orig_handler (the original VM handler stored in the trace extension) instead of going through trace_escape. This preserves the optimization of deferring the IS_UNDEF check to the result type guard while correctly handling the rare IS_UNDEF case during deoptimization.

Additionally, sets current_op_array in zend_jit_trace_exit_to_vm() so that the blacklisted exit deoptimizer can also resolve orig_handler, covering the case where side trace compilation is exhausted (jit_max_side_traces reached).

Test cases cover both the side trace path and the blacklisted exit path.

@dstogov
Copy link
Copy Markdown
Member

dstogov commented Mar 16, 2026

I see the problem, but the fix looks too pessimistic. It adds a guard check that is going to be almost always useless.
I should be possible to fix the problem in a better way. I'm continue working on this...

For now I simplified the test case . The first one fails on PHP-8.4 and above the second also fails on PHP-8.3

--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--
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
--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--
class C {
    public $x = true;
    public function __get($name) { return null; }
    public function getX() { return $this->x; }
}

$o1 = new C;
unset($o1->x);
$o2 = new C;
$a = [$o1, $o2, $o2];

for ($i = 0; $i < 8; $i++) {
    $m = $a[$i % 3];
    $m->getX();
    $m->getX();
}
?>
OK
--EXPECT--
OK

@dstogov
Copy link
Copy Markdown
Member

dstogov commented Mar 16, 2026

Following is a better fix:

diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 19b3ce12573..899188d5423 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -7972,7 +7972,19 @@ 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 overriden by JIT */
+	zend_jit_op_array_trace_extension *jit_extension =
+		(zend_jit_op_array_trace_extension*)ZEND_FUNC_INFO(jit->current_op_array);
+	size_t offset = jit_extension->offset;
+	zend_vm_opcode_handler_func_t orig_handler =
+		ZEND_OP_TRACE_INFO((opline - 1), offset)->orig_handler;
+	ir_ref ref = ir_CONST_ADDR(orig_handler);
+	if (GCC_GLOBAL_REGS || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL) {
+		zend_jit_tailcall_handler(jit, ref);
+	} else {
+		zend_jit_vm_enter(jit, ref);
+	}
 
 	ir_IF_TRUE(if_def);
 

This should be applied to PHP-8.4 and above.
@iliaal can you please update the PR.

iliaal added a commit to iliaal/php-src that referenced this pull request Mar 16, 2026
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 phpGH-21368.
@iliaal iliaal force-pushed the fix/gh-21267-jit-fetch-obj-undef-loop branch from 28e99da to 1d9aba6 Compare March 16, 2026 13:22
@iliaal iliaal changed the base branch from master to PHP-8.4 March 16, 2026 13:22
@iliaal
Copy link
Copy Markdown
Contributor Author

iliaal commented Mar 16, 2026

@dstogov Thanks for the review and the better fix. Updated the PR based on your approach — zend_jit_escape_if_undef() now dispatches to orig_handler directly instead of going through trace_escape. Simplified test case adopted, branch rebased to PHP-8.4.

One addition: I also set ctx.current_op_array in zend_jit_trace_exit_to_vm() before calling zend_jit_trace_deoptimization(). Without this, the blacklisted exit path (when jit_max_side_traces is exhausted) would hit a NULL current_op_array in zend_jit_escape_if_undef() since zend_jit_deoptimizer_start() doesn't set it. Added a second test with opcache.jit_max_side_traces=0 to cover this path — it reproduces the same infinite loop on unpatched code.

Comment on lines +8097 to +8107
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
ref = ir_CALL_1(IR_I32, ref, jit_FP(jit));
ir_GUARD(ir_GE(ref, ir_CONST_I32(0)), jit_STUB_ADDR(jit, jit_stub_trace_halt));
ir_RETURN(ir_CONST_I32(1)); // ZEND_VM_ENTER
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, my patch didn't handle all VM kinds right.
Anyway, I'm not completely sure if the "else" part of your patch is going to work for all VM kinds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The else branch matches the dispatch pattern used by zend_jit_trace_exit_stub (line 2527) and zend_jit_trace_return (line 17148) on PHP-8.4: ir_CALL_1(IR_I32, ...) + ir_GUARD + ir_RETURN(ZEND_VM_ENTER). On 8.4, JIT only runs with HYBRID VM kind — GCC_GLOBAL_REGS vs CALL convention covers both paths.

For a master forward-port this would need zend_jit_tailcall_handler/zend_jit_vm_enter to also handle ZEND_VM_KIND_TAILCALL.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we can use TAILCALL for the else part.

Comment on lines +7371 to +7375
/* Set current_op_array so that zend_jit_escape_if_undef() can
* resolve orig_handler to avoid dispatching to JIT-overwritten
* opline->handler (which would cause an infinite loop). */
ctx.current_op_array = zend_jit_traces[zend_jit_traces[trace_num].root].op_array;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ctx.current_op_array may be uninitialized, better pass op_array into zend_jit_escape_if_undef() as a parameter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — zend_jit_escape_if_undef now takes op_array as an explicit parameter. The call site passes jit->current_op_array, which is set by zend_jit_trace_start in the side-trace path and by the new initialization in zend_jit_trace_exit_to_vm for the blacklist path.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant not to set ctx.current_op_array, but pass the value directly into zend_jit_escape_if_undef() completely removing this chunk.

iliaal added a commit to iliaal/php-src that referenced this pull request Mar 16, 2026
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 phpGH-21368.
@iliaal iliaal force-pushed the fix/gh-21267-jit-fetch-obj-undef-loop branch from 1d9aba6 to c95f761 Compare March 16, 2026 14:04
Copy link
Copy Markdown
Member

@dstogov dstogov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reduced the patch to

diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 3ddaa027088..f5e821c2c66 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,21 @@ 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;
+	if (GCC_GLOBAL_REGS) {
+		ir_ref ref = ir_CONST_ADDR(ZEND_OP_TRACE_INFO((opline - 1), offset)->orig_handler);
+		ir_TAILCALL(IR_VOID, ref);
+	} else {
+		ir_ref ref = ir_CONST_ADDR(ZEND_OP_TRACE_INFO((opline - 1), offset)->call_handler);
+#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)) {

Do you see any problems?
Merge to master is going to be not trivial because of tail-call VM...

Comment on lines +7371 to +7375
/* Set current_op_array so that zend_jit_escape_if_undef() can
* resolve orig_handler to avoid dispatching to JIT-overwritten
* opline->handler (which would cause an infinite loop). */
ctx.current_op_array = zend_jit_traces[zend_jit_traces[trace_num].root].op_array;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant not to set ctx.current_op_array, but pass the value directly into zend_jit_escape_if_undef() completely removing this chunk.

Comment on lines +8097 to +8107
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
ref = ir_CALL_1(IR_I32, ref, jit_FP(jit));
ir_GUARD(ir_GE(ref, ir_CONST_I32(0)), jit_STUB_ADDR(jit, jit_stub_trace_halt));
ir_RETURN(ir_CONST_I32(1)); // ZEND_VM_ENTER
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we can use TAILCALL for the else part.

@dstogov
Copy link
Copy Markdown
Member

dstogov commented Mar 16, 2026

  • ir_ref ref = ir_CONST_ADDR(ZEND_OP_TRACE_INFO((opline - 1), offset)->call_handler);
    

It seems we may always use orig_handler.

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 phpGH-21368.
@iliaal
Copy link
Copy Markdown
Contributor Author

iliaal commented Mar 16, 2026

Updated — adopted all suggestions:

  1. zend_jit_escape_if_undef() takes op_array as an explicit parameter. The ctx.current_op_array assignment in zend_jit_trace_exit_to_vm() is removed; the call site passes exit_info->op_array directly.

  2. The non-GCC path now uses ir_TAILCALL_1 instead of ir_CALL_1 + ir_GUARD + ir_RETURN.

  3. orig_handler is used in both branches (per the follow-up comment — in the non-GCC path it's already the CALL-convention handler, so orig_handler == call_handler). Since both branches use the same handler, ref is hoisted before the if.

@dstogov
Copy link
Copy Markdown
Member

dstogov commented Mar 16, 2026

It's something wrong with github.
The commit 4cebe12 looks right, but PR still shows the old patch.

@iliaal iliaal force-pushed the fix/gh-21267-jit-fetch-obj-undef-loop branch from c95f761 to 4cebe12 Compare March 16, 2026 17:38
@iliaal
Copy link
Copy Markdown
Contributor Author

iliaal commented Mar 16, 2026

It's something wrong with github. The commit 4cebe12 looks right, but PR still shows the old patch.

There was wrong branch set for diff, should be ok now.

@dstogov
Copy link
Copy Markdown
Member

dstogov commented Mar 16, 2026

I don't see problems.
I'll merge this when tests passed.

@iliaal
Copy link
Copy Markdown
Contributor Author

iliaal commented Mar 16, 2026

Cool, thanks your help :)

@dstogov dstogov merged commit e1a3a4c into php:PHP-8.4 Mar 16, 2026
19 checks passed
dstogov added a commit that referenced this pull request Mar 16, 2026
* PHP-8.4:
  Fix GH-21267: JIT infinite loop on FETCH_OBJ_R with IS_UNDEF property (#21368)
dstogov added a commit that referenced this pull request Mar 16, 2026
* PHP-8.5:
  Fix support for TAILCALL VM
  Fix GH-21267: JIT infinite loop on FETCH_OBJ_R with IS_UNDEF property (#21368)
@vibbow
Copy link
Copy Markdown

vibbow commented Apr 10, 2026

This fix cause our application crash. We had to rollback to php 8.5.4

PHP 8.5.5 VS17 x64 Non Thread Safe on Windows Server 2022, running with IIS + FastCGI

dump file track to this line of code crash

ir_ref ref = ir_CONST_ADDR(ZEND_OP_TRACE_INFO((opline - 1), offset)->orig_handler);

*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************


KEY_VALUES_STRING: 1

    Key  : AV.Type
    Value: Read

    Key  : Analysis.CPU.mSec
    Value: 343

    Key  : Analysis.Elapsed.mSec
    Value: 1185

    Key  : Analysis.IO.Other.Mb
    Value: 0

    Key  : Analysis.IO.Read.Mb
    Value: 1

    Key  : Analysis.IO.Write.Mb
    Value: 0

    Key  : Analysis.Init.CPU.mSec
    Value: 281

    Key  : Analysis.Init.Elapsed.mSec
    Value: 3724

    Key  : Analysis.Memory.CommitPeak.Mb
    Value: 105

    Key  : Analysis.Version.DbgEng
    Value: 10.0.29547.1002

    Key  : Analysis.Version.Description
    Value: 10.2602.27.2 amd64fre

    Key  : Analysis.Version.Ext
    Value: 1.2602.27.2

    Key  : Failure.Bucket
    Value: INVALID_POINTER_READ_c0000005_php8.dll!zend_jit_escape_if_undef

    Key  : Failure.Exception.Code
    Value: 0xc0000005

    Key  : Failure.Exception.IP.Address
    Value: 0x7ff8745c21a0

    Key  : Failure.Exception.IP.Module
    Value: php8

    Key  : Failure.Exception.IP.Offset
    Value: 0x3721a0

    Key  : Failure.Hash
    Value: {975c7f30-0c3e-ea22-ef42-5cbfda68e2ac}

    Key  : Failure.ProblemClass.Primary
    Value: INVALID_POINTER_READ

    Key  : Faulting.IP.Type
    Value: Paged

    Key  : Timeline.OS.Boot.DeltaSec
    Value: 876094

    Key  : Timeline.Process.Start.DeltaSec
    Value: 40

    Key  : WER.OS.Branch
    Value: fe_release

    Key  : WER.OS.Version
    Value: 10.0.20348.1

    Key  : WER.Process.Version
    Value: 8.5.5.0


FILE_IN_CAB:  php-cgi.exe.896.dmp

NTGLOBALFLAG:  0

APPLICATION_VERIFIER_FLAGS:  0

CONTEXT:  (.ecxr)
rax=000001ed861086a0 rbx=00001000024f4358 rcx=0000000000000000
rdx=000000000000000f rsi=000000daafdf0bb0 rdi=0000000080000800
rip=00007ff8745c21a0 rsp=000000daafdf08d0 rbp=0000000000000006
 r8=00000000fffffff9  r9=0000026f38c98040 r10=0000000000000000
r11=000000daafdf07e0 r12=000010000293edc8 r13=00001000024f4358
r14=000000daafdf0bb0 r15=0000000080000800
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
php8!zend_jit_escape_if_undef+0x100:
00007ff8`745c21a0 488b8cc8d0000000 mov     rcx,qword ptr [rax+rcx*8+0D0h] ds:000001ed`86108770=????????????????
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ff8745c21a0 (php8!zend_jit_escape_if_undef+0x0000000000000100)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 0000000000000000
   Parameter[1]: 000001ed86108770
Attempt to read from address 000001ed86108770

PROCESS_NAME:  php-cgi.exe

READ_ADDRESS:  000001ed86108770 

ERROR_CODE: (NTSTATUS) 0xc0000005 - 0x%p            0x%p                    %s

EXCEPTION_CODE_STR:  c0000005

EXCEPTION_PARAMETER1:  0000000000000000

EXCEPTION_PARAMETER2:  000001ed86108770

STACK_TEXT:  
000000da`afdf08d0 00007ff8`745f5d23     : 00001000`0293f5a3 00000000`00000001 00000000`00000000 000000da`afdf7550 : php8!zend_jit_escape_if_undef+0x100
000000da`afdf0910 00007ff8`745f78e5     : 00000000`00000001 00001000`0293edc8 00001000`0293f598 00000000`00000001 : php8!zend_jit_trace_deoptimization+0x4a3
000000da`afdf09b0 00007ff8`74609adb     : 0000026f`00000020 000000da`00000000 00000000`00000036 00000000`00000005 : php8!zend_jit_trace+0x8a5
000000da`afdf12f0 00007ff8`7460a246     : 00390034`00360032 0033002d`00370039 00310037`00000000 00000000`00000000 : php8!zend_jit_compile_side_trace+0x18b
000000da`afdf74a0 00007ff8`745acf36     : 00001000`008a6a70 00000000`00000000 00000000`00000000 000000da`afdfb630 : php8!zend_jit_trace_hot_side+0x4d6
000000da`afdfb570 00001000`20000574     : 00000000`00000010 000000da`0000008a 000000da`afdfb690 0000026f`38c14260 : php8!zend_jit_trace_exit+0x6a6
000000da`afdfb610 00007ff8`7425febf     : 0000026f`38c14140 0000026f`38c140d0 00000000`00000000 00000000`00000000 : 0x00001000`20000574
000000da`afdfb7d0 00007ff8`746f9fdd     : 00000000`00000001 000000da`afdfe610 00000000`00000000 00000000`00000000 : php8!execute_ex_real+0x4f
000000da`afdfb800 00007ff8`7427ade6     : 00000000`00000000 00000000`00000001 00000000`00000000 00000000`00000000 : php8!execute_ex+0x4d
000000da`afdfb880 00007ff8`7427cc9d     : eac85028`91199f0d 00001000`00884228 0000026f`38c5d040 00000000`00000000 : php8!zend_execute+0x146
000000da`afdfb8b0 00007ff8`7427bf9f     : 00000000`00000000 00000000`00000000 00000000`0000000b 00000000`00000000 : php8!zend_execute_script+0x1fd
000000da`afdfb910 00007ff6`769d6ec5     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : php8!php_execute_script_ex+0x23f
000000da`afdfe4f0 00007ff6`769d2d18     : 00000000`00000000 00007ff6`769d2d99 0000026f`38012880 00000000`00000000 : php_cgi!main+0x1265
(Inline Function) --------`--------     : --------`-------- --------`-------- --------`-------- --------`-------- : php_cgi!invoke_main+0x22
000000da`afdffa80 00007ff8`92594cb0     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : php_cgi!__scrt_common_main_seh+0x10c
000000da`afdffac0 00007ff8`945dedfb     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0x10
000000da`afdffaf0 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x2b


STACK_COMMAND: ~0s; .ecxr ; kb

IP_IN_PAGED_CODE: 
php8!zend_jit_escape_if_undef+100 [C:\Users\runneradmin\AppData\Local\Temp\php-ce4c63e5-895d-4ee8-b4a6-81d2c81b3d77\config\vs17\x64\php-8.5.5\ext\opcache\jit\zend_jit_ir.c @ 7980]
00007ff8`745c21a0 488b8cc8d0000000 mov     rcx,qword ptr [rax+rcx*8+0D0h]

FAULTING_SOURCE_LINE:  C:\Users\runneradmin\AppData\Local\Temp\php-ce4c63e5-895d-4ee8-b4a6-81d2c81b3d77\config\vs17\x64\php-8.5.5\ext\opcache\jit\zend_jit_ir.c

FAULTING_SOURCE_FILE:  C:\Users\runneradmin\AppData\Local\Temp\php-ce4c63e5-895d-4ee8-b4a6-81d2c81b3d77\config\vs17\x64\php-8.5.5\ext\opcache\jit\zend_jit_ir.c

FAULTING_SOURCE_LINE_NUMBER:  7980

FAULTING_SOURCE_CODE:  
  7976: 	/* We can't use trace_escape() because opcode handler may be overridden by JIT */
  7977: 	zend_jit_op_array_trace_extension *jit_extension =
  7978: 		(zend_jit_op_array_trace_extension*)ZEND_FUNC_INFO(op_array);
  7979: 	size_t offset = jit_extension->offset;
> 7980: 	ir_ref ref = ir_CONST_ADDR(ZEND_OP_TRACE_INFO((opline - 1), offset)->orig_handler);
  7981: 	if (GCC_GLOBAL_REGS || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL) {
  7982: 		ir_TAILCALL(IR_OPCODE_HANDLER_RET, ref);
  7983: 	} else {
  7984: #if defined(IR_TARGET_X86)
  7985: 		ref = ir_CAST_FC_FUNC(ref);


SYMBOL_NAME:  php8!zend_jit_escape_if_undef+100

MODULE_NAME: php8

IMAGE_NAME:  php8.dll

FAILURE_BUCKET_ID:  INVALID_POINTER_READ_c0000005_php8.dll!zend_jit_escape_if_undef

OS_VERSION:  10.0.20348.1

BUILDLAB_STR:  fe_release

OSPLATFORM_TYPE:  x64

OSNAME:  Windows 10

IMAGE_VERSION:  8.5.5.0

FAILURE_ID_HASH:  {975c7f30-0c3e-ea22-ef42-5cbfda68e2ac}

Followup:     MachineOwner

iliaal added a commit to iliaal/php-src that referenced this pull request Apr 10, 2026
…ndef

PR php#21368 replaced the trace_escape stub dispatch in zend_jit_escape_if_undef
with a compile-time constant load of orig_handler, computed from the exit
info's op_array pointer. That pointer can be NULL (when current_frame is
NULL at exit-point creation) or stale (when the underlying op_array is
freed before the side trace compiles), producing an access violation
inside zend_jit_escape_if_undef. Reported on PHP 8.5.5 Windows NTS.

Drop the op_array parameter and emit a runtime lookup via
zend_jit_orig_opline_handler() instead. That helper resolves the
jit_extension through EX(func) at dispatch time, which is valid
regardless of the compile-time op_array state. The gh21267 tests
still pass, confirming the infinite-loop fix is preserved.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JIT tracing: infinite loop on FETCH_OBJ_R with IS_UNDEF property in polymorphic context

3 participants