Skip to content

JIT: persist the SHM op_array in trace exit_info - #21710

Open
iliaal wants to merge 1 commit into
php:PHP-8.4from
iliaal:fix/gh21368-escape-if-undef-runtime-lookup
Open

JIT: persist the SHM op_array in trace exit_info#21710
iliaal wants to merge 1 commit into
php:PHP-8.4from
iliaal:fix/gh21368-escape-if-undef-runtime-lookup

Conversation

@iliaal

@iliaal iliaal commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

zend_jit_trace_get_exit_point stored exit_info.op_array from the current frame. For methods of linked classes that miss the inheritance cache, that pointer is a heap copy of the op_array header. Trace metadata lives in SHM, so a later request or process then crashes in zend_jit_escape_if_undef. It now stores the original from the JIT extension, as root traces already do.

@dstogov

dstogov commented Apr 13, 2026

Copy link
Copy Markdown
Member

It would be great to understand where the failure comes from.
If we somehow have an inconsistent exit_info->op_array we may get troubles in other places.

As I see, JIT_G(current_frame) is always set during trace compilation (it can't be NULL) , so this part of deduction can not be the reason of the failure.

The fix generates more expensive JIT code that performs run-time resolution, I believe it should be possible to make this resolution at compile time.

@vibbow

vibbow commented Apr 16, 2026

Copy link
Copy Markdown

I can reproduce this bug consistently. If somone can help compile a Windows version, I can test this fix

@iliaal
iliaal force-pushed the fix/gh21368-escape-if-undef-runtime-lookup branch from 4c71cf2 to 7c69d4b Compare April 17, 2026 11:19
@iliaal

iliaal commented Apr 17, 2026

Copy link
Copy Markdown
Contributor Author
  1. JIT_G(current_frame) is allocated at zend_jit_trace.c:1428 before any
    exit-point creation, so the NULL hypothesis in my PR description was
    wrong. Updated the description.

  2. The crash fits a stale exit_info->op_array, not a NULL one: rax+0xD0
    on an unmapped page, long-lived IIS+FastCGI worker. If this pointer
    can go stale, it's a broader invariant concern (zend_jit_dump_exit_info
    is the only other direct consumer today). I haven't yet pinned down
    the concrete path by which op_array is freed while traces referencing
    it survive.

  3. Reworked the fix to keep compile-time dispatch:

    • zend_jit_escape_if_undef now uses jit->current_op_array for the
      ZEND_FUNC_INFO lookup (back to your original approach).
    • On the side-trace compile path (the actual crash path),
      zend_jit_trace_start sets jit->current_op_array to
      trace_buffer->op_array, which is freshly captured for this
      compilation and doesn't rely on the parent's possibly stale
      exit_info.
    • On the zend_jit_trace_exit_to_vm path, zend_jit_deoptimizer_start
      leaves current_op_array unset, so I set it from exit_info->op_array
      before the deopt call.

PR updated.

