bus_dmamap_destroy() frees a map still queued for a deferred load (EINPROGRESS) -> write-after-free with attacker-followed map->dmat pointer and permanent zone wedge (vkernel twin of pc64 DF-5347)
Summary
Same lifecycle hole as pc64 DF-5347, fork never got the fix: destroy's only guard is bpages!=NULL so a queued map (pagesneeded>0, empty bpages) is kfree'd while linked; the next bounce release pops the freed chunk - reserve follows the freed/replanted map->dmat pointer and writes pagesreserved through it, then the ghost reservation wedges the zone forever (every later load EINPROGRESS). Proven IN A LIVE VKERNEL on the guest (driver compiled into the vkernel): destroy returns 0, 72-byte spray reclaims the exact chunk, kernel follows the PLANTED dmat and writes pagesreserved 0->1 through the freed chunk, victim map wedged, vkernel stays alive (deterministic); no-reclaim run captured stale-link writes 2->3; destroy-EBUSY + unload-dequeue fix validated (EBUSY/healthy). Guest-of-guest reach gated on a bus_dma driver in the vkernel (none stock).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5370 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dfbusdma.c | β | 13.8 KB | view raw | |
| build_kernel.sh | β | 958 B | view raw | |
| build_image.sh | β | 1.1 KB | view raw | |
| run.sh | β | 118 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| build.log | β | 934.3 KB | β download | |
| base-build.log | β | 24.9 KB | view raw | |
| fix-build.log | β | 25.1 KB | view raw | |
| fix-build2.log | β | 25.0 KB | view raw | |
| run.log | β | 3.6 KB | view raw | |
| run.fixed.log | β | 3.2 KB | view raw | |
| run.fixed2.log | β | 3.2 KB | view raw | |
| env.txt | β | 348 B | view raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 7.5 KB | view raw |
DF-5370 VERDICT β REPRODUCED (vkernel kernel write-after-free + controlled-pointer dereference via freed bus_dma map)
Root cause (path:line into sys/platform/vkernel64/platform/busdma_machdep.c)
_bus_dmamap_load_buffer():580-593: a blocking load that cannot fully reserve bounce pages queues the map ondmat->bounce_zone->bounce_map_waitinglist(:587-589), stashesmap->dmat/buf/buflen(:583-585) and returns EINPROGRESS. Queued state:map->bpagesempty,map->pagesneeded > 0,map->pagesreserved= the partial reservation (both observed: 7 / 2 in the run).bus_dmamap_destroy():383-392: only guard isSTAILQ_FIRST(&map->bpages) != NULLβ a queued map iskfreed while still linked. Verified: destroy returned 0.bus_dmamap_unload()(macro sys/bus_dma.h:313) calls only_bus_dmamap_unload():891-900, which walks the emptybpageslist β the queued map is never dequeued (DF-5348 twin), so both orderings of conventional driver error cleanup leave the dangling link armed.- Wake path:
free_bounce_page():1156-1181 βget_map_waiting():1184-1200 pops the freed chunk βreserve_bounce_pages(map->dmat, map, 1):1071-1092 followsmap->dmatread from freed memory (bz = dmat->bounce_zone,BZ_LOCK(&bz->spin)on the pointed-to address, counter writes at bz+52/+56) and writesmap->pagesreserved += pagesinto freed memory.
Reproduction (DragonFly 6.5-DEVELOPMENT guest, VKERNEL64 vkernel booted with -m 512m)
The dfbusdma driver (compiled into the vkernel, SYSINIT at SI_SUB_CONFIGURE) plays a bus_dma driver: private lowaddr=16MB bounce zone seeded with 1 page (+1 per extra map_create β 3 total), mapA loads 1 page (consumed), mapB loads a 7-page buffer above 16MB β pagesneeded=7, partial reserve=2 β queued, EINPROGRESS (DFly EINPROGRESS=36).
bus_dmamap_destroy(tag, mapB)β 0 (freed while queued β UAF armed).- Immediate
kmalloc(72)spray reclaims the exact chunk (SAME CHUNK, deterministic; notesizeof(struct bus_dmamap)== 72 β 128-byte size class β a 64-byte spray misses, which cost one iteration to find). - Planted body:
pagesneeded=0x10000, pagesreserved=0, dmat=<static planted fake tag whose bounce_zone points at the real zone>. - Wake via
bus_dmamap_unload(tag, mapA): kernel followed the planted dmat (0x1120e20) and wrotepagesreserved 0β1into the freed chunk (write-after-free through an attacker-chosen pointer), readpagesneededas 65536 through it. The unsatisfiable pagesneeded keepsget_map_waitingreturning NULL, so the#ifdef notyetadd_map_callbackpanic stub (DF-1044) is NOT reached β the proof isolates this bug and leaves the vkernel ALIVE. - Victim
mapCload β EINPROGRESS forever: the ghost reservation permanently wedges the zone (one bounce page sits on the free list whilefree_bpages==0). - A separate baseline run without reclaim observed the kernel writing
pagesreserved2β3 through the stale link β the UAF does not depend on reclaim luck.
Escalation ceiling: identical code to pc64 DF-5347 (High, arbitrary ring-0 call via the freed map's callback). On the vkernel the deferred callback is the notyet panic, so the chain stops at controlled write + controlled pointer dereference + zone wedge; reaching the stub instead yields the DF-1044 panic. No stock VKERNEL64 driver uses bus_dma, so guest-of-guest reachability requires a bus_dma driver kldloaded into the vkernel β hence Medium severity (memcorrupt bucket) rather than the pc64 twin's High.
Fix validation
fix.diff (destroy EBUSY guard on pagesneeded/pagesreserved; unload dequeues queued maps and returns their reservation):
- baseline run.log: destroy=0, planted-dmat UAF write (0β1), wedge PROVEN.
- run.fixed.log:
bus_dmamap_destroy(queued mapB) = 16 (EBUSY), unload(mapA)+unload(mapB) drain the queue,mapC load err=0 nsegs=1 β zone healthy. No UAF, no wedge. Vkernel ALIVE.
Negative/notes
- No panic in the decisive runs (by design; deterministic).
- EINPROGRESS is 36 on DragonFly (not 115).
- vkernel pmap interplay:
_bus_dma_extract(:503-510) feedspmap_extract/pmap_kextract, whose empty-PTE garbage return is DF-5290; not re-verified here.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff (destroy EBUSY guard + unload dequeue/return-reservation) applied to guest /usr/src, VKERNEL64 rebuilt. Baseline: destroy=0, planted-dmat write-after-free (pagesreserved 0->1), zone wedged (mapC EINPROGRESS). Patched: bus_dmamap_destroy(queued mapB)=16 (EBUSY), unload(mapA)+unload(mapB) drain the waiting list, mapC load err=0 nsegs=1 'zone healthy'; no UAF, no wedge, vkernel ALIVE.
run.fixed.log: 'DF5370: bus_dmamap_destroy(queued mapB) = 16 (EBUSY - fix active, UAF not armed)' and 'DF5370: fixed path: mapC load err=0 nsegs=1 - zone healthy'; fix-build.log full rebuild log; fix.diff is git-apply-able against the pristine sys/ tree (hunk offsets verified)
Confirmed kernel references
- sys/platform/vkernel64/platform/busdma_machdep.c:383
- sys/platform/vkernel64/platform/busdma_machdep.c:587
- sys/platform/vkernel64/platform/busdma_machdep.c:583
- sys/platform/vkernel64/platform/busdma_machdep.c:891
- sys/platform/vkernel64/platform/busdma_machdep.c:1156
- sys/platform/vkernel64/platform/busdma_machdep.c:1184
- sys/platform/vkernel64/platform/busdma_machdep.c:1071
- sys/sys/bus_dma.h:313
Detail
Exploit chain
lowaddr-constrained bouncing tag (private zone, 3 pages) -> mapA load consumes 1 page -> mapB WAITOK load of a 7-page kernel buffer under zone pressure -> reserve_bounce_pages partial (2/7) -> map queued, EINPROGRESS -> driver error cleanup bus_dmamap_destroy(mapB) returns 0: kfree while linked (map_count--, chunk in 128B M_DEVBUF class) -> kmalloc(72) spray reclaims the chunk and plants pagesneeded=0x10000 / pagesreserved=0 / dmat=planted fake tag (bounce_zone -> real zone) -> bus_dmamap_unload(mapA) frees its bounce page -> free_bounce_page -> get_map_waiting pops the freed chunk -> reserve_bounce_pages dereferences the planted dmat (BZ_LOCK on attacker-chosen address, bz->free_bpages/reserved_bpages writes) and writes pagesreserved 0->1 into freed memory -> reservation stays partial so get_map_waiting returns NULL, avoiding the notyet add_map_callback panic -> zone permanently wedged (ghost reservation), all later loads on the zone EINPROGRESS forever. With a satisfiable planted pagesneeded the chain instead continues to dequeue + add_map_callback, which on this vkernel is panic('uncoded') (DF-1044); with the callback machinery implemented (as on pc64) this same chain yields the arbitrary ring-0 call of DF-5347.
Evidence (decisive lines)
run.log (pristine-source baseline): 'load B ret=36 (EINPROGRESS - mapB queued...)', 'mirror check mapB pagesneeded=7 pagesreserved=2 ... OK', 'bus_dmamap_destroy(queued mapB) = 0 (FREED WHILE STILL QUEUED - UAF armed)', 'reclaim: freed mapB=0x800e966068 fake=0x800e966068 SAME CHUNK - planted ... dmat=0x1120e20 (fully controlled)', 'after wake: freed chunk pagesneeded=65536 pagesreserved=1', 'VERDICT PROVEN - kernel wrote pagesreserved (0->1) into the reclaimed chunk after following its PLANTED dmat pointer', 'victim mapC load err=36 (EINPROGRESS forever...)', 'VERDICT wedge PROVEN'. run.fixed.log (fix.diff): 'bus_dmamap_destroy(queued mapB) = 16 (EBUSY - fix active, UAF not armed)' + 'fixed path: mapC load err=0 nsegs=1 - zone healthy'. An earlier baseline also captured the no-reclaim variant: 'after wake: freed chunk pagesneeded=7 pagesreserved=3' (stale 2->3 write through the queued link).
PoC changes
no seed PoC existed; driver authored fresh. Fixes vs first draft: (1) EINPROGRESS is 36 on DragonFly, and bus_dmamap_load returns 0 on non-EINPROGRESS errors (error only reaches the callback) - had to read cb_err; (2) large kmallocs are page-quantized so a +2048 interior offset forces uniform non-zero pmap_kextract offsets; (3) BOUNCE_ALIGN tags only bounce the first (partial) page of a linear buffer - the count loop samples pages at aligned starts - so the demo uses a lowaddr=16MB BOUNCE_LOWADDR tag where every heap page bounces; (4) DFly STAILQ_HEAD carries stqh_first AND stqh_last: sizeof(bus_dmamap)=72 and mirror offsets are pagesneeded=16/pagesreserved=20/dmat=24 - the 72-byte chunk lives in the 128-byte class, so the spray must kmalloc(72), not 64 (64-byte spray never reclaims); (5) vkernel needs /var/vkernel to exist and the mountroot prompt answered (ufs:vkd0) for a clean boot.
Verified recommended fix
bus_dmamap_destroy: also return EBUSY when map->pagesneeded || map->pagesreserved (pending deferred load); _bus_dmamap_unload: dequeue the map from the bounce-zone waiting list and return its reservation; see fix.diff (validated: EBUSY + zone healthy)
Verdict
bus_dmamap_destroy() in sys/platform/vkernel64/platform/busdma_machdep.c:383-392 frees a bus_dmamap that is still linked on bounce_zone->bounce_map_waitinglist after an EINPROGRESS deferred load (queued at :587-589); bus_dmamap_unload is equally a no-op for a queued map (macro calls only _bus_dmamap_unload:891-900, which walks the empty bpages list - the DF-5348 twin). Proven in a live VKERNEL64 vkernel on the guest with a compiled-in test driver: destroy of the queued map returned 0 (freed while queued), an immediate kmalloc(72) spray reclaimed the exact chunk, and the next bounce-page release (free_bounce_page:1156 -> get_map_waiting:1184 -> reserve_bounce_pages:1071) followed the PLANTED map->dmat pointer (attacker-chosen address, spinlock+counter writes through it) and wrote map->pagesreserved 0->1 into the freed chunk. The freed map's ghost reservation also permanently wedged the zone: a victim map's load returns EINPROGRESS forever while a bounce page sits on the free list (free_bpages==0) - recorded impact dos is the strongest fully-demonstrated end state; the underlying primitive is a vkernel-kernel write-after-free with controlled pointer dereference, the vkernel twin of pc64 DF-5347 (High, arbitrary ring-0 call). On the vkernel the deferred chain is cut off by the #ifdef notyet add_map_callback panic stub (pass-1 DF-1044, not re-reported here) unless the attacker keeps the reservation partial, as the PoC deliberately does to stay crash-free. Guest-of-guest reachability requires a bus_dma-using driver inside the vkernel (none in stock VKERNEL64; the file is compiled into every vkernel), hence Medium rather than the pc64 twin's High.
No comments yet.