Off-by-one OOB read in print_db iterates one past the DMA descriptor array
Summary
print_db loops with for(i=0;i<=max;i++) where max=dbch->ndesc iterating ndesc+1 times over db array that has only ndesc entries. Uses <= instead of <. When key==OHCI_KEY_ST2 accesses db[i+1] (line 2379) then i++ (line 2400) compounding OOB. Reads past DMA descriptor allocation potentially causing kernel panic if access hits unmapped memory or leaking adjacent DMA memory contents to kernel console. Reached via DUMPDMA ioctl (operator group) or when firewire_debug is set. [VERIFIED pass2: reproduced with byte-exact attribution - row i==8 reads exactly the canary planted past the ndesc array, last-element case reads outside every fwdma segment, compounding ST2 db[i+1] read live; world-readable dmesg disclosure; fix-validated exact loop bounds]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2301 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 3.3 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 | β | 658 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 | |
| prep_guest.sh | β | 1.8 KB | view raw | |
| run.log | β | 2.8 KB | view raw | |
| run.sh | β | 104 B | view raw | |
| run_fixed.log | β | 3.8 KB | view raw | |
| run_fixed.sh | β | 1.9 KB | view raw | |
| verdict.json | β | 3.8 KB | view raw |
DF-2301 β print_db off-by-one reads one descriptor past the DMA block
What this is
Verification pack for finding DF-2301 (sys/bus/firewire/fwohci.c:2347).
Low / OOB read + console info-leak. Claim: print_db() loops
for (i = 0; i <= max; i++) with max = dbch->ndesc (dump_db,
fwohci.c:2311) over a descriptor block that has exactly ndesc entries,
so row i == ndesc reads db[ndesc] β one struct fwohcidb (16 bytes)
past the block; the ST2 case additionally reads db[i+1] (fwohci.c:2379)
and double-increments (fwohci.c:2400).
Verdict (short)
REPRODUCED on the verbatim-interposition harness, with byte-exact attribution, triggered by an operator-group user via DUMPDMA:
- ch=0 (atrq, ndesc=8, ndb=64, real fwohci_db_init allocation):
print_dbprinted 9 rows (i=0..8). Row i=8 readdb[8]β the 9th descriptor of an 8-descriptor block: - in-allocation case (element 5): row 8 printed EXACTLY the
canary the rig planted in element 6 db[0]
{cafe0001,11111111,22222222,33333333}(KVA 0xfffff80117c14300, confirmed by the rig KVA peek printing the same 4 words). Because the canary cmd word happens to carry key bits ST2, print_db then ALSO took the fwohci.c:2377 branch and printeddb[9]β the compoundingdb[i+1]OOB read, live. - past-the-allocation case (element 63 = last element of the last
4096-byte fwdma segment): row 8 read the 16 bytes at
seg1.v_addr + 4096β OUTSIDE every fwdma segment β and printed them (OUTM ST0 ... 00000000), matching the rig KVA peek of the same address.
Rows go to the console/dmesg (kprintf, fwohci.c:2352), which is world-readable via dmesg on default installs -> kernel/DMA memory disclosure; on a differently-shaped allocation the same read can fault (latent panic).
Rig
Same fwohci.c TU harness as DF-2300; root prepares atrq via the REAL fwohci_db_init (ndesc=8, ndb=64 -> esize=128, 2 fwdma segments of 4096B, 32 elements each), fabricates the descriptors of elements 5 and 63 (INPUT_MORE | KEY_ST3 | INTR_TRUE, no early-exit conditions so the loop runs to completion) plus the canary in element 6, sets the ATQ command-pointer register in the emulated MMIO, and then opuser issues DUMPDMA(0): real fwohci_ioctl (fwohci.c:371) -> dump_dma -> dump_db (fwohci.c:2305) -> print_db (fwohci.c:2322).
Fix
fix.diff: loop bound i < max and the ST2 immediate print guarded by
i + 1 < max. A/B validated: patched kernel prints exactly ndesc rows,
no canary row, no row past the allocation.
Reachability honesty
DUMPDMA requires only the operator group (same 0660 node); the leak is console/dmesg-visible. The OOB is a READ only β Low severity as filed.
DF-2301 VERDICT β print_db off-by-one (fwohci.c:2347)
- finding: DF-2301, fwohci.c:2347 (
for (i = 0; i <= max; i++)in print_db, fwohci.c:2322-2404; caller dump_db fwohci.c:2311 passes max = dbch->ndesc) - status: reproduced (OOB read of one 16-byte descriptor past the ndesc-entry block, plus the compounding db[i+1] ST2 read, printed to console by an operator-group-triggered DUMPDMA)
- impact: leak (console/dmesg disclosure of the out-of-bounds 16 bytes; latent fault if the read lands on an unmapped page β not realizable on x86_64 direct-map in this configuration)
- confidence: certain
Mechanical proof (baseline kernel FWO #2, run.log)
Setup (root, via rig): real fwohci_db_init on atrq with ndesc=8,
ndb=64, psize=4096 -> fwdma_malloc_multiseg allocates 2 segments of
ssize=4096, esize=128, 32 elements/segment (rig prints the layout:
seg0=0xfffff80117c14000 seglast=0xfffff80117c15000).
Descriptors of element 5 and 63 fabricated as INPUT_MORE|KEY_ST3|
INTERRUPT_TRUE (no DEVICE key, no BRANCH_ALWAYS, no OUTPUT_LAST/
INPUT_LAST -> print_db runs to loop exhaustion); canary planted in
element 6 db[0] = {0xcafe0001, 0x11111111, 0x22222222, 0x33333333}.
ATQ command-pointer register (emulated MMIO 0x18c) set to the target
element bus_addr so dump_db's match (fwohci.c:2287) selects it.
opuser -> DUMPDMA(0):
- element-5 match: 9 rows. Row 8 =
030a5280 ST2 ALL ALL 1 11111111 22222222 3333:3333β exactly the canary at 0xfffff80117c14300 (= element5 + esize = &db[8]), byte-for-byte what the rig KVA peek returned for that address. The canary cmd key bits also triggered the ST2 branch (fwohci.c:2377): the line0x00000000 0x00000000 0x00000000 0x00000000is db[9] β the second, compounding OOB (fwohci.c:2379), after which i++ at fwohci.c:2400 skips the loop past the bound. - element-63 match (last element of the last segment): row 8 =
030a6f80 OUTM ST0 NEV NEV 0 00000000 00000000 0000:0000, reading at 0xfffff80117c16000 = seglast.va + 4096 β one full descriptor block beyond the second (last) bus_dma segment allocation; rig KVA peek of that address returns the same zeros.
Both DUMPDMA calls returned rc=0 for the operator-group user; output lands in dmesg/console (kprintf at fwohci.c:2352).
Why row 8 exists
i <= max iterates ndesc+1 times over db[0..ndesc] while the block
(fwdma element) holds ndesc entries; there is no NULL or bounds guard.
Fix validation (A/B)
fix.diff: for (i = 0; i < max; i++) + if (key == OHCI_KEY_ST2 &&
i + 1 < max) for the db[i+1] print. Fixed kernel: same prep, opuser
DUMPDMA (and root DUMPDMA, since the DF-2300 fix in the combined
patch also gates DUMPDMA - validated as root) prints exactly 8 rows,
no canary row, no ninth row. See run_fixed.log.
Notes
- The disclosure surface is the console/dmesg (kernel-side print), not a copyout; severity Low as filed.
- The ST2 compounding read (db[i+1], fwohci.c:2379) can reach up to db[ndesc+1] when the last valid descriptor carries KEY_ST2 β the fix guards both.
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: identical prep and DUMPDMA print exactly ndesc=8 rows with no ninth row, no canary row, and no ST2 compounding line (run_fixed.log). Note the combined fix also gates DUMPDMA behind the privilege check, so the A/B dump was issued by root; opuser DUMPDMA produced no print_db output at all on the patched kernel.
run_fixed.log DF-2301 section: opuser DUMPDMA prints nothing (gate), root DUMPDMA prints 8 rows ending at i==7 (aa000007/bb000007) with no further row
Confirmed kernel references
Detail
Exploit chain
operator-group user -> DUMPDMA(0) on /dev/fw0.0 -> fwohci_ioctl (fwohci.c:371) -> dump_db (max=ndesc, fwohci.c:2311) -> print_db loop i<=max (fwohci.c:2347) reads db[ndesc] (and db[i+1] for ST2 keys) -> 16+ bytes past the descriptor block printed to console/dmesg
Evidence (decisive lines)
run.log: two print_db dumps with 9 rows each; element-5 dump row 8 = canary values matching CTL_KREAD of &db[8]; element-63 dump row 8 = values at seg1+4096 matching CTL_KREAD of the same out-of-allocation address; run_fixed.log: patched kernel prints exactly 8 rows, no OOB row
PoC changes
Rig prep (fwohci_harness.c FWOIOC_DBINIT/DESCSET/REGWR/KREAD): real fwohci_db_init on atrq with ndesc=8 ndb=64 (2 fwdma segments, 32 elements each), fabricated INPUT_MORE|KEY_ST3 descriptors that avoid all early-exit conditions so the loop runs to exhaustion, canary in element 6, ATQ command-pointer register set in emulated MMIO so dump_db matches the chosen element; trigger issued by opuser as /home/opuser/fwpoc dumpdma 0.
Verified recommended fix
Loop bound i<max plus guard the ST2 immediate print with i+1<max (fix.diff)
Verdict
print_db (fwohci.c:2322-2404) driven by an operator-group DUMPDMA(0) through the real fwohci_ioctl/dump_dma/dump_db path printed ndesc+1 = 9 rows for an 8-descriptor block: row i==8 read db[8], one 16-byte struct fwohcidb past the block. Attribution is byte-exact: (a) with the match on element 5, row 8 printed exactly the canary planted in element 6 db[0] (KVA 0xfffff80117c14300, rig KVA peek returns the same cafe0001/11111111/22222222/33333333), and the canary cmd key bits also fired the compounding db[i+1] ST2 read (fwohci.c:2377-2382, printing db[9]); (b) with the match on element 63 (last element of the last 4096-byte fwdma segment), row 8 read at seglast.v_addr+4096 - outside every segment allocation - matching the KVA peek of that address. Output goes to console/dmesg (world-readable): kernel DMA-memory disclosure; latent fault if the read ever lands unmapped.
No comments yet.