Skip to content

sanitize: clear recycled-block poison in the heap, not per handler - #97

Merged
sidick merged 2 commits into
mainfrom
fix/recycled-alloc-shadow-full
Sep 28, 2026
Merged

sidick merged 2 commits into
mainfrom
fix/recycled-alloc-shadow-full

Conversation

@sidick

@sidick sidick commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Fixes #95. Builds on #96 (@codewiz's commit is the first of the two here, unchanged) and finishes the job.

#96's diagnosis and its AllocVec shadow-marking fix are right. Two things were left over.

1. The clear reached 8 of ~48 allocation sites

AllocMem/AllocVec/AllocPooled are not the only things that carve guest structures out of GuestHeap -- dosfile, doslock, dosargs, dosseg, dosanchor, bsdsocket, intuition, graphics, locale, dosdevlist, exectask, execlib and dispatch all do, ~48 sites in total, and any of them can be handed a block that FreeMem/FreeVec/FreePooled poisoned.

A guest that frees a block larger than the 64 KiB quarantine budget (so it is released immediately rather than held back) and then opens a file reproduces #95's symptom exactly, against the FileHandle/MsgPort that dos Open carves out. With AllocVec(100000) -> FreeVec -> Open("T:x", MODE_NEWFILE) -> Write, on #96's branch and on main alike:

sanitizer: 1 site(s), 46 violation(s):
  PC 0x000007e2: 46 invalid 1-byte writes (freed block), addresses 0x00002c18-0x00002c43

Those are the Open handler's own initializing writes, attributed to the guest's PC. Worse than the reports themselves, the block stays Unaddressable while live, so later guest reads of that FileHandle report too.

Patching the other 40 sites by hand would work until the next one is added. So the clear moves to GuestHeap::alloc/alloc_with_requested -- the one point where a block leaves the heap's control -- and they now take &mut dyn AddressSpace, which turns "remember to clear the shadow map" from a convention into a compile error. The whole block is cleared, redzones included (nothing about the previous tenant applies to these addresses any more); callers wanting redzone/alignment-slack protection re-poison via poison_allocation_edges afterwards, which they already had to do after their own initializing writes.

The per-handler clear_fresh_block calls from #96 are gone, as is the need for handlers to know about any of this. GuestHeap's own placement/coalescing/quarantine tests keep a module-private alloc_bare shorthand, since they have no address space in play.

2. The wall-clock half of #95 was not the dedup hash

#95 guessed the slowdown was the (pc, addr, kind) hash past the MAX_VIOLATIONS cap. It measures as noise. The real cost is in check_read/check_write, which detected "did that byte report?" by comparing violation_count() before and after each byte -- and violation_count sums hits across the whole log. Once the log filled, every byte of every violating access walked 1000 entries twice. The byte checkers now return that answer directly.

On a 400,000-byte use-after-free loop, output byte-identical:

before after
wall clock 1.33 s 0.03 s