Comment thread ext/opcache/jit/zend_jit_ir.c Outdated
@@ -8094,7 +8094,7 @@ static int zend_jit_escape_if_undef(zend_jit_ctx *jit, int var, uint32_t flags,

/* 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);
(zend_jit_op_array_trace_extension*)ZEND_FUNC_INFO(jit->current_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.

Are you sure jit->current_op_array is always initialized?
It seems, it may be NULL on the following patch - zend_jit_trace_exit() -> zend_jit_trace_hot_side() -> zend_jit_blacklist_trace_exit() -> zend_jit_trace_exit_to_vm() - > zend_jit_trace_deoptimization() -> zend_jit_escape_if_undef().

May be I'm wrong...

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.

exit_info.op_array is written in one place, zend_jit_trace_get_exit_point (trace.c:197), and the surrounding code at trace.c:146-164 assigns op_array and stack_size together: JIT_G(current_frame) non-NULL gives a real op_array with some stack_size; the else branch sets both op_array = NULL and stack_size = 0 in the same step.

So on your path, ctx.current_op_array == NULL only when exit_info.stack_size == 0, which means zend_jit_trace_deoptimization loops over zero entries, check2 stays at -1, and zend_jit_escape_if_undef at trace.c:3606 isn't called. The NULL never reaches ZEND_FUNC_INFO(jit->current_op_array).

Can add ZEND_ASSERT(exit_info[exit_num].op_array || exit_info[exit_num].stack_size == 0) to make it explicit.

@iluuu1994

Copy link
Copy Markdown
Member

Should the original fix be reverted until we fully understand the issue? I don't have the time to look at the problem, and also don't have a Windows setup. Otherwise we'll miss the next release cycle (I don't feel comfortable merging this without an RC-phase).

@iliaal

iliaal commented Apr 24, 2026

Copy link
Copy Markdown
Contributor Author

Possibly, same as you I cannot reproduce the issue due to lack of windows env, fix is made based on code analysis. Ultimately be nice to have windows build the reporter could try, maybe revert from 8.5/8.4 keep in master? And then try fully patched version for 8.4/8.5 back port after current release is out?

@shivammathur

Copy link
Copy Markdown
Member

@iliaal

jit->current_op_array may not be stale on the side-trace compile path after this PR, but the remaining compile-time ZEND_OP_TRACE_INFO((opline - 1), offset)->orig_handler lookup still seems unsafe there. Would using zend_jit_orig_opline_handler(jit) after jit_LOAD_IP_ADDR(jit, opline - 1) be safer?

@iliaal

iliaal commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

Yes, (opline - 1) has the same compile-time staleness exposure. Before another rework, the blocker is repro: none of us with patches in flight can hit this, only vibbow can, and there's no Windows build of any patch for them to test. Two rounds so far on code reasoning alone, and a third would be the same. @dstogov, given that: (a) switch to runtime resolution now and get a Windows build to vibbow for validation, (b) hold while we dig into why exit_info goes stale in the first place, or (c) revert GH-21368 from 8.4/8.5 per @iluuu1994's RC concern, keep the fix in master, and land a Windows-validated patch later. Compile-time vs runtime is downstream of which option you pick.

@shivammathur

Copy link
Copy Markdown
Member

@iliaal

I’ve built Windows binaries for this PR here:
https://github.com/php/php-windows-builder/actions/runs/25253712864
You can find them in artifacts.zip.

@iliaal

iliaal commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

@iliaal

I’ve built Windows binaries for this PR here: https://github.com/php/php-windows-builder/actions/runs/25253712864 You can find them in artifacts.zip.

Oh, that's awesome thank you! I'll ask the original bug reporter to try, hopefully good news. Just to confirm these are based on latest state of the PR?

@shivammathur

Copy link
Copy Markdown
Member

@iliaal

Oh, that's awesome thank you! I'll ask the original bug reporter to try, hopefully good news. Just to confirm these are based on latest state of the PR?

Yes, it is based on the latest state of the PR.

@iliaal

iliaal commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

I can reproduce this bug consistently. If somone can help compile a Windows version, I can test this fix

@vibbow Can you please try the builds shivammathur prepared (https://github.com/php/php-windows-builder/actions/runs/25253712864 ) if that fixes the issue. As you can tell from the convo, none of us are able to reproduce the issue.

@vibbow

vibbow commented May 2, 2026

Copy link
Copy Markdown

@iliaal @shivammathur The origional crash is happened on php 8.5.5

Today I tested with PHP 8.4.20 and PHP 8.4.21-dev (link provided), none of them crashed.

And PHP 8.5.5 is still crashed.

Can you made a php 8.5.6-dev build so I can test with it?

iliaal referenced this pull request May 12, 2026
…#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.
@iliaal
iliaal force-pushed the fix/gh21368-escape-if-undef-runtime-lookup branch from 7c69d4b to 0777c51 Compare May 12, 2026 13:06
iliaal added a commit to iliaal/php-src that referenced this pull request May 13, 2026
zend_jit_escape_if_undef received an op_array pointer captured at
parent-trace compile time. That pointer can go stale by the time a
side trace compiles for the exit. Drop the parameter and read
jit->current_op_array instead; zend_jit_trace_start sets it for
trace compilation, and zend_jit_trace_exit_to_vm now seeds it from
exit_info->op_array so the deoptimizer path has it too.

Closes phpGH-21710
@iliaal
iliaal force-pushed the fix/gh21368-escape-if-undef-runtime-lookup branch from 0777c51 to 3079a72 Compare May 13, 2026 16:37
@vibbow

vibbow commented Jun 16, 2026

Copy link
Copy Markdown

Today I upgrade to php 8.5.7, and still get this issue.

And this time I created a full process crash dump. Hope this can help with solve this issue:
http://static.vsean.net/crashdumps/php-cgi.exe.4368.zip

Following is the crash dump analysis:

0:000> !analyze -v
................................................................
.....................
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************


KEY_VALUES_STRING: 1

    Key  : AV.Type
    Value: Read

    Key  : Analysis.CPU.mSec
    Value: 406

    Key  : Analysis.Elapsed.mSec
    Value: 1384

    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: 265

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

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

    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: 0x7ff8f3272ac3

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

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

    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: 572529

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

    Key  : WER.OS.Branch
    Value: ge_release

    Key  : WER.OS.Version
    Value: 10.0.26100.1

    Key  : WER.Process.Version
    Value: 8.5.7.0


FILE_IN_CAB:  php-cgi.exe.4024.dmp

NTGLOBALFLAG:  0

APPLICATION_VERIFIER_FLAGS:  0

CONTEXT:  (.ecxr)
rax=000002158ebe6498 rbx=0000100002407a98 rcx=0000000000000000
rdx=000000000000000f rsi=0000001df3ff06c0 rdi=0000000080000800
rip=00007ff8f3272ac3 rsp=0000001df3ff03e0 rbp=0000000000000006
 r8=0000000000000000  r9=000002158c179240 r10=0000000000000000
r11=0000001df3ff02f0 r12=0000100002c16338 r13=0000100002407a98
r14=0000001df3ff06c0 r15=0000000080000800
iopl=0         nv up ei pl zr na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
php8!jit_CONST_FUNC+0x8 [inlined in php8!zend_jit_escape_if_undef+0x103]:
00007ff8`f3272ac3 488b8cc8d0000000 mov     rcx,qword ptr [rax+rcx*8+0D0h] ds:00000215`8ebe6568=????????????????
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ff8f3272ac3 (php8!jit_CONST_FUNC+0x0000000000000008)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 0000000000000000
   Parameter[1]: 000002158ebe6568
Attempt to read from address 000002158ebe6568

PROCESS_NAME:  php-cgi.exe

READ_ADDRESS:  000002158ebe6568 

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

EXCEPTION_CODE_STR:  c0000005

EXCEPTION_PARAMETER1:  0000000000000000

EXCEPTION_PARAMETER2:  000002158ebe6568

STACK_TEXT:  
(Inline Function) --------`--------     : --------`-------- --------`-------- --------`-------- --------`-------- : php8!jit_CONST_FUNC+0x8
0000001d`f3ff03e0 00007ff8`f32a6563     : 00001000`02c16e83 00000000`00000001 00000000`00000000 0000001d`f3ff7060 : php8!zend_jit_escape_if_undef+0x103
0000001d`f3ff0420 00007ff8`f32a8135     : 00000000`00000001 00001000`02c16338 00001000`02c16e78 00000000`00000001 : php8!zend_jit_trace_deoptimization+0x4a3
0000001d`f3ff04c0 00007ff8`f32ba335     : 00001000`00000020 00000000`00003a80 000001f0`00000000 00000000`00000005 : php8!zend_jit_trace+0x8b5
0000001d`f3ff0e00 00007ff8`f32baaae     : 00000000`00000022 0000001d`f3ff7020 00000000`00000022 00000000`00000000 : php8!zend_jit_compile_side_trace+0x185
0000001d`f3ff6fb0 00007ff8`f325d886     : 00001000`008b1588 00000000`00000000 00000000`00000000 0000001d`f3ffb140 : php8!zend_jit_trace_hot_side+0x4de
0000001d`f3ffb080 00001000`20000574     : 00001000`00000034 0000001d`0000008d 0000001d`f3ffb1a0 00000215`8c013c00 : php8!zend_jit_trace_exit+0x6a6
0000001d`f3ffb120 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : 0x00001000`20000574


STACK_COMMAND: ~0s; .ecxr ; kb

IP_IN_PAGED_CODE: 
php8!zend_jit_escape_if_undef+103 [C:\Users\runneradmin\AppData\Local\Temp\php-4e72ff57-341f-4859-9ceb-59956af1c221\config\vs17\x64\php-8.5.7\ext\opcache\jit\zend_jit_ir.c @ 7982]
00007ff8`f3272ac3 488b8cc8d0000000 mov     rcx,qword ptr [rax+rcx*8+0D0h]

FAULTING_SOURCE_LINE:  C:\Users\runneradmin\AppData\Local\Temp\php-4e72ff57-341f-4859-9ceb-59956af1c221\config\vs17\x64\php-8.5.7\ext\opcache\jit\zend_jit_ir.c

FAULTING_SOURCE_FILE:  C:\Users\runneradmin\AppData\Local\Temp\php-4e72ff57-341f-4859-9ceb-59956af1c221\config\vs17\x64\php-8.5.7\ext\opcache\jit\zend_jit_ir.c

FAULTING_SOURCE_LINE_NUMBER:  7982

FAULTING_SOURCE_CODE:  
   575: #else
   576: 	ir_ref proto = 0;
   577: #endif
   578: 
>  579: 	return jit_CONST_FUNC_PROTO(jit, addr, proto);
   580: }
   581: 
   582: static ir_ref jit_CONST_OPCODE_HANDLER_FUNC(zend_jit_ctx *jit, zend_vm_opcode_handler_t handler)
   583: {
   584: 	return jit_CONST_FUNC(jit, (uintptr_t)handler, IR_FASTCALL_FUNC);


SYMBOL_NAME:  php8!zend_jit_escape_if_undef+103

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.26100.1

BUILDLAB_STR:  ge_release

OSPLATFORM_TYPE:  x64

OSNAME:  Windows 10

IMAGE_VERSION:  8.5.7.0

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

Followup:     MachineOwner
---------

@vibbow

vibbow commented Jun 22, 2026

Copy link
Copy Markdown

Update:

If I change opcache.jit from 1254 to 1205, it crashed at different place:

0:000> !analyze -v
................................................................
.....................
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************


KEY_VALUES_STRING: 1

    Key  : AV.Type
    Value: Read

    Key  : Analysis.CPU.mSec
    Value: 406

    Key  : Analysis.Elapsed.mSec
    Value: 1421

    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: 296

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

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

    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!ir_fix_bb_order

    Key  : Failure.Exception.Code
    Value: 0xc0000005

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

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

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

    Key  : Failure.Hash
    Value: {ae9eb934-375f-4f66-2aff-a6e27b32fba5}

    Key  : Failure.ProblemClass.Primary
    Value: INVALID_POINTER_READ

    Key  : Faulting.IP.Type
    Value: Paged

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

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

    Key  : WER.OS.Branch
    Value: ge_release

    Key  : WER.OS.Version
    Value: 10.0.26100.1

    Key  : WER.Process.Version
    Value: 8.5.7.0


FILE_IN_CAB:  php-cgi.exe.9580.dmp

NTGLOBALFLAG:  0

APPLICATION_VERIFIER_FLAGS:  0

CONTEXT:  (.ecxr)
rax=00000000a0000082 rbx=0000000000000000 rcx=00000265d2c000c0
rdx=0000000000000140 rsi=0000000000000008 rdi=00000265d5734000
rip=00007ff8f2d35789 rsp=000000dd6d9fa8f0 rbp=000000dd6d9fadf0
 r8=000000000000006d  r9=00000265d58e5060 r10=0000000000000000
r11=00000265d58d6000 r12=00000265d563b940 r13=00000265d58e5000
r14=0000000000000001 r15=00000265d5883000
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!ir_fix_bb_order+0x2b9:
00007ff8`f2d35789 8b0c87          mov     ecx,dword ptr [rdi+rax*4] ds:00000268`55734208=????????
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ff8f2d35789 (php8!ir_fix_bb_order+0x00000000000002b9)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 0000000000000000
   Parameter[1]: 0000026855734208
Attempt to read from address 0000026855734208

PROCESS_NAME:  php-cgi.exe

READ_ADDRESS:  0000026855734208 

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

EXCEPTION_CODE_STR:  c0000005

EXCEPTION_PARAMETER1:  0000000000000000

EXCEPTION_PARAMETER2:  0000026855734208

STACK_TEXT:  
000000dd`6d9fa8f0 00007ff8`f2d330d0     : 00000265`00000190 00000265`d58d6000 00000265`d5883000 00000265`d5737000 : php8!ir_fix_bb_order+0x2b9
000000dd`6d9fa970 00007ff8`f2c95642     : 000000dd`6d9fadf0 00001000`02e33630 000000dd`6d9fadf0 00000265`00001280 : php8!ir_schedule+0x180
000000dd`6d9faa40 00007ff8`f2cbeb0d     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : php8!zend_jit_ir_compile+0x172
000000dd`6d9faa90 00007ff8`f2cca83a     : 00000000`e0fffbff 00000000`00000000 00000000`0000000a 00000000`000003fe : php8!zend_jit_finish+0x7d
000000dd`6d9faae0 00007ff8`f2c80d80     : 00007ff8`f35e8cc8 00000265`d576cda0 00001000`02e08340 00000265`d576c5e8 : php8!zend_jit+0x6a8a
000000dd`6d9fb320 00007ff8`f2c77141     : 00001000`02e093b8 00001000`02e093a0 00000000`00000000 00001000`02e08340 : php8!zend_jit_script+0x270
000000dd`6d9fb390 00007ff8`f2c6587e     : 00000265`d56f5c00 00000265`d56f5c00 00000000`00000008 00000265`d56f5c00 : php8!zend_accel_script_persist+0x341
000000dd`6d9fb3d0 00007ff8`f2c62ab1     : 00000000`00000008 00000000`00000001 000000dd`6d9fb4f0 00000265`d56f5c00 : php8!cache_script_in_shared_memory+0x19e
000000dd`6d9fb410 00007ff8`f2968e1d     : 00000000`00000008 000000dd`6d9fb590 00000000`00000008 00007ff8`f29732a0 : php8!persistent_compile_file+0x5c1
000000dd`6d9fb4d0 00007ff8`f2968a96     : 00000265`d2c133e0 00000000`00000000 00000000`0000003c 00000265`d2c7bc00 : php8!compile_filename+0x3d
000000dd`6d9fb560 00007ff8`f2fda9d4     : 00000265`d2c13430 00000000`00000008 00000265`d2c13230 00000000`00000008 : php8!zend_include_or_eval+0x7e
000000dd`6d9fb620 00007ff8`f292ed6f     : 00000265`d2c13430 00000265`d2c13230 00000000`00000000 00000000`00130000 : php8!ZEND_INCLUDE_OR_EVAL_SPEC_TMPVAR_HANDLER+0x34
000000dd`6d9fb670 00007ff8`f2ddbd02     : 00000000`00000054 00000000`00000000 00000000`00000000 00001000`021975c0 : php8!execute_ex_real+0x4f
000000dd`6d9fb6a0 00007ff8`f29687b6     : 00000000`00000000 00000000`00000001 00000000`00000000 00000000`00000000 : php8!execute_ex+0x52
000000dd`6d9fb770 00007ff8`f292482d     : b036aca5`0293a384 00001000`00884d88 00000265`d2c612c0 00000000`00000000 : php8!zend_execute+0x146
000000dd`6d9fb7a0 00007ff8`f2923b2f     : 00000000`00000000 00000000`00000000 00000000`0000000b 00000000`00000000 : php8!zend_execute_script+0x1fd
000000dd`6d9fb800 00007ff6`15a66ec5     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : php8!php_execute_script_ex+0x23f
000000dd`6d9fe3e0 00007ff6`15a62d18     : 00000000`00000000 00000000`00000000 00000265`d1d71f60 00000000`00000000 : php_cgi!main+0x1265
(Inline Function) --------`--------     : --------`-------- --------`-------- --------`-------- --------`-------- : php_cgi!invoke_main+0x22
000000dd`6d9ff970 00007ff9`114ee8d7     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : php_cgi!__scrt_common_main_seh+0x10c
000000dd`6d9ff9b0 00007ff9`11e2c53c     : 00000000`00000000 00000000`00000000 000004f0`fffffb30 000004d0`fffffb30 : kernel32!BaseThreadInitThunk+0x17
000000dd`6d9ff9e0 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x2c


STACK_COMMAND: ~0s; .ecxr ; kb

IP_IN_PAGED_CODE: 
php8!ir_fix_bb_order+2b9 [C:\Users\runneradmin\AppData\Local\Temp\php-4e72ff57-341f-4859-9ceb-59956af1c221\config\vs17\x64\php-8.5.7\ext\opcache\jit\ir\ir_gcm.c @ 952]
00007ff8`f2d35789 8b0c87          mov     ecx,dword ptr [rdi+rax*4]

FAULTING_SOURCE_LINE:  C:\Users\runneradmin\AppData\Local\Temp\php-4e72ff57-341f-4859-9ceb-59956af1c221\config\vs17\x64\php-8.5.7\ext\opcache\jit\ir\ir_gcm.c

FAULTING_SOURCE_FILE:  C:\Users\runneradmin\AppData\Local\Temp\php-4e72ff57-341f-4859-9ceb-59956af1c221\config\vs17\x64\php-8.5.7\ext\opcache\jit\ir\ir_gcm.c

FAULTING_SOURCE_LINE_NUMBER:  952

FAULTING_SOURCE_CODE:  
   948: 	prev = 0;
   949: 	for (b = 1, bb = new_blocks + 1; b <= ctx->cfg_blocks_count; b++, bb++) {
   950: 		bb->idom = xlat[bb->idom];
   951: 		bb->loop_header = xlat[bb->loop_header];
>  952: 		n = bb->successors_count;
   953: 		if (n > 0) {
   954: 			for (q = ctx->cfg_edges + bb->successors; n > 0; q++, n--) {
   955: 				*q = xlat[*q];
   956: 			}
   957: 		}


SYMBOL_NAME:  php8!ir_fix_bb_order+2b9

MODULE_NAME: php8

IMAGE_NAME:  php8.dll

FAILURE_BUCKET_ID:  INVALID_POINTER_READ_c0000005_php8.dll!ir_fix_bb_order

OS_VERSION:  10.0.26100.1

BUILDLAB_STR:  ge_release

OSPLATFORM_TYPE:  x64

OSNAME:  Windows 10

IMAGE_VERSION:  8.5.7.0

FAILURE_ID_HASH:  {ae9eb934-375f-4f66-2aff-a6e27b32fba5}

Followup:     MachineOwner

@iliaal
iliaal requested a review from iluuu1994 June 25, 2026 20:01
@vibbow

vibbow commented Jul 21, 2026

Copy link
Copy Markdown

Tried with PHP 8.5.8

If I set opcache.jit -> optimization level to 4 or 5, it will crash like before. 0-3 works fine.

@vibbow

vibbow commented Aug 5, 2026

Copy link
Copy Markdown

PHP 8.5.9, this issue still exists.

But when I try to run PHP direct with php-cgi.exe -b 127.0.0.1:9000, and do the exactly same request test, PHP didn't crash.

So it must be something wrong only with IIS + PHP FastCGI, maybe the shared memory model.

@vibbow

vibbow commented Aug 5, 2026

Copy link
Copy Markdown

Since I'm guessing it's something wrong with shared memory, so I add following php.ini config:

opcache.file_cache=D:\PHP\Temp\OPcache
opcache.file_cache_only=1

And PHP no longer crashs !

Hope this information can help you guys to debug this issue.

@iluuu1994

Copy link
Copy Markdown
Member

@arnaud-lb Is this maybe something you can review?

@arnaud-lb

arnaud-lb commented Aug 14, 2026

Copy link
Copy Markdown
Member

I'm able to reproduce on Linux:

The root cause is that exit_info.op_array may be initialized with a non-SHM op_array which is invalid in other processes or subsequent requests.

This can happen at least for methods of linked classes that couldn't be cached in the inheritance cache, because the op_array of these methods is copied to the heap during linking and never persisted back to SHM, but since the original op_array was instrumented with JIT, it is JIT-able.

I guess that it happens more often on Windows because linked classes are more likely to not be cached on this platforms, but it can happen on others too.

Reproducer:

$pid = pcntl_fork();
if ($pid) {
    pcntl_waitpid($pid, $status, 0);

    $buf = [];
    for ($i = 0; $i < 100; $i++) {
        $buf[] = str_repeat('a', $i*100);
    }

    require 'test.inc';

    for ($i = 0; $i < 1000; $i++) {
        // Using getenv() to disable optimizations
        // Using call_user_func() so that C::f() is the root of the trace
        getenv('call_user_func')('C::f', [true]);
    }
} else {
    require 'test.inc';

    for ($i = 0; $i < 1000; $i++) {
        getenv('call_user_func')('C::f', [false]);
    }
}
// test.inc

if (getenv('call_user_func')) {
    eval('class P {}');
}

// This class is not cached after linking because the parent is not in SHM
class C extends P {
    static function f($v) {
        // $v[0] triggers escape_if_undef when the type changes
        return $v[0];
        if ($a) {
            return 1;
        } else {
            return 2;
        }
    }
}
$ USE_ZEND_ALLOC=0 call_user_func=call_user_func php -d opcache.jit=tracing test.php

A possible fix is to change

t->exit_info[exit_point].op_array = op_array;

so that it fetches the original op_array from the func info as we do in

if (!(t->op_array->fn_flags & ZEND_ACC_IMMUTABLE)) {
zend_jit_op_array_trace_extension *jit_extension =
(zend_jit_op_array_trace_extension*)ZEND_FUNC_INFO(t->op_array);
t->op_array = jit_extension->op_array;

@luthermonson

Copy link
Copy Markdown
Contributor

Independent reproduction, and a repro that needs neither IIS nor FastCGI — just php.exe -S and a stock Laravel skeleton. Posting the data here rather than opening a duplicate issue, since @arnaud-lb's analysis above already names the root cause exactly.

Repro

Official Windows builds from downloads.php.net, x64 TS, no extra software:

; php.ini
extension_dir = "ext"
extension=mbstring
extension=openssl
extension=fileinfo
extension=pdo_sqlite
extension=sqlite3
opcache.enable = 1
opcache.enable_cli = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.jit = tracing
opcache.jit_buffer_size = 64M
composer create-project laravel/laravel app     # Laravel 13, default skeleton
cd app && composer dump-autoload -o
php -c php.ini -S 127.0.0.1:8080 -t public

Then GET / in a loop. The process dies with 0xC0000005 on the third request, every time.

Bisect

Same app, same ini, 150 requests per cell, one process per cell. "Served" counts [200] lines the built-in server logged before it died; every crashing cell was reproduced at least twice with an identical count.

PHP (Win x64 TS) Requests served
8.4.14 150 — clean
8.4.24 7, then 0xC0000005
8.5.0 150 — clean
8.5.4 150 — clean
8.5.5 1, then 0xC0000005
8.5.7 2, then 0xC0000005
8.5.9 2, then 0xC0000005

That window brackets #21368 (merged 2026-03-16 to PHP-8.4) on both branches. Every "clean" row was verified to have the JIT genuinely on (opcache_get_status()['jit']['on'] === true) — my first pass had a false negative on 8.4 because OPcache is a separate DLL there and I had not loaded it.

Stack (8.5.9, official debug pack)

ExceptionAddress: php8ts!jit_CONST_FUNC+0x8 [inlined in php8ts!zend_jit_escape_if_undef+0x103]
   ExceptionCode: c0000005 (Access violation)
Attempt to read from address 00000294b44df038

php8ts!zend_jit_escape_if_undef+0x103        <- mov rcx,qword ptr [rax+rcx*8+0D0h]
php8ts!zend_jit_trace_deoptimization+0x500
php8ts!zend_jit_trace+0x961
php8ts!zend_jit_compile_side_trace+0x1c3
php8ts!zend_jit_trace_hot_side+0x47f
php8ts!zend_jit_trace_exit+0x7fb
0x00001000`10000644                          <- side exit out of JIT'd code
php8ts!execute_ex_real / execute_ex
php8ts!zend_call_function
php8ts!zend_std_cast_object_tostring
php8ts!ZEND_CAST_SPEC_CV_HANDLER
php8ts!zend_execute_script
php!php_cli_server_dispatch_script

rax is a heap address whose whole region is unmapped, and zend_func_info_rid is 0, so the faulting read is op_array->reserved[0]ZEND_FUNC_INFO(exit_info->op_array) at zend_jit_ir.c:7986, exactly as described above.

With opcache.jit_debug=28672 the last line before the fault is always a side-trace compile, e.g.

---- TRACE 257 start (side trace 201/36) Symfony\Component\HttpFoundation\Cookie::isHttpOnly() .../Cookie.php:383
---- TRACE 257 stop (return)
<dies here>

where the parent (trace 201, Cookie::__toString()) was compiled during an earlier request.

Workarounds that hold

Both survive 150 requests on 8.5.9 where the default dies at 3:

  • opcache.jit=function
  • opcache.jit_hot_side_exit=0 (keeps root traces, never compiles a side trace)

opcache.jit=disable obviously also works.

Why this may matter more than the IIS report suggests

Any SAPI that keeps one process alive across many requests with a shared OPcache hits this, not just IIS+FastCGI — the built-in server above is single-process and reproduces it in seconds. It also bites embedded/long-lived SAPIs: we hit it in ePHPm (PHP embedded in a Rust application server), where a stock Laravel app killed the whole server process after three requests with no PHP error at all. We have turned the tracing JIT off by default on Windows until #21710 lands.

Happy to test a patched build if that helps.

@luthermonson

Copy link
Copy Markdown
Contributor

Just some Saturday morning vibe coding and didn't know Claude would post all the above. Sorry about that, I'm trying to stay in my repos with all the slop and avoid upstream. Hope it's helpful though as I did double check the results.

iliaal added a commit to iliaal/php-src that referenced this pull request Aug 22, 2026
exit_info.op_array was taken from the current frame. Methods of linked
classes that miss the inheritance cache use a heap copy of the op_array
header, so that pointer is invalid in other processes and later
requests. Store the original from the JIT extension, as root traces
already do.

Closes phpGH-21710
@iliaal
iliaal force-pushed the fix/gh21368-escape-if-undef-runtime-lookup branch from 3079a72 to 3144bbb Compare August 22, 2026 17:36
@iliaal

iliaal commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Done, 3144bbb.

@iliaal iliaal changed the title Fix GH-21368 crash: runtime orig_handler lookup in escape_if_undef JIT: persist the SHM op_array in trace exit_info Aug 22, 2026
exit_info.op_array was taken from the current frame. Methods of linked
classes that miss the inheritance cache use a heap copy of the op_array
header, so that pointer is invalid in other processes and later
requests. Store the original from the JIT extension, as root traces
already do.

Closes phpGH-21710
@iliaal
iliaal force-pushed the fix/gh21368-escape-if-undef-runtime-lookup branch from 3144bbb to c0814c9 Compare August 22, 2026 17:57
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.

7 participants