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

Divide-by-zero in fwdma_malloc_multiseg when esize is zero

Summary

fwdma_malloc_multiseg computes ssize=rounddown(PAGE_SIZE esize) without checking esize==0. rounddown macro expands to ((PAGE_SIZE)/(esize))*(esize) causing integer divide-by-zero (CPU #DE -> kernel panic) when esize is zero. Caller fwdev_allocbuf passes b->psize as esize after roundup2(b->psize 4) and roundup2(0 4)==0 so user-supplied psize=0 reaches this division. Same bug chain as DF-2293 (filed against fwdev.c); the division itself executes in fwdma.c:159. Second div-by-zero path: psize>=0x80000001 cast to int esize negative making rounddown(4096 negative) yield ssize=0 then howmany(n 0/esize) divides by zero at :160.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2297 Β· 20 files
FileTypeDescriptionSize
df229x.c β€” 19.9 KB view raw
Makefile β€” 1.8 KB ↓ download
shim/ β€” 4.0 KB ↓ download
build.sh β€” 208 B view raw
run_bootA.sh β€” 803 B view raw
run_bootB.sh β€” 467 B view raw
run_fix.sh β€” 1.1 KB view raw
run.log β€” 871 B view raw
panic.txt β€” 712 B view raw
run_variantB.log β€” 885 B view raw
panic_psize80000000.txt β€” 885 B view raw
fix_run.log β€” 2.6 KB view raw
baseline_build.log β€” 7.0 KB view raw
fix_build.log β€” 7.0 KB view raw
env.txt β€” 2.0 KB view raw
fix.diff β€” 1.4 KB view raw
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
manifest.json β€” 1.7 KB view raw
verdict.json β€” 5.1 KB view raw

DF-2297 β€” Divide-by-zero in fwdma_malloc_multiseg when esize is zero (or negative)

File: sys/bus/firewire/fwdma.c:145-161 (crash sites: 159 and 160) Verdict: REPRODUCED β€” Fatal trap 18: integer divide fault while in kernel mode, twice (both variants), guest dead in DDB; fix validated.

Claim

FW_SSTBUF (fwdev.c:504-506) bcopy()s a struct fw_isobufreq (three userspace unsigned ints per direction) into fw_drv1.bufreq with no validation. FW_STSTREAM/FW_SRSTREAM (fwdev.c:458/486) pass b->psize as esize to fwdma_malloc_multiseg() after only roundup2(b->psize, 4) (fwdev.c:107-109) β€” and roundup2(0,4)==0.

  • psize = 0 β†’ esize = 0 β†’ ssize = rounddown(PAGE_SIZE, 0) = (4096/0)*0 β†’ #DE at fwdma.c:159.
  • psize = 0x80000000 β†’ esize = (int)0x80000000 = INT_MIN β†’ esize > PAGE_SIZE false β†’ ssize = (4096/INT_MIN)*INT_MIN = 0 β†’ nseg = howmany(n, ssize/esize) = howmany(n, 0) = (n-1)/0 β†’ #DE at fwdma.c:160.

The pass-2 audit did not re-file this (same crash site as the pass-1 finding, noted in DF-5613's verdict); this run confirms the claim is reachable today on unmodified production code.

How to build / run (no FireWire hardware needed β€” KLD harness)

The harness compiles the unmodified production firewire.c + fwdma.c + fwdev.c into df229x.ko (globals -D-renamed, DF-5613/DF-5413 pattern) and drives the real fw_ioctl path with the synthetic fc/cdev state an fwohci controller + devfs would create.

# guest, root, fresh `vm.sh reset with-src`
cd /root/df229x && sh build.sh          # -> df229x.ko   (baseline_build.log)
kldload ./df229x.ko
sysctl kern.df229x_test=3               # psize=0        -> panic (run.log)
# after `vm.sh reset with-src` + redeploy + rebuild:
sysctl kern.df229x_test=1               # psize=0x80000000 -> panic (run_variantB.log)

Expected (baseline, reproduced β€” both variants)

Fatal trap 18: integer divide fault while in kernel mode
...
Stopped at      dfw_dma_malloc_multiseg+0x139:  idivl   %esi,%eax     # psize=0
Stopped at      dfw_dma_malloc_multiseg+0x153:  divq    %rsi,%eax     # psize=0x80000000

(fwdma_malloc_multiseg is fwdma.c code renamed into the module; the two distinct faulting instructions confirm the two distinct division sites, fwdma.c:159 rounddown(PAGE_SIZE, esize) and fwdma.c:160 howmany(n, ssize/esize).) The ioctl is reachable from any user with the /dev/fwU.S node open (0660 root:operator).

Expected (fix.diff applied to /usr/src, module rebuilt β€” fix_run.log)

kern.df229x_status: DF-2297 t3 psize=0x0:          STSTREAM err=22, no trap
kern.df229x_status: DF-2297 t1 psize=0x80000000:   STSTREAM err=22, no trap
kern.df229x_status: guard: esize=0 -> 0, esize<0 -> 0, n=0 -> 0 (no trap)
kern.df229x_status: regression OK: legit bufspec still works

Files

df229x.c (harness; tests 1/3 are this finding), Makefile + shim/ (production includes), build.sh, run_bootA.sh / run_bootB.sh / run_fix.sh, run.log + panic.txt (psize=0), run_variantB.log + panic_psize80000000.txt (INT_MIN variant), fix_run.log, baseline_build.log / fix_build.log, env.txt, fix.diff, VERDICT.md, manifest.json, verdict.json

VERDICT.md
↓ download raw

DF-2297 β€” VERDICT

Status: REPRODUCED (impact: panic β€” kernel-mode integer-divide fault from an ioctl-reachable, unvalidated userspace value). Confidence: certain.

Reachability trace (production code, this tree, today)

  1. fwdev.c:504-506 β€” case FW_SSTBUF: bcopy(ibufreq, &d->bufreq, sizeof(d->bufreq)): the whole struct fw_isobufreq (firewire.h:50-56; unsigned int nchunk, npacket, psize per direction) is copied from the ioctl buffer with no validation. Any open of /dev/fwU.S (0660 root:operator, fwdev.c:173-175) can set arbitrary ints.
  2. fwdev.c:447-461 β€” case FW_STSTREAM: finds a free it queue and calls fwdev_allocbuf(fc, it, &d->bufreq.tx).
  3. fwdev.c:107 β€” b->psize = roundup2(b->psize, sizeof(u_int32_t)): roundup2(0,4) == 0; roundup2(0x80000000,4) == 0x80000000.
  4. fwdev.c:108-109 β€” q->buf = fwdma_malloc_multiseg(fc, sizeof(u_int32_t), b->psize, b->nchunk * b->npacket, BUS_DMA_WAITOK): psize reaches fwdma.c as int esize verbatim.
  5. fwdma.c:153-161 β€” no esize validation: * esize == 0: esize > PAGE_SIZE false β†’ ssize = rounddown(PAGE_SIZE, esize); rounddown(x,y) is (((x)/(y))*(y)) (sys/param.h:400) β†’ 4096 / 0 β†’ integer-divide fault. * esize == INT_MIN (psize β‰₯ 0x80000000): branch still false β†’ ssize = (4096/INT_MIN)*INT_MIN = 0 (no trap: divisor β‰  0) β†’ nseg = howmany(n, ssize/esize); ssize is bus_size_t (u64), so 0 / (u64)INT_MIN = 0, then howmany(n, 0) = ((n)+(0)-1))/(0) (sys/param.h:396) β†’ / 0 β†’ integer-divide fault.

What was run

KLD harness df229x.ko (this pack) with the unmodified production firewire.c + fwdma.c + fwdev.c #included (DF-5613 pattern), synthetic fc/queue/cdev state, driving the real fw_ioctl:

  • test 3 (psize=0): Fatal trap 18: integer divide fault while in kernel mode … Stopped at dfw_dma_malloc_multiseg+0x139: idivl %esi,%eax (panic.txt / run.log). Guest dead in DDB (db>), ssh hung mid-sysctl β€” the exact "device dies on ioctl" DoS.
  • test 1 (psize=0x80000000, the negative-esize variant named in the finding summary): same fatal trap at dfw_dma_malloc_multiseg+0x153: divq %rsi,%eax (panic_psize80000000.txt / run_variantB.log) β€” the other division site, confirming both cited lines trap.

The two distinct faulting instructions map 1:1 to the two cited lines: idivl (32-bit PAGE_SIZE/esize) = fwdma.c:159; divq (64-bit ssize/esize after integer promotion of ssize) = fwdma.c:160.

Reconciliation with the pass-2 negative note

Pass 2 did not claim unreachability β€” it declined to re-file because the negative-esize path is "the same crash site/class as DF-2297". Verified: both variants are reachable today from FW_SSTBUF on unmodified production code; both trap fatally. Not a false positive, not already fixed.

Fix validation (fix.diff, applied to guest /usr/src, module rebuilt)

  • psize=0 β†’ STSTREAM err=22 (EINVAL), no trap β€” fwdev_allocbuf now rejects psize==0 before the call (fwdev.c hunk).
  • psize=0x80000000 β†’ err=22, no trap (rejected by the psize > 0x7fffffff arm).
  • Direct guard probe (fwdma_malloc_multiseg(esize=0 / -4096 / n=0)) β†’ NULL for all three, no trap (fwdma.c hunk is the hard boundary).
  • Regression: legit {nchunk=4, npacket=8, psize=2048} still allocates and streams (STSTREAM=0 fw_write=0).
  • Guest alive after the full suite (fix_run.log).

Impact

Unprivileged-but-device-authorized local user (operator group) β†’ instant kernel panic of the whole machine, on demand, via one ioctl pair. DoS only β€” no memory corruption on this path (the trap fires before any allocation is used). Severity High stands (CVSS AV:L/AC:L/PR:L/A:H as filed).

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 (fwdma.c esize/n guard + fwdev.c fwdev_allocbuf validation) applied to the guest /usr/src; harness module rebuilt from the patched production sources. Baseline: both variants = Fatal trap 18, guest dead in DDB. Patched: psize=0 and psize=0x80000000 both return err=22 (EINVAL) with no trap; direct fwdma_malloc_multiseg(esize=0/-4096/n=0) probes return NULL; the legit {4,8,2048} config still allocates and streams; guest alive after the full suite.

fix_run.log (full patched suite: t3/t1 err=22 no trap, t4/t2 rejected, t5 regression OK, t6 guard NULLs, 'guest still alive'), fix_build.log (patched build), fix.diff (git apply --check verified against the repo sys/ tree; patch(1) applied cleanly to guest /usr/src)
↓ fix.diffper-fix-DF-2297

Confirmed kernel references

Detail

Exploit chain

open /dev/fw0.0 (operator group) -> ioctl(FW_SSTBUF, {psize=0 or >=0x80000000}) -> ioctl(FW_STSTREAM) -> fwdev_allocbuf -> fwdma_malloc_multiseg(esize=0|INT_MIN) -> kernel-mode integer-divide fault -> immediate system-wide panic/DoS. No memory-corruption escalation on this path (trap precedes any allocation use).

Evidence (decisive lines)

panic.txt (psize=0: 'Fatal trap 18: integer divide fault while in kernel mode ... Stopped at dfw_dma_malloc_multiseg+0x139: idivl %esi,%eax'), panic_psize80000000.txt + run_variantB.log (INT_MIN variant: '+0x153: divq %rsi,%eax' -- the second, distinct division site), run.log (baseline trail: SSTBUF err=0 then the trap), fix_run.log (patched: 'DF-2297 t3 psize=0x0: STSTREAM err=22, no trap', 't1 psize=0x80000000: err=22', 'guard: esize=0 -> 0, esize<0 -> 0, n=0 -> 0 (no trap)', 'regression OK'), baseline_build.log / fix_build.log (clean builds of unmodified/patched production sources), env.txt

PoC changes

No seed existed (poc_path was empty). Harness df229x.c written fresh on the DF-5613/DF-5413 KLD pattern (production firewire.c+fwdma.c+fwdev.c #included with -D-renamed globals; synthetic fc/queues/cdevs): tests 1/3 fire the production SSTBUF+STSTREAM pair with psize=0x80000000/0; test 6 probes the fwdma guard directly; test 5 is a legit-config regression guard for fix validation.

Verified recommended fix

Validate in fwdma_malloc_multiseg (hard boundary: reject esize<=0 || n<=0 -> NULL) and in fwdev_allocbuf (EINVAL for psize==0, psize>0x7fffffff, nchunk==0, npacket==0, or npacket > 0x7fffffff/nchunk) -- fix.diff in this pack.

Verdict

REPRODUCED on the QEMU guest with the unmodified production firewire.c+fwdma.c+fwdev.c compiled into KLD df229x.ko (DF-5613 pattern), driving the real FW_SSTBUF/FW_STSTREAM ioctl path. FW_SSTBUF (fwdev.c:504-506) copies userspace ints with no validation; roundup2(psize,4) leaves 0 as 0 and 0x80000000 as 0x80000000; fwdev.c:108-109 passes psize as esize to fwdma_malloc_multiseg, which divides by it with no check. psize=0 -> Fatal trap 18 (integer divide fault), Stopped at dfw_dma_malloc_multiseg+0x139: idivl (fwdma.c:159 rounddown(PAGE_SIZE,0)); psize=0x80000000 (INT_MIN esize -> ssize=0) -> same fatal trap at +0x153: divq (fwdma.c:160 howmany(n, ssize/esize=0)). Both variants killed the guest in DDB on first fire. Reachable today from any user with /dev/fwU.S open (0660 root:operator). Pass-2 had merely declined to re-file (same crash site), not claimed unreachability -- this run confirms the claim on current production code.