β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5348

bus_dmamap_unload() of an EINPROGRESS-queued map is a silent no-op: phantom deferred load + callback on stale buffer state, reservation leak

Summary

_bus_dmamap_unload only frees active bounce pages - a queued map stays on the waiting list with stale buf/buflen/callback and partial reservation; any later bounce release in the zone dequeues the UNLOADED map and busdma_swi runs a full deferred load on it: driver callback for an aborted transaction over the freed/reused buffer (pmap_kextract of stale vaddr + bounce copies in sync = write-after-free at the driver) + stuck reservations drain the shared zone. Guest-proven (harness POC_TEST=2): 'DEFERRED CALLBACK FIRED FOR UNLOADED MAP', PHANTOM CONFIRMED; patched kernel FAILED (no callback). Same diff as DF-5347.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5348 Β· 9 files
FileTypeDescriptionSize
busdmauaf.c β€” 12.4 KB view raw
run.log.baseline β€” 4.7 KB ↓ download
VERDICT.md β€” 2.8 KB ↓ raw
README.md β€” 2.0 KB ↓ raw
fix.diff β€” 2.3 KB view raw
build.log β€” 800 B view raw
env.txt β€” 1.2 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 3.3 KB view raw

DF-5348 β€” bus_dmamap_unload() of a deferred-load (EINPROGRESS) map is a silent no-op β†’ phantom deferred load + callback on stale state

sys/platform/pc64/x86_64/busdma_machdep.c (DragonFly 6.5-DEVELOPMENT)

What

bus_dmamap_unload (macro, sys/sys/bus_dma.h:313) only calls _bus_dmamap_unload() (busdma_machdep.c:1022), which merely frees active bounce pages. For a map queued at EINPROGRESS it is a complete no-op: the map stays on bz->bounce_map_waitinglist, keeps pagesneeded/ pagesreserved and its partial bounce-page reservation, and keeps stale buf/buflen/callback/callback_arg. Nothing ever dequeues it except get_map_waiting().

When any bounce page is later freed anywhere in the zone, the reservation completes, the map is dequeued, and busdma_swi() executes bus_dmamap_load(map->dmat, map, map->buf, map->buflen, ...) β€” a full deferred load for a map the driver has already unloaded β€” and invokes the driver callback with stale state. If the driver freed or reused the buffer after unload (the normal abort path), the deferred load pmap_kextracts the stale buffer and later bus_dmamap_sync bounces into/out of it β†’ write-after-free at the driver level. The stuck reservation also silently shrinks the zone (DoS).

Reproduce

cd /root/busdmauaf
printf "KMOD=busdmauaf_t2\nSRCS=busdmauaf.c\nCOPTS+=-DPOC_TEST=2\n..." > Makefile
make -m /usr/share/mk SYSDIR=/usr/src/sys MAKEOBJDIRPREFIX=/root/obj
kldload ./busdmauaf_t2.ko
dmesg | grep PHANTOM

Expected (baseline)

BUSDMA-PHANTOM: mapY load -> 36 (EINPROGRESS; mapY queued, ...)
BUSDMA-PHANTOM: bus_dmamap_unload(mapY) returned -- map is STILL on the waiting list
BUSDMA-PHANTOM: *** DEFERRED CALLBACK FIRED FOR UNLOADED MAP *** (after unload(mapX))
BUSDMA-PHANTOM: RESULT=PHANTOM CONFIRMED

Fix validation (same fix.diff as DF-5347)

BUSDMA-PHANTOM: RESULT=FAILED      (no callback fires; map dequeued+reservation released)
BUSDMA-PHANTOM: cleanup done       (guest stable)
VERDICT.md
↓ download raw

DF-5348 VERDICT β€” REPRODUCED (phantom deferred load + callback on an unloaded map)

Root cause

  • sys/sys/bus_dma.h:313 β€” bus_dmamap_unload expands to _bus_dmamap_unload only.
  • sys/platform/pc64/x86_64/busdma_machdep.c:1022-1031 β€” _bus_dmamap_unload frees active bpages and nothing else: it never removes the map from bz->bounce_map_waitinglist (busdma_machdep.c:651, the EINPROGRESS queueing site) and never resets map->pagesneeded/map->pagesreserved or releases the reservation taken by reserve_bounce_pages (busdma_machdep.c:1263).
  • The ONLY dequeue path is get_map_waiting() (busdma_machdep.c:1408), driven by any bounce-page release (free_bounce_page:1399 / return_bounce_pages:1306) β€” even long after the driver aborted.
  • busdma_swi() (busdma_machdep.c:1445) then executes the deferred bus_dmamap_load with the map's stashed buf/buflen/callback (queue-time values, busdma_machdep.c:647-649) and invokes the driver callback (busdma_machdep.c:804).

Reproduced

KLD harness (POC_TEST=2), clean-boot DragonFly 6.5-DEVELOPMENT guest:

mapY load -> 36 (EINPROGRESS; mapY queued, holds partial reservation of 1 bpage)
bus_dmamap_unload(mapY) returned -- map is STILL on the waiting list
bus_dmamap_unload(mapX) frees a bpage -> wakes the UNLOADED mapY ...
*** DEFERRED CALLBACK FIRED FOR UNLOADED MAP *** buf=0xd0000002 nsegs=2 error=0
RESULT=PHANTOM CONFIRMED

The callback fired for a transaction the driver had already unloaded; the deferred load also bound a bounce page to the stale buffer (datavaddr) inside the map. With a driver that frees its buffer on abort (the normal pattern) this becomes a write-after-free via the subsequent bus_dmamap_sync(PREWRITE/POSTREAD) bounce copies (busdma_machdep.c:1046, 1057).

Impact

Concrete (demonstrated): driver-visible phantom callback after unload β†’ double completion of aborted transactions, stale-buffer DMA setup, and leaked bounce-page reservations shrinking the shared zone (zone-exhaustion DoS). Driver-dependent worst case: write-after-free in whichever driver uses the map, on any bounce-page release (attacker-groomable timing).

Fix validation

Same fix.diff as DF-5347 (unload dequeues + releases reservation; busdma_swi drops reset maps). Patched kernel #1 (Oct 3 06:29): mapY's unload dequeues it, the later page free wakes nothing:

RESULT=FAILED      <- phantom callback did NOT fire (fix works)
cleanup done

Guest stable. Baseline behavior GONE.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (DF-5347 pack) applied to guest /usr/src, kernel rebuilt. Baseline: phantom callback fired for unloaded map. Patched: 'RESULT=FAILED' -- no callback after unload, reservation released, cleanup completed, guest stable.

dmesg_fixrun.txt (DF-5347 pack): BUSDMA-PHANTOM lines on patched kernel show RESULT=FAILED + cleanup done
↓ fix.diffper-fix-DF-5348

Confirmed kernel references

Detail

Exploit chain

bouncing tag -> load returns EINPROGRESS (map queued holding partial reservation) -> driver aborts: bus_dmamap_unload (no-op) + buffer freed/reused -> any later bounce-page release in the zone -> get_map_waiting completes reservation + dequeues the unloaded map -> busdma_swi runs deferred load on stale buf -> driver callback fires for dead transaction / bounce copies touch the freed buffer

Evidence (decisive lines)

run.log.baseline: 'bus_dmamap_unload(mapY) returned -- map is STILL on the waiting list' then '*** DEFERRED CALLBACK FIRED FOR UNLOADED MAP ***' and 'RESULT=PHANTOM CONFIRMED'; patched kernel (dmesg in DF-5347/dmesg_fixrun.txt): 'RESULT=FAILED' + 'cleanup done'

PoC changes

authored fresh (no seed); POC_TEST=2 variant of the same KLD harness as DF-5347

Verified recommended fix

_bus_dmamap_unload: if pagesneeded||pagesreserved, STAILQ_REMOVE the map from the zone waiting list and call return_bounce_pages; busdma_swi: skip maps whose counters were reset (see fix.diff)

Verdict

bus_dmamap_unload() is a silent no-op for a map queued at EINPROGRESS: _bus_dmamap_unload only frees active bounce pages and never dequeues the map from bounce_map_waitinglist nor resets pagesneeded/pagesreserved nor releases its partial bounce-page reservation. Reproduced on a clean-boot DragonFly 6.5 guest: after unloading the queued map, releasing any bounce page in the zone completed its reservation and busdma_swi executed a full deferred bus_dmamap_load on the UNLOADED map, invoking its callback with stale buf/buflen state ('DEFERRED CALLBACK FIRED FOR UNLOADED MAP'). For drivers that free/reuse their buffer after aborting (the normal pattern) this is a write-after-free via the subsequent sync bounce copies; the stuck reservations also drain the shared bounce zone (DoS). impact recorded as dos (the driver-independent harm); the driver-dependent write-after-free ceiling is documented in VERDICT.md.