fix: user opcode handlers under PHP 8.6's tail-call VM (macOS arm64) - #281
Draft
lisachenko wants to merge 5 commits into
Draft
fix: user opcode handlers under PHP 8.6's tail-call VM (macOS arm64)#281lisachenko wants to merge 5 commits into
lisachenko 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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