bus_dmamap_destroy() frees a map still queued for deferred load (EINPROGRESS) -> UAF with arbitrary-call/RIP control from freed memory
Summary
destroy's only guard is bpages!=NULL - a map queued on bounce_map_waitinglist (empty bpages, pagesneeded!=0) is kfree'd while linked; bounce release then writes into the freed chunk and busdma_swi executes a full deferred load ending in callback(callback_arg,...) through the freed chunk's function pointer. Trigger is CONVENTIONAL driver error cleanup (if(err){unload;destroy;}), zone pressure groomable via ordinary I/O on bouncing tags (default hw.busdma.bounce_alignment=1 makes every alignment>1 tag bounce). Guest-proven via KLD harness on clean boot: planted callback EXECUTED ('HIJACK PROVEN', arg=0xbaadf00d, twice), callback=0x414141414000 -> Stopped at 0x414141414000 (FULL RIP control), plus NULL-deref and slab-metadata-corruption manifestations. Fix (pending-load EBUSY + unload dequeue/reset + swi drop) validated on rebuilt kernel: hijack and panics gone. No in-tree driver exposes destroy-after-EINPROGRESS unpriv (twa handles it) - High not Critical.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5347 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| busdmauaf.c | β | 12.4 KB | view raw | |
| build.sh | β | 172 B | view raw | |
| run.sh | β | 139 B | view raw | |
| build.log | β | 800 B | view raw | |
| run.log | β | 4.7 KB | view raw | |
| panic.txt | β | 939 B | view raw | |
| slab_corruption_panic.txt | β | 1.1 KB | view raw | |
| getmapwaiting_null_panic.txt | β | 1.4 KB | view raw | |
| boot.log.tests123 | β | 17.9 KB | β download | |
| dmesg_fixrun.txt | β | 15.1 KB | view raw | |
| env.txt | β | 1.2 KB | view raw | |
| fix.diff | β | 2.3 KB | view raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-5347 VERDICT β REPRODUCED (kernel UAF β arbitrary-call control-flow hijack)
Root cause (path:line into sys/platform/pc64/x86_64/busdma_machdep.c)
_bus_dmamap_load_buffer()line 645-657: when a blocking load cannot reserve enough bounce pages, the map is queued ondmat->bounce_zone->bounce_map_waitinglist(map->buf/buflen/dmat/callback stashed in the map) andEINPROGRESSis returned. At this momentmap->bpagesis EMPTY andmap->pagesneeded > 0.bus_dmamap_destroy()line 440-449: the ONLY liveness guard isSTAILQ_FIRST(&map->bpages) != NULLβ EBUSY. A queued map passes the guard and iskfree(map, M_DEVBUF)d (72-byte chunk) while still linked on the waiting list. Nothing ever validates queue membership.- Any later bounce-page release (
free_bounce_pageline 1399 βget_map_waitingline 1408) pops the freed chunk:reserve_bounce_pages()line 1279 writesmap->pagesreservedinto freed memory; the chunk is then threaded onto the globalbounce_map_callbacklist(add_map_callbackline 1430). busdma_swi()line 1445 runsbus_dmamap_load(map->dmat, map, map->buf, map->buflen, map->callback, map->callback_arg, 0)on the freed chunk, which ends incallback(callback_arg, segments, nsegs, error)(bus_dma_machdep line 804) β an indirect call through a function pointer read from freed, attacker-repopulatable memory.
The destroy-after-EINPROGRESS pattern is ordinary driver error cleanup
(if (error) { bus_dmamap_unload(); bus_dmamap_destroy(); }). The only
in-tree driver that ever receives EINPROGRESS (twa,
sys/dev/raid/twa/tw_osl_freebsd.c:1390) handles it correctly; every other
driver using a bouncing tag with conventional error handling would convert
bounce-zone pressure into this UAF. Zone pressure is groomable by ordinary
I/O on lowaddr-constrained tags (and by default hw.busdma.bounce_alignment=1
makes every tag with alignment>1 and no BUS_DMA_ALIGNED bounce).
Reproduction (all on DragonFly 6.5-DEVELOPMENT #0, clean snapshot boots)
KLD harness busdmauaf.c (plays the vulnerable driver):
- private 2-page bounce zone (lowaddr=16MB), mapB loads both pages,
mapA's load β EINPROGRESS (queued),
- bus_dmamap_destroy(tag, mapA) β 0 β freed while linked,
- heap spray reclaims the chunk ("spray chunk 0 == freed mapA" β
deterministic LIFO on the 72-byte M_DEVBUF zone),
- freeing mapB's pages wakes the freed map,
- busdma_swi() executes the deferred load on the freed chunk and calls
the planted map->callback (hijack_proof) with planted arg 0xbaadf00d:
BUSDMA-UAF: *** HIJACK PROVEN *** attacker-chosen function 0xffffffff82600000 executed, arg=0xbaadf00d nsegs=1 error=0
(twice on one boot; see run.log).
-
POC_TEST=3 plants
callback=0x414141414000β kernel jumps there:Stopped at 0x414141414000: kernel: type 12 trap, code=0
(see panic.txt) β full RIP control from freed memory.
Two additional independent manifestations captured:
- getmapwaiting_null_panic.txt β NULL-deref panic inside the inlined
reserve_bounce_pages when the freed chunk held garbage dmat.
- slab_corruption_panic.txt β the deferred load's
STAILQ_INSERT_TAIL(&map->bpages, ...) wrote the bounce-page pointer at
offset 0 of the still-free chunk, corrupting kern_slaballoc's free-chunk
linkage; the NEXT unrelated kmalloc(72) panicked in
chunk_mark_allocated (kern_slaballoc.c:1659). Reproduced twice.
Primitive / exploit chain
Arbitrary indirect call in ring 0: target = reclaimed map->callback,
RDI = map->callback_arg (both fully attacker-set via the spray).
With no SMEP/SMAP/KASLR on the audit guest, pointing the callback at any
kernel text gadget (e.g. stack pivot β ROP, or a function that grants
privileges when called with controlled RDI) is mechanically trivial β the
harness demonstrates the controlled call itself, which is the hard part.
Escalation to uid=0 additionally requires an unprivileged-reachable
driver that destroys/unloads an EINPROGRESS map; no in-tree driver
currently does, so end-to-end unprivβroot was not demonstrated and severity
is High rather than Critical.
Fix validation
fix.diff (git-apply clean against the sys/ tree; applied inside the guest
/usr/src only): destroy returns EBUSY while pagesneeded||pagesreserved,
unload dequeues + releases the reservation (includes the one-line
add_map_callback(wait_map) typo fix it depends on), busdma_swi drops
unloaded maps. Patched kernel (#1 Oct 3 06:29) re-ran all three tests:
bus_dmamap_destroy(queued mapA) -> 16 (EBUSY) [tests 1 & 3] RESULT=FAILED (no phantom) [test 2] no panic, guest stays up
Baseline behavior is GONE (dmesg_fixrun.txt).
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 applied to guest /usr/src, kernel rebuilt+installed (nativekernel, X86_64_GENERIC #1). Baseline: destroy=0 + hijack + RIP panic + slab corruption. Patched: destroy returns EBUSY(16) in tests 1 and 3, no hijack, no panic; test 2 phantom callback gone; guest stable across all three runs.
dmesg_fixrun.txt: 'bus_dmamap_destroy(queued mapA) -> 16' (x2) + no HIJACK lines + guest_still_up; fix.diff applies with git apply --check against the pristine sys/ tree
Confirmed kernel references
Detail
Exploit chain
bouncing tag (lowaddr
Evidence (decisive lines)
run.log: 'bus_dmamap_destroy(queued mapA) -> 0', 'spray chunk 0 == freed mapA (reclaimed!)', '*** HIJACK PROVEN *** attacker-chosen function ... executed, arg=0xbaadf00d'; panic.txt: 'Stopped at 0x414141414000'; slab_corruption_panic.txt + getmapwaiting_null_panic.txt: alternate corruptions; dmesg_fixrun.txt: patched kernel -> destroy=16 (EBUSY), no hijack, no panic
PoC changes
harness authored fresh (finding had no seed PoC): KLD module drives the public bus_dma API; struct bus_dmamap replica pinned to 72 bytes after disassembling get_map_waiting.isra.8 (DF STAILQ_HEAD has stqh_first+stqh_last); zone arithmetic tuned so the second map_create grows the private zone to exactly 2 bounce pages; nsegments=4 so the 2-page load fits; tests must run at MOD_LOAD (alloc_bounce_zone's SYSCTL_ADD_NODE deadlocks if called from a sysctl handler)
Verified recommended fix
bus_dmamap_destroy: also return EBUSY when map->pagesneeded || map->pagesreserved (pending deferred load); _bus_dmamap_unload: dequeue queued map and return its reservation; busdma_swi: drop maps whose pagesneeded was reset; see fix.diff (includes the return_bounce_pages wait_map typo fix the unload path depends on)
Verdict
bus_dmamap_destroy() frees a map still linked on the bounce-zone waiting list (its only guard, STAILQ_FIRST(&map->bpages), is empty for an EINPROGRESS-queued map). The deferred machinery later writes into and executes a load on the freed 72-byte chunk, ending in an indirect call through the freed chunk's callback pointer. Demonstrated on a clean-boot DragonFly 6.5 guest: destroy returned 0, a heap spray reclaimed the chunk, and the kernel called the attacker-planted callback (hijack_proof, arg 0xbaadf00d) twice on one boot; POC_TEST=3 planted 0x414141414000 and the kernel jumped there ('Stopped at 0x414141414000'). Two further independent manifestations captured: NULL-deref panic in get_map_waiting/reserve_bounce_pages, and slab-allocator metadata corruption (chunk_mark_allocated panic on the next unrelated kmalloc). impact recorded as panic (strongest fully-demonstrated end state); the underlying primitive is an arbitrary ring-0 indirect call with controlled target and RDI, which on this no-SMEP/SMAP/KASLR guest is one gadget away from uid=0 -- full unpriv-to-root additionally needs an in-tree driver that destroys/unloads an EINPROGRESS map, which none currently does.
No comments yet.