Repository navigation
sanitize: clear recycled-block poison in the heap, not per handler - #97
Merged
Merged
Conversation
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>
This was referenced Sep 28, 2026
This was referenced Sep 28, 2026
Merged
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>
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.
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
AllocVecshadow-marking fix are right. Two things were left over.1. The clear reached 8 of ~48 allocation sites
AllocMem/AllocVec/AllocPooledare not the only things that carve guest structures out ofGuestHeap--dosfile,doslock,dosargs,dosseg,dosanchor,bsdsocket,intuition,graphics,locale,dosdevlist,exectask,execlibanddispatchall do, ~48 sites in total, and any of them can be handed a block thatFreeMem/FreeVec/FreePooledpoisoned.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/MsgPortthat dosOpencarves out. WithAllocVec(100000)->FreeVec->Open("T:x", MODE_NEWFILE)->Write, on #96's branch and onmainalike:Those are the
Openhandler's own initializing writes, attributed to the guest's PC. Worse than the reports themselves, the block staysUnaddressablewhile live, so later guest reads of thatFileHandlereport 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 viapoison_allocation_edgesafterwards, which they already had to do after their own initializing writes.The per-handler
clear_fresh_blockcalls 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-privatealloc_bareshorthand, 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 theMAX_VIOLATIONScap. It measures as noise. The real cost is incheck_read/check_write, which detected "did that byte report?" by comparingviolation_count()before and after each byte -- andviolation_countsumshitsacross 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:
(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
cargo fmt --all --checkrejected sanitize: clear the freed-block poison when a block is reallocated #96's new test, which would have failed CI (ci.yml:29). Fixed.clippy::collapsible_ifin the reworkedAllocVecblock, likewise a-D warningsCI failure. Fixed.Tests
guestmem::tests::alloc_clears_a_recycled_blocks_freed_poison-- the fix where it lives, against a realFlatMemory.execmem::tests::a_genuine_use_after_free_is_still_reported-- the guard rail.allocnow clears whole blocks, so it matters that a block which has been freed and not reallocated stays poisoned, or--sanitizewould have quietly stopped detecting use-after-free.a_recycled_alloc_vec_block_is_no_longer_a_freed_blockkept (verified it still fails with the fix stubbed out).Verification
cargo test --release: 1010 passed, 0 failed.cargo clippy --all-targets -- -D warningsandcargo fmt --all --checkclean.Sanitizer output byte-identical to
mainon real workloads -- PhxAss under--sanitizeand--sanitize-uninit, with and without--cpu 68020, both usage output and a real assemble; a real SAS/Csc 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
mainwith the repo's own fixtures:--ramabove 16 MiB breaks any program that reaches an exec.library continuation.fixtures/memtestdoes the same.fixtures/hello(dos only, no continuation) is fine at 64M. Worth its own issue.🤖 Generated with Claude Code