fix: phase lifecycle safety bugs (nested phase + realloc UB)#10
Merged
Conversation
…o prevent UB on aliased dst
b4f5280 to
23004b5
Compare
TomWambsgans
added a commit
to leanEthereum/leanMultisig
that referenced
this pull request
May 9, 2026
…c#10) - realloc: switch copy_nonoverlapping -> copy. When a Vec grows inside the arena, the freshly-bumped destination can overlap the source within the same slab, violating copy_nonoverlapping's precondition (UB). - begin_phase: document that phases must not nest. A nested begin_phase recycles the slab and overwrites the outer phase's still-live data. Refcounting (per PR #10) papers over the issue but inner allocations would silently outlive their inner end_phase, so we make the flat-phase contract explicit instead. Co-authored-by: Barnadrot <kbarna.drot@gmail.com>
TomWambsgans
added a commit
to leanEthereum/leanMultisig
that referenced
this pull request
May 9, 2026
) - realloc: switch copy_nonoverlapping -> copy. When a Vec grows inside the arena, the freshly-bumped destination can overlap the source within the same slab, violating copy_nonoverlapping's precondition (UB). - begin_phase: panic if a phase is already active. Phases must stay flat in a bump allocator: a nested begin would recycle the slab and overwrite the outer phase's still-live data. Rather than refcounting (per PR #10, which silently lets inner allocations outlive their inner end_phase), we make the violation loud via an atomic swap + assert. Co-authored-by: Barnadrot <kbarna.drot@gmail.com>
TomWambsgans
added a commit
to leanEthereum/leanMultisig
that referenced
this pull request
May 9, 2026
) - realloc: switch copy_nonoverlapping -> copy. When a Vec grows inside the arena, the freshly-bumped destination can overlap the source within the same slab, violating copy_nonoverlapping's precondition (UB). - begin_phase: panic if a phase is already active. Phases must stay flat in a bump allocator: a nested begin would recycle the slab and overwrite the outer phase's still-live data. Rather than refcounting (per PR #10), we make the violation impossible via an assert. Co-authored-by: Barnadrot <kbarna.drot@gmail.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
Two phase lifecycle bugs found by automated bug hunter (boundary-value testing + hypothesis-driven code audit). Both are memory-safety issues in the arena's phase management — one causes silent data corruption, the other is undefined behavior per the Rust spec.
Depends on #9 (size-routing infrastructure provides
PHASE_DEPTHcounter andPhaseGuard).Bug 1: Nested
begin_phase()Silently Corrupts Outer Phase Data (high)Root cause:
begin_phase()unconditionally bumpedGENERATIONand recycled every thread's slab. When called inside an already-active phase, the innerbegin_phase()reset the slab — silently overwriting all bump-allocated data from the outer phase.Characterization:
0xAAbegin_phase()+ allocation overwrites the slab from offset 0test_phase_guard::nested_phase_guards_composemissed this because it allocated nothing in the outer phaseWhy it matters: Today leanMultisig calls
begin_phase()/end_phase()at the top-level prove boundary, so nesting doesn't occur. But the API permits it, and nothing warns the caller. As zk-alloc integrates deeper — library wrappers,PhaseGuardin helper functions, test scaffolding — a nested call silently corrupts with no diagnostic. A public allocator API that silently destroys live data on a legal call sequence is a latent safety hole.Fix:
PHASE_DEPTHatomic counter.begin_phase()increments depth; only the outermost transition (0 → 1) bumpsGENERATIONand activates the arena.end_phase()decrements; only the outermost transition (1 → 0) deactivates. Nested calls compose safely as no-ops.Bug 2:
reallocUsescopy_nonoverlappingon Potentially Aliased Memory (medium)Root cause:
GlobalAlloc::realloccalledptr::copy_nonoverlapping(old_ptr, new_ptr, ...)to move data during growth. When growing within the same slab (bump pointer advanced past old allocation, then old allocation is reallocated to a larger size at the same base),old_ptrandnew_ptrcan alias — the new allocation overlaps the source.copy_nonoverlappingis UB on overlapping regions per the Rust spec.Characterization:
0xBBreallocto 256 bytes — new pointer returned at same base address (bump allocator reuses position)copy_nonoverlappingwith overlapping src/dst — UB, Miri would flagFix: Replace
ptr::copy_nonoverlappingwithptr::copy(memmove semantics). Handles overlapping regions correctly. Zero performance impact — realloc is not on the hot path.Findings Summary
begin_phaserecycles outer slabcopy_nonoverlappingon aliased src/dstARENA_ACTIVEOVERFLOW_COUNTskipped on first no-slab allocOVERFLOW_COUNTcounts failed OOM as overflowValidation
cargo test --release— all tests pass including 2 new regression testsend_phase()calls are no-ops (saturate at zero depth)Test plan
cargo test --release— all existing + new tests passcargo +nightly miri test— verify hunt-2 fix eliminates UB under MiriGenerated with Claude Code