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

NULL pointer dereference in fwohci_db_free when bus_dmamap_create fails partway through initialization

Summary

In fwohci_db_init if bus_dmamap_create fails on iteration K of ndb-entry loop fwohci_db_free called to clean up. But fwohci_db_free loop iterates ndb times over STAILQ that only has K entries. After K iterations STAILQ_NEXT returns NULL but idb<ndb still true so body dereferences db_tr->buf and db_tr->dma_map with db_tr==NULL -> kernel panic. Circular link only established AFTER loop completes. Trigger: memory pressure during controller init or iso channel setup. [VERIFIED pass2: reproduced as real panic - modeled 4MB DMA window exhaustion drives real bus_dmamap_create failure then real fwohci_db_free Fatal trap 12 at fault VA 0x18; DF busdma clears later map_create failures so K=0 is the reachable form; fix-validated NULL-guarded unwind clean across 46 rounds, naive loop-only fix would have kfree NULL - caught]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2302 Β· 17 files
FileTypeDescriptionSize
README.md β€” 3.7 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
build.log β€” 928 B view raw
build_fix.sh β€” 511 B view raw
build_kernel.sh β€” 380 B view raw
env.txt β€” 1.3 KB view raw
fix.diff β€” 760 B view raw
fwohci_harness.c β€” 22.3 KB view raw
fwohci_harness.h β€” 3.9 KB view raw
fwpoc.c β€” 7.9 KB view raw
panic.txt β€” 2.9 KB view raw
prep_guest.sh β€” 1.8 KB view raw
run.log β€” 3.8 KB view raw
run.sh β€” 134 B view raw
run_fixed.log β€” 4.7 KB view raw
run_fixed.sh β€” 1.9 KB view raw
verdict.json β€” 4.9 KB view raw

DF-2302 β€” fwohci_db_free NULL deref on partial bus_dmamap_create failure

What this is

Verification pack for finding DF-2302 (sys/bus/firewire/fwohci.c:1236 failure path -> fwohci_db_free, fwohci.c:1157-1182). Low / local DoS panic. Claim: if bus_dmamap_create() fails at loop iteration K, fwohci_db_init fakes FWOHCI_DBCH_INIT (fwohci.c:1238) and calls fwohci_db_free, whose loop (fwohci.c:1166-1167) iterates idb < ndb over a queue that only holds K entries -> STAILQ_NEXT yields NULL -> db_tr->buf/db_tr->dma_map dereference of NULL -> kernel panic.

Verdict (short)

REPRODUCED β€” kernel panic through 100% real code:

bus_dmamap_create failed                              <- fwohci.c:1237
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x18                          <- NULL + offsetof(fwohcidb_tr, dma_map)
fwohci_db_free.part.7() at ...  <- fwohci.c:1166-1174
fwoctl_ioctl() ...
panic: page fault

The panic (panic.txt) came from the REAL fwohci_db_init -> REAL bus_dma failure -> REAL fwohci_db_free, on the verbatim fwohci.c TU harness kernel. Trigger: the 32-bit DMA bounce window (4 MB, modeling a >4 GB-RAM host where the OHCI 32-bit window is a scarce resource) exhausted so alloc_bounce_pages -> contigmalloc fails (busdma_machdep.c:412) inside bus_dmamap_create.

Rig mechanics (how the real failure was produced)

  • The fabricated parent bus_dma tag has lowaddr=0x400000 (4 MB). On a

    4GB-RAM host the stock fwohci tag (lowaddr=BUS_SPACE_MAXADDR_32BIT, fwohci.c:1197) has exactly this character: bounce pages must come from below lowaddr.

  • fwpoc ctl memeat F ch: pins EVERY free page of [0,4MB) with contigmalloc (M_WAITOK β€” same 3-pass reclamation power as the bus_dmamap_create path, sys/vm/vm_contig.c:300-376), frees F pages, then calls the real fwohci_db_init on the channel.
  • Repeated rounds convert window pages into permanent zone bounce pages (each successful db_init permanently parks 1-2 pages in the busdma zone). After ~57 rounds the window is fully pinned (ate=0); the final round runs db_init with zero free window pages -> the first bus_dmamap_create fails -> db_free -> panic (run.log has the ate-by-round history: 128 -> 0).

K-quantification (DF 6.5 busdma behavior found during verification)

bus_dmamap_create() only returns ENOMEM while BUS_DMA_MIN_ALLOC_COMP is clear β€” after the first successful create the else { error = 0; } at busdma_machdep.c:423-425 clears later alloc failures. Consequence: on this tree the reachable form of the bug is K=0 (first create fails, queue empty, first loop body derefs NULL β€” fault VA 0x18 = offsetof(dma_map)). The K>0 form (partial queue) needs a first-create success followed by a later failure, which current busdma masks β€” but the loop defect is identical, and the fix guards both.

Reachability honesty

Requires FireWire hardware + memory pressure in the 32-bit DMA window during channel (re)initialization (controller init, iso channel setup). Attacker role: unprivileged users can generate the memory pressure; the init window belongs to open/enable paths reachable from the operator group. Severity Low as filed (panic/DoS only).

Fix

fix.diff: NULL-safe loop (db_tr != NULL && idb < dbch->ndb) plus a guard on the tail kfree(STAILQ_FIRST(...)) (kfree(NULL) panics on DF, kern_slaballoc.c:1409 β€” the tail free otherwise just moves the panic). Residual (documented): the K=0 error path leaks the one kmalloc'd db_tr array because db_init loses its base pointer across the loop; fixing that cleanly requires keeping the array head in db_init (not done to keep the patch minimal). A/B validated: fixed kernel survives the identical trigger with a clean unwind (see run_fixed.log).

VERDICT.md
↓ download raw

DF-2302 VERDICT β€” NULL deref in fwohci_db_free after partial db_init

  • finding: DF-2302, fwohci.c:1236-1241 (bus_dmamap_create failure -> faked FWOHCI_DBCH_INIT -> fwohci_db_free) and fwohci.c:1166-1174 (the unguarded loop)
  • status: reproduced (kernel panic, fatal trap 12, fault VA 0x18)
  • impact: panic (denial of service; requires memory pressure in the 32-bit DMA window during FireWire channel init on real hardware)
  • confidence: certain

Reproduction narrative

  1. Rig: fwohci.c TU harness (same as DF-2300); fabricated parent bus_dma tag with a 4 MB lowaddr models the >4 GB-RAM host on which the stock fwohci tag (lowaddr = BUS_SPACE_MAXADDR_32BIT, fwohci.c:1197) must bounce. All busdma code executed is the real stock platform code (busdma_machdep.c, vm_contig.c).
  2. fwpoc ctl memeat 2 3: eat the whole [0,4MB) window with M_WAITOK contigmalloc (same 3-pass reclamation power the map_create path has), free 2 pages, run REAL fwohci_db_init on arrs.
  3. Rounds 1..56: db_init succeeds; each round permanently converts 1-2 window pages into busdma-zone bounce pages (ate declines 128 -> 0 monotonically, run.log).
  4. Final round (ate=0, freed=0): inside the real fwohci_db_init loop the FIRST bus_dmamap_create of dbch->dmat hits alloc_bounce_pages -> contigmalloc -> NULL (busdma_machdep.c:412, vm_contig.c:560-580) -> ENOMEM -> fwohci.c:1237 prints "bus_dmamap_create failed" -> fwohci.c:1238 fakes INIT -> fwohci_db_free -> fwohci.c:1166 loop starts with db_tr = STAILQ_FIRST = NULL (queue empty, K=0) -> first body evaluates db_tr->dma_map (compiled first; fault VA 0x18 = offsetof(struct fwohcidb_tr, dma_map), fwohcireg.h:330-337) -> fatal trap 12 -> panic. Serial capture verbatim in panic.txt (backtrace: fwohci_db_free.part.7 <- fwoctl_ioctl; the .part.7 is GCCs clone of the static fwohci_db_free from the TU-compiled verbatim fwohci.c).

K-quantification (new line-accurate knowledge from this run)

busdma_machdep.c:412-425: after the first successful create sets BUS_DMA_MIN_ALLOC_COMP, the else { error = 0; } clears subsequent alloc_bounce_pages failures. Only the FIRST map_create of a tag can return ENOMEM. Hence on this tree the reachable defect form is K=0 (empty queue, fault at iteration 0). The originally-filed K>0 form (partial queue, NULL after the last queued entry) is masked by the busdma quirk today but is the same unguarded loop; any future busdma change re-arms it. The fix guards both forms.

Fix validation (A/B)