(I did implement the Bloom-filter pre-screen #95 suggests, measured it at 1.31 s -> 1.32 s, and dropped it rather than carry dead complexity.)

3. Smaller

Tests

  • guestmem::tests::alloc_clears_a_recycled_blocks_freed_poison -- the fix where it lives, against a real FlatMemory.
  • execmem::tests::a_genuine_use_after_free_is_still_reported -- the guard rail. alloc now clears whole blocks, so it matters that a block which has been freed and not reallocated stays poisoned, or --sanitize would have quietly stopped detecting use-after-free.
  • sanitize: clear the freed-block poison when a block is reallocated #96's a_recycled_alloc_vec_block_is_no_longer_a_freed_block kept (verified it still fails with the fix stubbed out).

Verification

cargo test --release: 1010 passed, 0 failed. cargo clippy --all-targets -- -D warnings and cargo fmt --all --check clean.

Sanitizer output byte-identical to main on real workloads -- PhxAss under --sanitize and --sanitize-uninit, with and without --cpu 68020, both usage output and a real assemble; a real SAS/C sc hello.c; and all 12 fixtures under --sanitize.

Clean where they were not before: both #95 repro cases, and the Open-after-free case above.

Unrelated bug found while testing

Not touched here, reproduces on main with the repo's own fixtures: --ram above 16 MiB breaks any program that reaches an exec.library continuation.

$ volamos --ram 17M fixtures/exectest
volamos: fixtures/exectest: continuation stub trapped at 0x000000c4 with no pending
continuation (guest jumped to the trampoline stub directly, or ContinuationStack
bookkeeping is out of sync)
$ volamos --ram 16M fixtures/exectest
exec ok

fixtures/memtest does the same. fixtures/hello (dos only, no continuation) is fine at 64M. Worth its own issue.

🤖 Generated with Claude Code

codewiz and others added 2 commits September 28, 2026 07:53
A block that FreeVec poisoned keeps that marking when the heap hands the
same addresses back out, so the allocating handler's own header write or
zeroing, and then every write the guest makes, are all reported as
use-after-free.  Writing over the block does not clear it either:
check_write_byte heals Uninit bytes but records and keeps Unaddressable
ones.

Clear the shadow where the block is handed out, before the handler writes
anything into it.  AddressSpace::clear_fresh_block does it, so the
allocators that only hold a &mut dyn AddressSpace can call it too.

BenchWork's "backdrop lha-pack" under --sanitize went from 6 minutes and
691416 violations to 10 seconds and none.

Fixes #95

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the #95 fix. Two things were left over.

**The clear reached only 8 of ~48 allocation sites.** Every one of them
carves guest structures out of the same GuestHeap, so any of them can be
handed a block FreeMem/FreeVec/FreePooled poisoned -- not just the exec
allocators. A guest that frees a block larger than the 64 KiB quarantine
budget (released immediately rather than held back) and then opens a
file reproduces the original symptom against the FileHandle/MsgPort that
dos Open carves out: 46 spurious "invalid write (freed block)" reports,
one per byte, attributed to the guest's PC.

So the clear moves to GuestHeap::alloc/alloc_with_requested, the one
point where a block leaves the heap's control, and they now take the
address space as a parameter -- which makes the invariant a compile
error to forget rather than a convention to remember. The whole block is
cleared, redzones included; callers that want redzone protection
re-poison via poison_allocation_edges afterwards, as they already had to.
The allocator's own placement/coalescing tests keep a module-private
`alloc_bare` shorthand, since they have no address space in play.

**The wall-clock half of #95 was not the dedup hash.** check_read and
check_write detected "did that byte report?" by comparing
violation_count() before and after -- and violation_count *sums* hits
across the whole log, so once the log filled, every byte of every
violating access walked 1000 entries twice. The byte checkers now return
that answer directly. On a 400,000-byte use-after-free loop: 1.33s
before, 0.03s after, with byte-identical output.

Verified: the #95 repro pair, the Open-after-free case above, and the
whole fixture corpus are clean; sanitizer output is byte-identical to
main for PhxAss (--sanitize, --sanitize-uninit, with and without
--cpu 68020) and for a real SAS/C `sc hello.c`; a genuine use-after-free
is still reported (new regression test, alongside one for the recycled
block at the heap level). cargo test 1010 passed, clippy and fmt clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sidick
sidick merged commit f779356 into main Sep 28, 2026
9 checks passed
@sidick
sidick deleted the fix/recycled-alloc-shadow-full branch September 28, 2026 05:42
sidick added a commit that referenced this pull request Sep 28, 2026
Opens the 0.8 changelog section with everything merged since v0.7, and
moves the #95/#97 and #98 entries into it -- both were written into the
already-released 0.7 section by mistake.

New in 0.8: the --sanitize recycled-block false positives (#95/#97), the
--ram-versus-CPU-address-bus check (#98/#99), pr_Arguments on the fake
Process (#89), AllocDosObject's DOS_FIB (#90), SetVar on a missing
variable (#91), and OpenLibrary's CMP.W version comparison (#92). The
three m68k crate bumps (#88/#93/#94) get no entry, as dependency updates
never have.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

--sanitize: recycled heap block keeps its freed-block poison, so a fresh AllocVec reports use-after-free and runs ~75x slower

2 participants