Poison the other allocators that carve from the guest heap - #84
Conversation
`--sanitize`'s heap detectors only ever poisoned AllocMem, AllocVec and
AllocPooled. But three other guest-visible APIs allocate from the very
same GuestHeap and hand the guest a block it can overrun:
utility.library AllocateTagItems / FreeTagItems
dos.library AllocDosObject / FreeDosObject
exec.library CreateIORequest / DeleteIORequest
CreateMsgPort / DeleteMsgPort
Because they go through GuestHeap::alloc, redzone *space* was already
being reserved for them whenever --sanitize was on -- the guard bytes
existed, nothing marked them poisoned. So a guest overrunning a
FileInfoBlock, an RDArgs, a MsgPort or a TagItem array that volamos
handed it went entirely unreported. Sharing execmem's existing helpers
(now pub(crate)) is the whole fix; no per-module reimplementation.
Ordering follows the rule established in #80 at every site: poison
*after* the handler's own zeroing and field initialisation, never
before, because those writes go through the checked path and heal
shadow bytes -- poisoning first would make each handler report a
violation against its own setup. Each call passes
`mark_data_uninit = false` for the same reason: these blocks are zeroed
or field-initialised by the time they are handed over, so they are
genuinely initialised, and marking them uninit would produce false
uninitialized-read reports under --sanitize-uninit.
CreateIORequest is poisoned in its handler rather than in the
create_io_request free function, which takes heap/mem separately and so
has no HandlerContext to reach the shadow map through.
Tests cover both halves: redzones present either side of an
AllocateTagItems block (and the block itself still reading as
initialised), and a freed block reading as Unaddressable with
PoisonReason::Freed specifically, rather than as a redzone.
Not covered, and tracked in #83: exec.library/Allocate, where the guest
owns the MemHeader, so guard space cannot be reserved without touching
the guest's own accounting. That is where C malloc lives. AllocAbs,
AllocEntry and AllocVecPooled are in the LVO table with no handlers, so
there is nothing to miss yet -- they should get poisoning when they get
implemented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AKBJRT9j5APTyKyZtj8f23
Correction, and the sweep finding is a true positiveI mis-attributed the PhxAss violation in the description above — it is caused by this PR, not by #82. My bisect was wrong: the "does it reproduce on main" check piped the build output away and I never verified the rebuild, so I was testing a stale binary. Re-run properly as a matrix, it is unambiguous:
Deterministic across repeated runs and independent of the work directory. Apologies for the noise; #82 is clean. What the violation actually isIt is a genuine out-of-bounds read, which is the whole point of the feature — this PR made it visible by poisoning Traced with temporary instrumentation (since removed):
It is not volamos's own read. So PhxAss reads past its own Consequence for this PRNone, beyond the correction: the detector is working as designed on a real binary. Worth noting for anyone running the corpus sweep in future, though, that PhxAss is no longer silent under I have not chased down which PhxAss source construct produces the over-read; that would need disassembly around |
Tier 0 of #83.
--sanitize's heap detectors only ever poisonedAllocMem/AllocVec/AllocPooled, but three other guest-visible APIs allocate from the sameGuestHeap:AllocateTagItems/FreeTagItemsutility.rsAllocDosObject/FreeDosObjectdosfile.rsCreateIORequest/DeleteIORequest,CreateMsgPort/DeleteMsgPortexeclist.rsBecause they go through
GuestHeap::alloc, redzone space was already being reserved for them whenever--sanitizewas on — the guard bytes existed, nothing marked them poisoned. So a guest overrunning aFileInfoBlock, anRDArgs, aMsgPortor aTagItemarray that volamos handed it went entirely unreported. Sharingexecmem's existing helpers (nowpub(crate)) is the whole fix.Ordering, at every site
Poison after each handler's own zeroing and field initialisation, never before — those writes go through the checked path and heal shadow bytes, so poisoning first would make each handler report a violation against its own setup. Same rule established in #80. Each call also passes
mark_data_uninit = false, because these blocks are initialised by the time they're handed over; marking them uninit would produce false reports under--sanitize-uninit.CreateIORequestis poisoned in its handler rather than in thecreate_io_requestfree function, which takesheap/memseparately and has noHandlerContextto reach the shadow map through.Testing
1002 tests green, clippy clean at
-D warnings, fmt clean — all three verified by exit code this time, not through a pipe.New tests cover both halves: redzones present either side of an
AllocateTagItemsblock (with the block itself still reading as initialised), and a freed block reading asUnaddressablewithPoisonReason::Freedspecifically rather than as a redzone.False-positive sweep: all fixtures,
memtest cleanand pLhA (102-file archive) stay silent; detection still fires onmemtest overrun.One thing to flag, not caused by this PR
The sweep surfaced a violation in PhxAss:
It reproduces on
mainwithout this change, and PhxAss uses none of the four APIs above — I bisected it to #82 (7eac2f1 and 88f0fc9 are both clean). So it's out of scope here, and I'll raise it separately: either a genuine PhxAss 2-byte over-read newly exposed by a heap-layout shift, or a false positive introduced by that PR.Still not covered
exec.library/Allocate(#83's main body), where the guest owns theMemHeaderso guard space can't be reserved without touching its accounting — that's where Cmalloclives.AllocAbs,AllocEntryandAllocVecPooledare in the LVO table with no handlers, so nothing is missed yet; noted that they should get poisoning when implemented.🤖 Generated with Claude Code
https://claude.ai/code/session_01AKBJRT9j5APTyKyZtj8f23