Fix debugger segfault: VmFrame::ctxChain must not relocate its elements - #129
Merged
Conversation
Setting a breakpoint inside a module and clicking Continue crashed
instantly with SIGSEGV at address 0x10, in buildDebugFrame.
Evaluator::enterUserCall stores CallStackFrame::bodyCtx as a pointer to
frame->ctxChain.back() (pushBracketedCallFrame/pushBracketedModuleFrame).
That pointer is read much later, by any checkDebug() that walks the whole
call stack to build per-frame debugger locals. ctxChain was a
std::vector, so the next push onto it reallocated and left bodyCtx
pointing at the moved-from element -- whose let_ shared_ptr is null,
hence the read at offset 0x10 from null.
The push that does it is Op::PushBuiltinWrap: every transform inside a
compiled body pushes a context. So the trigger is as ordinary as
`module mid(a) { translate([a,0,0]) inner(a); }`.
It only bites while debugging, because a chunk is compiled at all only
when fastContinueBreakpoints_ is set (chunkEligibleNow) -- which is
exactly the "breakpoint set, now continue" state. A breakpointed callee
is forced to interpret while its caller stays compiled, and pausing in
the callee walks the caller's now-stale frame.
std::deque never invalidates references to existing elements on
push_back/pop_back, so the fix is the container. lastCtx_ and
childrenCallerCtx point into the same storage and are covered by it too.
No measurable cost: 8 runs of eval_perf_benchmark (50,000 translate()
calls, the same push path) give min 432ms vs 426ms for vector, medians
451ms vs 467ms -- within noise both ways.
Regression test drives the user's exact scenario: a breakpoint that
forces the callee to interpret while its caller runs compiled, then
reads the caller's locals from the debug frames. It segfaults without
the fix.
Co-Authored-By: Claude Opus 5 (1M context) <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.
The crash
Setting a breakpoint inside a module and clicking Continue crashed instantly:
Root cause
Evaluator::enterUserCallstoresCallStackFrame::bodyCtxas a pointer toframe->ctxChain.back(). That pointer is read much later, by anycheckDebug()that walks the whole call stack to build per-frame debugger locals.ctxChainwas astd::vector<EvalContext>, so the nextpush_backreallocated and leftbodyCtxpointing at the moved-from element — whoselet_shared_ptris null.ctx->let_->items()then reads offset 0x10 from null.buildDebugFramenull-checksctxbut notctx->let_.The push that does it is
Op::PushBuiltinWrap— every transform inside a compiled body. So the trigger is as ordinary as:module mid(a) { translate([a,0,0]) inner(a); }It only bites while debugging because a chunk compiles at all only when
fastContinueBreakpoints_is set (chunkEligibleNow) — exactly the "breakpoint set, now continue" state. The breakpointed callee is forced to interpret while its caller stays compiled, and pausing in the callee walks the caller's now-stale frame. The doc comment atbytecode_vm.cpp:276describes that scenario for an earlier fix of the same class; this is the hole that fix left.Fix
std::deque<EvalContext>— never invalidates references to existing elements onpush_back/pop_back.lastCtx_andchildrenCallerCtxpoint into the same storage and are covered by the same change.Cost
None measurable. 8 runs of
eval_perf_benchmark(50,000translate()calls — the same push path):Within noise both ways.
Verification
OSCAD_BYTECODE_VM=1and=0.🤖 Generated with Claude Code