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)
PoC verification
Evidence pack
findings/poc/DF-2297 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_SIZEfalse β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
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)
fwdev.c:504-506βcase FW_SSTBUF:bcopy(ibufreq, &d->bufreq, sizeof(d->bufreq)): the wholestruct fw_isobufreq(firewire.h:50-56;unsigned int nchunk, npacket, psizeper 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.fwdev.c:447-461βcase FW_STSTREAM:finds a freeitqueue and callsfwdev_allocbuf(fc, it, &d->bufreq.tx).fwdev.c:107βb->psize = roundup2(b->psize, sizeof(u_int32_t)):roundup2(0,4) == 0;roundup2(0x80000000,4) == 0x80000000.fwdev.c:108-109βq->buf = fwdma_malloc_multiseg(fc, sizeof(u_int32_t), b->psize, b->nchunk * b->npacket, BUS_DMA_WAITOK):psizereachesfwdma.casint esizeverbatim.fwdma.c:153-161β noesizevalidation: *esize == 0:esize > PAGE_SIZEfalse β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);ssizeisbus_size_t(u64), so0 / (u64)INT_MIN = 0, thenhowmany(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-esizevariant named in the finding summary): same fatal trap atdfw_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_allocbufnow rejectspsize==0before the call (fwdev.chunk).psize=0x80000000βerr=22, no trap (rejected by thepsize > 0x7fffffffarm).- Direct guard probe (
fwdma_malloc_multiseg(esize=0 / -4096 / n=0)) βNULLfor all three, no trap (fwdma.chunk 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
fixedfix.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)
Confirmed kernel references
- sys/bus/firewire/fwdma.c:145
- sys/bus/firewire/fwdma.c:153
- sys/bus/firewire/fwdma.c:159
- sys/bus/firewire/fwdma.c:160
- sys/bus/firewire/fwdev.c:107
- sys/bus/firewire/fwdev.c:108
- sys/bus/firewire/fwdev.c:458
- sys/bus/firewire/fwdev.c:486
- sys/bus/firewire/fwdev.c:504
- sys/bus/firewire/firewire.h:50
- sys/sys/param.h:396
- sys/sys/param.h:400
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.
No comments yet.