fix.diff: * loop condition db_tr != NULL && idb < dbch->ndb (stops at the real queue tail), * if (db_tr != NULL) kfree(db_tr, M_FW); on the tail free β€” WITHOUT this, the fixed loop merely moves the panic to kfree(NULL) (kern_slaballoc.c:1409 panics on NULL), which I verified while authoring the patch. Fixed kernel (FWO #4): identical trigger (full window exhaustion + memeat rounds) -> "bus_dmamap_create failed" printed, db_free unwinds without panic, guest stays up, repeated rounds keep surviving (run_fixed.log). Residual K=0 leak of the db_tr array documented in README (db_init loses the array base across the loop).

Reachability

Hardware + operator-group (channel setup) + 32-bit-DMA-window memory pressure. Unprivileged users contribute the pressure; the panic is local DoS. No privilege escalation path β€” NULL-deref only.

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

Patched kernel: full conditioning replicated from a fresh boot (41 rounds to window exhaustion), the bus_dmamap_create failed message observed twice in dmesg (the exact failure that panicked the baseline), every round returned survived=1, channel state flags=0 (fwohci_db_free completed cleanly), guest up throughout.

run_fixed.log: rounds 1..41 with ate declining 128->0 then terminal rounds; dmesg: 2x bus_dmamap_create failed + db_init RETURNED flags=0; ctl state 3: flags=0 ndb=64 top=1
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #4: Sun Oct 18 12:45:22 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/FWO x86_64

Confirmed kernel references

Detail

Exploit chain

memory pressure in the 32-bit DMA window (unpriv users can generate it) + FireWire channel init (operator group) -> bus_dmamap_create ENOMEM inside fwohci_db_init -> fake flag + fwohci_db_free -> NULL deref -> fatal trap 12 -> kernel panic (local DoS). No escalation path: NULL dereference only.

Evidence (decisive lines)

panic.txt: bus_dmamap_create failed then Fatal trap 12 fault VA 0x18 with fwohci_db_free in the trace, after ate=0/freed=0 window exhaustion; run.log: the 57-round conditioning history (ate 128 -> 0, each round converting 1-2 window pages into permanent zone bounce pages); run_fixed.log: 41 conditioning rounds + 5 terminal rounds on the patched kernel, bus_dmamap_create failed observed twice, every round survived, ctl state shows flags=0 (clean unwind)

PoC changes

Rig FWOIOC_MEMEAT: contigmalloc(M_WAITOK) eater pins the whole [0,4MB) window with the same 3-pass reclamation power as the busdma path, frees F pages, then runs the real fwohci_db_init; a 4MB-lowaddr fabricated parent tag models the >4GB host. Initial NOWAIT eater and small F values failed to trigger because the first map_create can reclaim cache/active pages; the WORKING sequence is repeated rounds (each permanently converts window pages into zone bounce pages) until ate=0, then the terminal rounds fail map_create deterministically.

Verified recommended fix

NULL-safe loop condition (db_tr != NULL && idb < dbch->ndb) plus guard the tail kfree(STAILQ_FIRST(...)) (fix.diff)

Verdict

Kernel panic through 100% real code: with the sub-4MB 32-bit DMA bounce window exhausted (rig models the >4GB-RAM host where the stock fwohci tag lowaddr=32BIT must bounce), the REAL fwohci_db_init loop hit a REAL bus_dmamap_create failure (alloc_bounce_pages -> contigmalloc -> NULL, busdma_machdep.c:412), printed bus_dmamap_create failed (fwohci.c:1237), faked FWOHCI_DBCH_INIT (fwohci.c:1238) and called fwohci_db_free, whose unguarded loop (fwohci.c:1166-1167, idb < ndb over a K-entry queue) dereferenced NULL: Fatal trap 12, fault VA 0x18 = offsetof(fwohcidb_tr, dma_map), backtrace fwohci_db_free <- db_init <- ioctl (panic.txt). New line-accurate knowledge: DF busdma clears later map_create failures once MIN_ALLOC_COMP is set (busdma_machdep.c:423-425 error=0), so the reachable form on this tree is K=0 (empty queue, fault at iteration 0); the K>0 partial-queue form is masked today but is the same defect and re-arms with any busdma change. Fix (NULL-safe loop + tail kfree guard) validated A/B: the patched kernel took the identical bus_dmamap_create-failed path repeatedly and unwound cleanly every time.