Skip to content

Fix dangling CallStackFrame::bodyCtx for compiled function/module calls - #62

Merged
revarbat merged 1 commit into
mainfrom
fix-dangling-bodyctx-in-compiled-call-push
Aug 1, 2026
Merged

Fix dangling CallStackFrame::bodyCtx for compiled function/module calls#62
revarbat merged 1 commit into
mainfrom
fix-dangling-bodyctx-in-compiled-call-push

Conversation

@revarbat

@revarbat revarbat commented Aug 1, 2026

Copy link
Copy Markdown
Member

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) pointed CallStackFrame::bodyCtx at their own local childCtx, which is destroyed the instant they return — but the real, long-lived context lives in the pushed VmFrame's own ctxChain (heap-allocated), not at that address. bodyCtx was 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.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 frame's whole lifetime.

Test plan

  • New regression test (DebugHooks.GetFrameDoesNotCrashWalkingACompiledCallerAboveAForcedInterpretedCallee) — verified it crashes the test process without this fix (git stash before landing)
  • Full 709-test suite green both OSCAD_BYTECODE_VM states
  • Verified against BelfrySCAD's own previously-crashing test directly, and its full 306-test suite, via an editable install of this fix

🤖 Generated with Claude Code

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>
@revarbat
revarbat merged commit 19bacbb into main Aug 1, 2026
3 checks passed
@revarbat
revarbat deleted the fix-dangling-bodyctx-in-compiled-call-push branch August 1, 2026 00:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant