DF-5347 / verdict.json
{ "finding_id": "DF-5347", "status": "reproduced", "reproduced": true, "impact": "panic", "confidence": "certain", "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 chunks 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.", "exploit_chain": "bouncing tag (lowaddr<physmem or alignment>1 w/ default bounce_alignment=1) -> bus_dmamap_load returns EINPROGRESS under groomed bounce-zone pressure -> driver error-cleanup calls bus_dmamap_destroy -> map kfreed while linked on bz->bounce_map_waitinglist -> spray reclaims the 72B M_DEVBUF chunk, plants dmat/buf/buflen/callback/callback_arg -> any bounce-page release (free_bounce_page) -> get_map_waiting writes map->pagesreserved in freed memory, dequeues, threads chunk onto bounce_map_callbacklist -> busdma_swi -> bus_dmamap_load on freed chunk -> callback(callback_arg,...) = arbitrary ring-0 call, target+RDI attacker-controlled -> (no SMEP/SMAP/KASLR on guest) stack pivot/ROP -> uid=0; demonstrated up to the controlled call incl. RIP=0x414141414000", "evidence": "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", "kernel_refs": [ "sys/platform/pc64/x86_64/busdma_machdep.c:645", "sys/platform/pc64/x86_64/busdma_machdep.c:440", "sys/platform/pc64/x86_64/busdma_machdep.c:1279", "sys/platform/pc64/x86_64/busdma_machdep.c:1408", "sys/platform/pc64/x86_64/busdma_machdep.c:1445", "sys/platform/pc64/x86_64/busdma_machdep.c:804" ], "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_zones SYSCTL_ADD_NODE deadlocks if called from a sysctl handler)", "attempts": 7, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline) / #1 Oct 3 06:29 for fix validation", "runtime_sec": 4800, "guest_dirty": 0, "build_cmd": "cd /root/busdmauaf && make -m /usr/share/mk SYSDIR=/usr/src/sys MAKEOBJDIRPREFIX=/root/obj (COPTS+=-DPOC_TEST=1|3)", "run_cmd": "kldload /root/busdmauaf/busdmauaf_t1.ko ; dmesg | tail (t3: kldload busdmauaf_t3.ko -> panic at 0x414141414000)", "code_hash": "bab05c50e33011b81dbdc0092940682cf9cebd45759eca53a301eacf188b3167", "notes": "destroy-after-EINPROGRESS is conventional driver error cleanup; twa (the only in-tree EINPROGRESS-aware driver) handles it correctly, so severity is High (kernel UAF w/ RIP control) rather than Critical. Zone pressure is groomable via ordinary I/O on lowaddr-constrained tags. Side-captures: 2x chunk_mark_allocated slab panic, get_map_waiting NULL deref. Guest reverted to clean snapshot after validation.", "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)", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "fix.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.", "fix_evidence": "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" } |