--sanitize's heap detectors (#65, #66) wrap AllocMem/AllocVec/AllocPooled. They do not reach exec.library/Allocate, and that turns out to be where C programs' malloc actually lives — so the heap detectors miss the most common heap bug in the most common kind of program.
Measured
A deliberately buggy C program built with the real m68k-amigaos-gcc:
static void clobber(char *p) { p[32] = 'X'; } /* one past a 32-byte buffer */
int main(void) {
char *buf = malloc(32);
clobber(buf);
...
}
Under --sanitize, volamos reports nothing. The equivalent bug written against AllocMem (fixtures/memtest overrun) is caught immediately.
Tracing the allocation calls that program makes explains why:
11 exec.library(-186) -> Allocate
3 exec.library(-684) -> AllocVec
12 exec.library(-564) -> ObtainSemaphore
libnix's malloc takes a few large chunks via AllocVec and then sub-allocates from its own MemHeader with Allocate — eleven Allocate calls for one malloc(32). So the guest's 32-byte buffer sits in the middle of a chunk volamos handed out as one big block, and buf[32] lands in libnix's own pool rather than in any redzone volamos placed.
Stack detection is unaffected, which is why a gcc-built strcpy stack smash is caught (see #74) — this is specific to the heap detectors.
Why this is fixable
crate::execchunk implements the real Allocate/Deallocate algorithm itself, walking the guest's MemChunk chain. So volamos already sees every call, the returned address, and the size — the information the detectors need is in hand; it simply isn't used.
Worth being clear about what differs from AllocMem: there, volamos owns the heap and can freely reserve extra space for redzones. Here the guest owns the pool, so any change to block sizes has to keep the guest's own chunk accounting consistent.
Tier 1 — no layout change (cheap, do this first)
Mark the shadow map on the existing calls, without altering a single byte of the guest's pool:
- On
Allocate: mark the returned range Uninit (unless it is a cleared path), exactly as execmem's handlers do.
- On
Deallocate: mark the freed range Unaddressable with PoisonReason::Freed.
That immediately buys use-after-free and uninitialised-read detection for malloc'd memory, which is most of the value, for a handful of lines and no behavioural risk. Note the ordering rule from #80 applies: writes heal shadow bytes, so poison after any zeroing the handler does, not before.
Tier 2 — redzones (harder, needs care)
Catching overruns needs guard bytes, which means Allocate taking more from the chunk than the caller asked for and returning an inner pointer, with a host-side map from returned pointer to true extent so Deallocate can release the real block. GuestHeap already does exactly this for AllocMem (alloc_with_requested/extent_of_live_alloc), so the shape is proven.
Two things to think through before attempting it:
- A guest that walks its own
mh_First chain to compute free memory will see different numbers. That is probably acceptable — it is the same trade AvailMem already makes under --sanitize (see guestmem's docs) — but it is a genuine guest-visible change, whereas Tier 1 is not, so it deserves its own decision and possibly its own flag.
Deallocate takes the size the caller believes it allocated, so the pointer-to-extent map is load-bearing: get it wrong and the guest's pool corrupts. execmem's FreeMem size-mismatch check is the precedent for failing loudly rather than silently mis-freeing.
Scope
Related
#65 (the sanitizer), #66 (heap redzones and the quarantine), #68 (uninitialised reads), #80 (--dirty-heap, whose fill has the same Allocate blind spot), #74 (where this was found, and why stack detection is unaffected).
--sanitize's heap detectors (#65, #66) wrapAllocMem/AllocVec/AllocPooled. They do not reachexec.library/Allocate, and that turns out to be where C programs'mallocactually lives — so the heap detectors miss the most common heap bug in the most common kind of program.Measured
A deliberately buggy C program built with the real
m68k-amigaos-gcc:Under
--sanitize, volamos reports nothing. The equivalent bug written againstAllocMem(fixtures/memtest overrun) is caught immediately.Tracing the allocation calls that program makes explains why:
libnix's
malloctakes a few large chunks viaAllocVecand then sub-allocates from its ownMemHeaderwithAllocate— elevenAllocatecalls for onemalloc(32). So the guest's 32-byte buffer sits in the middle of a chunk volamos handed out as one big block, andbuf[32]lands in libnix's own pool rather than in any redzone volamos placed.Stack detection is unaffected, which is why a gcc-built
strcpystack smash is caught (see #74) — this is specific to the heap detectors.Why this is fixable
crate::execchunkimplements the realAllocate/Deallocatealgorithm itself, walking the guest'sMemChunkchain. So volamos already sees every call, the returned address, and the size — the information the detectors need is in hand; it simply isn't used.Worth being clear about what differs from
AllocMem: there, volamos owns the heap and can freely reserve extra space for redzones. Here the guest owns the pool, so any change to block sizes has to keep the guest's own chunk accounting consistent.Tier 1 — no layout change (cheap, do this first)
Mark the shadow map on the existing calls, without altering a single byte of the guest's pool:
Allocate: mark the returned rangeUninit(unless it is a cleared path), exactly asexecmem's handlers do.Deallocate: mark the freed rangeUnaddressablewithPoisonReason::Freed.That immediately buys use-after-free and uninitialised-read detection for
malloc'd memory, which is most of the value, for a handful of lines and no behavioural risk. Note the ordering rule from #80 applies: writes heal shadow bytes, so poison after any zeroing the handler does, not before.Tier 2 — redzones (harder, needs care)
Catching overruns needs guard bytes, which means
Allocatetaking more from the chunk than the caller asked for and returning an inner pointer, with a host-side map from returned pointer to true extent soDeallocatecan release the real block.GuestHeapalready does exactly this forAllocMem(alloc_with_requested/extent_of_live_alloc), so the shape is proven.Two things to think through before attempting it:
mh_Firstchain to compute free memory will see different numbers. That is probably acceptable — it is the same tradeAvailMemalready makes under--sanitize(seeguestmem's docs) — but it is a genuine guest-visible change, whereas Tier 1 is not, so it deserves its own decision and possibly its own flag.Deallocatetakes the size the caller believes it allocated, so the pointer-to-extent map is load-bearing: get it wrong and the guest's pool corrupts.execmem'sFreeMemsize-mismatch check is the precedent for failing loudly rather than silently mis-freeing.Scope
UninitonAllocate,FreedonDeallocate. Verify a use-after-free and an uninitialised read throughmallocin a gcc-built C program are both reported.mallocuse-after-free and an uninitialisedmallocread.scmust stay clean, plus the gcc binaries now in play.mallocin a C runtime may not be reached.Related
#65 (the sanitizer), #66 (heap redzones and the quarantine), #68 (uninitialised reads), #80 (
--dirty-heap, whose fill has the sameAllocateblind spot), #74 (where this was found, and why stack detection is unaffected).