Fix dangling CallStackFrame::bodyCtx for compiled function/module calls - #62
Merged
Merged
Conversation
Real crash, found via BelfrySCAD's own CI: pausing a debug session
deep inside a native (interpreter-forced, e.g. by an active breakpoint
on its own line) function call, with a COMPILED caller above it on the
call stack, segfaulted the instant anything tried to read that
caller's own frame locals (Evaluator::buildDebugFrames, walked by
DebugFramesFn/getFrame() -- exactly what a debugger's "step over a
call, but still honor a breakpoint set inside it" scenario needs).
Root cause: pushBracketedCallFrame/pushBracketedModuleFrame (the
explicit-stack VM's own compiled-call push helpers, bytecode_vm.cpp)
called enterUserCall with a reference to their OWN local `childCtx`
variable. enterUserCall stores that reference as
CallStackFrame::bodyCtx for later use ("per-frame locals for the
debugger") -- but the real, long-lived copy of that context ends up
living in the pushed VmFrame's own ctxChain (heap-allocated, owned by
vmCallStack_), not at the address bodyCtx pointed to. The instant
either push helper returned, its own local childCtx was destroyed,
leaving bodyCtx dangling for the ENTIRE lifetime of that call -- never
noticed until something actually reads a compiled caller's own frame
locals while a DEEPER (possibly native) call is still active on the
stack.
Confirmed present since the explicit-stack VM redesign itself (PR #59)
-- bisected by rebuilding at each of PR #59/#60/#61's own commits and
reinstalling into BelfrySCAD's venv, all three reproduce it. Root-
caused via a targeted AddressSanitizer build of the Python bindings
(a plain synchronous C++ probe didn't reproduce it -- needed the real
cross-thread blocking a GUI debug session's own hook does) plus a
threaded Python repro mirroring BelfrySCAD's debugger.py exactly.
Fix: reorder both push helpers to build the VmFrame FIRST (so its own
ctxChain owns the context), then call enterUserCall with a reference
to frame->ctxChain.back() -- stable for the VmFrame's whole lifetime,
since VmFrame lives behind a unique_ptr and never moves even if
vmCallStack_ itself reallocates.
New regression test (DebugHooks.
GetFrameDoesNotCrashWalkingACompiledCallerAboveAForcedInterpretedCallee)
reproduces the exact shape and crashes the test process without this
fix (verified via git stash before landing this commit). Full 709-test
suite green both OSCAD_BYTECODE_VM states. Verified against
BelfrySCAD's own previously-crashing test (both directly and via its
full 306-test suite) using an editable install of this fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
Summary
Real segfault, discovered via BelfrySCAD's own CI (v0.13.0 dependency-bump PR): pausing a debug session deep inside a call forced into the interpreter (e.g. by an active breakpoint on its own line), with a compiled caller above it on the call stack, crashes the instant anything reads that caller's own frame locals (
getFrame()/Evaluator::buildDebugFrames) — exactly what a "step over a call, but still honor a breakpoint set inside it" debug scenario needs.Root cause:
pushBracketedCallFrame/pushBracketedModuleFrame(the explicit-stack VM's compiled-call push helpers) pointedCallStackFrame::bodyCtxat their own localchildCtx, which is destroyed the instant they return — but the real, long-lived context lives in the pushedVmFrame's ownctxChain(heap-allocated), not at that address.bodyCtxwas dangling for the entire lifetime of every compiled call, only surfacing when something actually reads a compiled caller's frame locals while a deeper call is still active.Confirmed present since the explicit-stack VM redesign itself (PR #59) — bisected by rebuilding at each of PR #59/#60/#61's own commits and reinstalling into BelfrySCAD's venv; all three reproduce it. Root-caused via a targeted AddressSanitizer build of the Python bindings (a synchronous C++-only probe didn't reproduce it — needed the real cross-thread blocking a GUI debug session's hook does) plus a threaded Python repro mirroring BelfrySCAD's
debugger.pyexactly.Fix: reorder both push helpers to build the
VmFramefirst (so its ownctxChainowns the context), then callenterUserCallwith a reference toframe->ctxChain.back()— stable for the frame's whole lifetime.Test plan
DebugHooks.GetFrameDoesNotCrashWalkingACompiledCallerAboveAForcedInterpretedCallee) — verified it crashes the test process without this fix (git stashbefore landing)OSCAD_BYTECODE_VMstates🤖 Generated with Claude Code