Skip to content

sanitize: clear the freed-block poison when a block is reallocated - #96

Closed
codewiz wants to merge 1 commit into
sidick:mainfrom
codewiz:fix/recycled-alloc-shadow
Closed

codewiz wants to merge 1 commit into
sidick:mainfrom
codewiz:fix/recycled-alloc-shadow

Conversation

@codewiz

@codewiz codewiz commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #95.

A block that FreeVec poisoned keeps its PoisonReason::Freed marking when the heap hands the same addresses back out. Two kinds of write then get reported as use-after-free: the allocating handler's own header write or zeroing, and afterwards every write the guest makes into its fresh buffer. Writing over the block does not clear the marking -- check_write_byte() heals Uninit bytes but records and keeps Unaddressable ones -- so several handlers' "these writes heal the shadow bytes, so poison afterwards" comments hold only for blocks that were never freed.

Changes

  • New AddressSpace::clear_fresh_block(), a no-op by default and implemented by FlatMemory to clear the shadow over the block. A trait method rather than a HandlerContext helper so that create_io_request(), which only holds a &mut dyn AddressSpace, can call it too.
  • Called immediately after each successful allocation, before the handler writes anything: AllocMem, AllocVec, AllocPooled, CreatePool, AllocateTagItems, AllocDosObject, CreateMsgPort, CreateIORequest.
  • alloc_vec_handler() additionally marks [user_ptr, user_ptr + user_rounded) uninit (valid with MEMF_CLEAR). user_ptr is the address that excludes the FreeVec header, which is what the existing false argument was protecting; as a side effect AllocVec allocations now carry uninitialized state for --sanitize-uninit, where before they were marked as nothing at all.
  • Regression test a_recycled_alloc_vec_block_is_no_longer_a_freed_block: with the quarantine budget set to 0 so the block really is recycled, allocate, free, allocate the same size, write one byte, and expect no violations. Verified that it fails with the fix removed.

Other allocation sites (dosargs.rs, dosseg.rs, intuition.rs, dosdevproc.rs) write into fresh blocks without any shadow marking at all; they would want the same one-line call if their blocks can ever be recycled, but I have left them alone since nothing observed reaches them.

Verification

Both cases from #95, BenchWork under --sanitize --cpu 68020:

before after
backdrop lha-pack ~6 min, 691416 violations 10 s, 0
lha-pack zlib-inflate 2 violations (the AllocVec header pair) 0

Benchmark checksums are unchanged throughout (4e91d2c2, 3f1b1c70, 72e7a604), and backdrop lha-pack zlib-deflate png-encode is also clean.

cargo test --release: 860 passed, 1 failed. The failure is exectask::tests::pending_host_break_folds_into_wait, which fails the same way on an unpatched tree and picks a different member of that family from run to run, so it looks like a race on the pending-break state rather than anything here.

🤖 Generated with Claude Code

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 sidick#95

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sidick

sidick commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Closing in favour of #97, which contains this commit unchanged plus the rest of the fix. Thanks for the diagnosis here -- it was right, and the AllocVec shadow-marking part of it carried over as-is.

Two reasons for continuing it there rather than merging this first:

The clear reached 8 of ~48 allocation sites, and the gap is reachable. The note about the other modules ("nothing observed reaches them") turns out not to hold. A guest that frees a block bigger than the 64 KiB quarantine budget -- released immediately rather than held back -- and then opens a file reproduces #95's symptom against the FileHandle/MsgPort that dos Open carves out. AllocVec(100000) -> FreeVec -> Open("T:x", MODE_NEWFILE) -> Write gives 46 spurious "invalid write (freed block)" reports on this branch, the same as on main. Since ~40 sites across dosfile, doslock, dosargs, dosseg, dosanchor, bsdsocket, intuition and more all draw on the same heap, #97 moves the clear into GuestHeap::alloc/alloc_with_requested and has them take &mut dyn AddressSpace -- so a new allocation site cannot compile without it, rather than relying on each one remembering.

The wall-clock half of #95 has a different cause than the issue guessed. Not the dedup hash: that measures as noise (I implemented the pre-screen and got 1.31s -> 1.32s). check_read/check_write detected "did that byte report?" by comparing violation_count() before and after each byte, and violation_count sums hits across the whole log -- so once the log filled, every byte of every violating access walked 1000 entries twice. Returning the answer directly takes a 400,000-byte use-after-free loop from 1.33s to 0.03s, output byte-identical.

Also fixed there: cargo fmt --all --check rejected the new test, and a clippy::collapsible_if in the reworked AllocVec block -- both -D warnings/--check CI failures.

@sidick sidick closed this Sep 28, 2026
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