Skip to content

Fix GH-21368 crash: runtime orig_handler lookup in escape_if_undef#21710

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

Fix GH-21368 crash: runtime orig_handler lookup in escape_if_undef#21710
iliaal wants to merge 1 commit intophp:PHP-8.4from
iliaal:fix/gh21368-escape-if-undef-runtime-lookup

Conversation

@iliaal
Copy link
Copy Markdown
Contributor

@iliaal iliaal commented Apr 10, 2026

Follow-up to #21368.

@vibbow reported an access violation in zend_jit_escape_if_undef on PHP 8.5.5 VS17 x64 NTS (Windows + IIS + FastCGI), crashing at the fix I introduced in #21368:

FAILURE_BUCKET_ID:  INVALID_POINTER_READ_c0000005_php8.dll!zend_jit_escape_if_undef
FAULTING_SOURCE_LINE_NUMBER:  7980

The crashing instruction is mov rcx, [rax+rcx*8+0xD0], where rax is a heap address and the byte at +0xD0 is unmapped. On 64-bit NTS that offset matches zend_op_array->reserved[zend_func_info_rid], which is exactly what ZEND_FUNC_INFO(op_array) expands to in my fix.

Root cause

#21368 changed zend_jit_escape_if_undef to dispatch directly to orig_handler, computed at JIT compile time from exit_info->op_array. That pointer is unreliable:

  • zend_jit_trace_get_exit_point sets op_array = NULL at ext/opcache/jit/zend_jit_trace.c:164 when JIT_G(current_frame) is NULL at exit-point creation.
  • Even when non-NULL, the op_array can be freed between exit-point creation and side-trace compilation. The crash's read address at +0xD0 lands in released memory, which fits a freed/reused op_array.

Either way, ZEND_FUNC_INFO(op_array) dereferences a bad pointer.

Fix

Use the existing zend_jit_orig_opline_handler(jit) runtime helper instead. It emits IR that loads EX(func)->reserved[rid]->offset at dispatch time, adds it to the current IP, and loads orig_handler. EX(func) is the currently-executing frame's function, so no compile-time op_array is needed. zend_jit_trace_exit_stub already uses this exact pattern at zend_jit_ir.c:2525.

I dropped the op_array parameter from zend_jit_escape_if_undef and updated the call site in zend_jit_trace_deoptimization.

Verification

gh21267.phpt and gh21267_blacklist.phpt both still pass, confirming the original infinite-loop fix is preserved.

Reproducer

I tried to build a native Linux reproducer and couldn't trigger the NULL/stale op_array path through synthetic stress, opcache invalidation, or trace-buffer manipulation. The race looks specific to long-running FastCGI workers on Windows where opcache eviction crosses with side-trace compilation. The crash dump analysis is unambiguous and the runtime-lookup fix eliminates the class of issue.

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

1 participant