fix: user opcode handlers under PHP 8.6's tail-call VM (macOS arm64) - #281
fix: user opcode handlers under PHP 8.6's tail-call VM (macOS arm64)#281lisachenko wants to merge 5 commits into
Conversation
PHP 8.6 on macOS arm64 (the clang/aarch64 tail-call VM build) corrupts VM state when a user opcode handler re-enters PHP (#280, found via lisachenko/zdebug#24). The probes install a raw zend_set_user_opcode_handler callback - no OpCodeHook, no ExecutionData - and climb from a handler that touches nothing to a per-fire dump of EG(current_execute_data) chaining, vm_stack_top/end and the interrupted frame, with an ADD-without-EXT_STMT baseline. The temporary diagnose-280 workflow runs the ladder on macos-latest (arm64, failing) and macos-15-intel (x64, control), plus an lldb backtrace of the failing shape. All modes pass on linux-x64 8.6 (hybrid VM). Probes and workflow are removed once the fix lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4cKi87mVQ896uSrw592fG
…cktrace The first arm64 run segfaulted in every mode with zero output, so the crash point is unknown (lldb's -k commands produced no backtrace and the nearest-symbol frame zend_class_init_statics is unreliable for the static TAILCALL handlers). Stderr stage markers now bracket the crash (autoload / init / options / installed / payload-first-statement / payload-done / uninstalled), install-only separates handler installation from the first dispatch, the probe loop tolerates crashes so all modes report, and lldb uses -o so bt/registers/disassembly actually print. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4cKi87mVQ896uSrw592fG
The generated ZEND_USER_OPCODE_SPEC_TAILCALL_HANDLER dispatches through the single-step (fastcall) handler table and returns its next-opline result straight up the musttail chain; execute_ex() only refreshes its execute_data local in the ZEND_VM_ENTER_BIT branch, so a plain returned opline resumes execution against the frame execute_ex was ENTERED with. Any user opcode handler firing in a deeper frame (an include, any function call) then executes the following oplines against the wrong frame: wrong run-time cache (calls dispatch to the wrong function), wrong CVs, and eventually SIGSEGV. Hybrid/call VM builds are immune (opline and frame live in global registers / per-op returns), which is why only clang-without-global-regs builds - notably Apple Silicon macOS - are affected. Diagnosed on the arm64 runner via the layered probes in tools/diagnostics/issue-280 (a payload fwrite executed as the outer frame's cached unlink is the smoking gun); this is a php-src bug to be reported upstream. Until php-src resolves it, z-engine fails fast instead of corrupting the debuggee: - Core::vmKind() reports zend_vm_kind() through a dedicated one-symbol FFI binding (usable before init(), no generated-header changes), with the VM_KIND_* constants mirroring Zend/zend_vm_opcodes.h - OpCodeHook::install() throws OpCodeHookException::tailCallVmUnsupported() on VM_KIND_TAILCALL with a message naming the issue - OpCodeHookVmKindGuardTest runs in every CI leg (deliberately not in the internal group): on tail-call builds it asserts the refusal, on every other build the unchanged install/uninstall lifecycle - README/AGENTS document the platform caveat Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4cKi87mVQ896uSrw592fG
…VM bug Reduces the arm64 corruption to ext-ffi against the engine's exported API alone (an ADD user handler returning DISPATCH, fired inside a function frame) - the shape the upstream php-src report needs. Green on hybrid-VM builds (linux 8.5/8.6); the diagnose workflow verifies the crash on the arm64 tail-call build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4cKi87mVQ896uSrw592fG
The arm64 verification is done (probe run 33256998594, pure-FFI repro run 33257997984): the corruption is php-src's tail-call VM bug and the OpCodeHook guard covers consumers. Keep the probes and the workflow as the upstream repro harness, but stop running them on every pull request - a manual dispatch against a new PHP build answers 'is it fixed yet'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4cKi87mVQ896uSrw592fG
|
@claude Look for the relevant bug report for PHP at https://github.com/php/php-src/ (issues + PRs) and link it here if you will find anything relevant |
|
Searched php/php-src issues and PRs (queries: Closest related upstream items (same bug class — the tail-call VM returning a next-opline/tagged value that the caller mishandles — but all about
Those two merged fixes show the pattern is a known weak spot, which should make the upstream report easy to triage — it can reference them, the pure-FFI repro ( Generated by Claude Code |
What
Fixes #280 on z-engine's side: on PHP 8.6 builds using the new tail-call VM (
ZEND_VM_KIND_TAILCALL— clang without global-register support, notably Apple Silicon macOS), user opcode handlers mis-resume execution inside the engine and corrupt the process. The diagnostics on this branch reduced it to a php-src bug (pure ext-ffi repro, no z-engine code — see the upstream-ready report on #280): the generatedZEND_USER_OPCODE_SPEC_TAILCALL_HANDLERreturns the single-step dispatch result up the musttail chain andexecute_ex()resumes it against its stale entry frame.Until php-src resolves it, z-engine fails fast instead of corrupting the debuggee:
Core::vmKind()— reportszend_vm_kind()through a dedicated one-symbol FFI binding (usable beforeinit(), no generated-header changes), withVM_KIND_*constants mirroringZend/zend_vm_opcodes.hOpCodeHook::install()throwsOpCodeHookException::tailCallVmUnsupported()onVM_KIND_TAILCALL, naming the issueOpCodeHookVmKindGuardTestruns in every CI leg (deliberately not in theinternalgroup — the existing opcode-hook tests are internal-only, which is why the macOS legs never caught this): on tail-call builds it asserts the refusal, elsewhere the unchanged install/uninstall lifecycleDiagnostics kept as the upstream repro harness
tools/diagnostics/issue-280/(probe ladder +pure-ffi-repro.php) and the now dispatch-onlyDiagnose issue 280workflow stay in-tree: re-dispatching it against a newer PHP build answers "is the php-src bug fixed yet"; both go away with the guard once upstream resolves it.Verification
vm_kind=5, SIGSEGV) proving the underlying bug is engine-level — run 33257997984Downstream: lisachenko/zdebug#24 keeps its macOS arm64 + 8.6 leg experimental until upstream is fixed; with this guard, consumers get a clear
OpCodeHookExceptioninstead of corrupted debuggees.🤖 Generated with Claude Code
https://claude.ai/code/session_01U4cKi87mVQ896uSrw592fG