fix(stack): keep finally-block locals in distinct slots (no reuse)#3235
Merged
Conversation
A block with a finally section runs that section on every exit, including an early return that lexically precedes a later variable's declaration. The interpreter zeroes such locals at block entry, but the stack-reuse optimization (popSp restoring stackTop) could hand a finally-referenced local's slot to an earlier sub-scope, clobbering the zero -- so the finally saw garbage and crashed when it dereferenced a pointer local (e.g. a defer'd close on a resource declared after an early --help/error return). Fix: bump doNotOptimize while inside a block that has a finalList, so its locals keep distinct slots and the block-entry memzero stays valid. Mirrors the AOT path, which already pre-declares every block var as distinct zeroed storage (finallyBeforeBody). Regression test in tests/bare_block; full suite green (10405 passed, 0 failed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes an interpreter-only crash/UB where finally (including defer-lowered finally blocks) could read uninitialized/garbage locals on early exits due to stack slot reuse within a block that has a finalList. The change aligns interpreter stack allocation behavior more closely with the AOT “distinct storage” approach for finally-reachable locals.
Changes:
- Disable scoped stack slot reuse within any
ExprBlockthat has a non-emptyfinalListby incrementing/decrementing the existingdoNotOptimizecounter for that block’s traversal. - Add a regression test that exits early before a later local declaration and asserts the finally observes a zero-initialized value rather than clobbered garbage.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/ast/ast_allocate_stack.cpp |
Prevents stack slot reuse within blocks that have finally sections, preserving block-entry memzero correctness for early exits. |
tests/bare_block/finally_stack_reuse.das |
Regression test ensuring finally-referenced locals declared after an early return remain zeroed (no reuse/clobber). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
Symptom
A
defer'd cleanup crashes when a function takes an earlyreturnbefore the deferred resource is declared. Minimal shape:deferlowers to the block'sfinally, which runs on every block exit — including the earlyreturnabove, which precedesp's declaration.Root cause
The interpreter already zeroes a finally-block's locals at block entry (the memzero in
SimulateVisitor::visit(ExprBlock*), gated onfinalList, with the comment "bad scenario we fight is ( in scope X ; return ; in scope Y )"). But the stack-reuse optimization defeats it:popSp()restoresstackTopon sub-scope exit, so a later finally-referenced local can reuse a slot that an earlier sub-scope on the early-return path writes into — clobbering the zero. The finally then reads garbage (and dereferences it for a pointer local → access violation).AOT never hits this: its
finallyBeforeBodypath pre-declares every block variable as distinct,memset-zeroed storage.Fix
ast_allocate_stack.cpp: bump the existingdoNotOptimizecounter while inside a block that has afinalList(balanced acrosspreVisit/visitofExprBlock). That block's locals then keep distinct slots, so the block-entry memzero stays valid when the finally fires before a local's initializer ran. It's the interpreter analog of AOT's distinct-storage approach. No effect on blocks without afinally.Tests
tests/bare_block/finally_stack_reuse.das— afinallyreferencing a local declared after an earlyreturn; asserts the local reads as0(not clobbered garbage). Fails before the fix, passes after.🤖 Generated with Claude Code