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)
PoC verification
Evidence pack
findings/poc/DF-2302 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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
- 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).
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.- 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).
- 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 evaluatesdb_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
fixedPatched 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
Confirmed kernel references
- sys/bus/firewire/fwohci.c:1229
- sys/bus/firewire/fwohci.c:1236
- sys/bus/firewire/fwohci.c:1238
- sys/bus/firewire/fwohci.c:1157
- sys/bus/firewire/fwohci.c:1166
- sys/bus/firewire/fwohcireg.h:330
- sys/platform/pc64/x86_64/busdma_machdep.c:359
- sys/platform/pc64/x86_64/busdma_machdep.c:412
- sys/platform/pc64/x86_64/busdma_machdep.c:423
- sys/vm/vm_contig.c:560
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.
No comments yet.