Skip to content

Fix GH-17990: Tracing JIT uses the SSA of a dead process - #25399

Open
kamil-tekiela wants to merge 1 commit into
php:PHP-8.4from
kamil-tekiela:fix-gh17990-trace-jit-stale-ssa
Open

kamil-tekiela wants to merge 1 commit into
php:PHP-8.4from
kamil-tekiela:fix-gh17990-trace-jit-stale-ssa

Conversation

@kamil-tekiela

@kamil-tekiela kamil-tekiela commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

The tracing JIT stores the SSA of each function of a trace in the func_info of the op_array, which is in shared memory. The SSA itself is in the arena of the compiling process, and zend_jit_trace() resets the func_info when the compilation ends.

A process that dies during the compilation leaves the func_info behind. The next process that compiles a trace through the same function finds a non-zero ssa->cfg.blocks_count and reads the pointers of the dead process.

Reuse an SSA only if the current compilation built it, and block interruptions during the compilation, as the rest of opcache does while it changes shared memory.

Fixes #17990 Can you check if this fixes the problem? @YuanchengJiang

The tracing JIT stores the SSA of each function of a trace in the
func_info of the op_array, which is in shared memory. The SSA itself is
in the arena of the compiling process, and zend_jit_trace() resets the
func_info when the compilation ends.

A process that dies during the compilation leaves the func_info behind.
The next process that compiles a trace through the same function finds
a non-zero ssa->cfg.blocks_count and reads the pointers of the dead
process.

Reuse an SSA only if the current compilation built it, and block
interruptions during the compilation, as the rest of opcache does while
it changes shared memory.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>

@iliaal iliaal left a comment

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.

LGTM. The zend_jit_trace_has_ssa() check is what fixes this: on its own it makes both your SIGKILL test and a SIGTERM variant pass, while the interruption blocking on its own only fixes SIGTERM. It's worth saying that in the commit message.

Not for this PR, but both compile functions install the new code (opline->handler = handler, zend_jit_link_side_trace()) before ZEND_JIT_TRACE_NUM++, so a SIGKILL between those stores leaves live code pointing at a trace slot the next compilation reuses. The window is only a few stores.

zend_jit_trace_exit_info exit_info[ZEND_JIT_TRACE_MAX_EXITS];
bool do_bailout = 0;

/* Don't let a signal kill the process while func_info in SHM refers to our arena */

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.

This only defers the signals zend_signal handles (SIGTERM, SIGINT, SIGHUP...). SIGKILL, a segfault or the OOM killer still end the process here, and that is the case your test covers. Maybe /* Defer signals while func_info in SHM refers to our arena */?

zend_jit_trace_exit_info exit_info[ZEND_JIT_TRACE_MAX_EXITS];
bool do_bailout = 0;

/* Don't let a signal kill the process while func_info in SHM refers to our arena */

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.

Same as above.

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.

2 participants