FW_SSTBUF -> 32-bit nchunk*npacket wrap in fwdev_allocbuf -> 0-segment fwdma buffer -> fwdma_v_addr() OOB seg[0] -> controlled kernel write (and read) via production fw_write/fw_read
Summary
FW_SSTBUF copies the fully user-controlled bufspec with no validation and nchunk*npacket wraps as 32-bit (0x10000*0x10000=0): a 0-segment 32-byte fwdma_alloc_multi is returned while bnchunk/bnpacket stay 0x10000 - fw_write computes the packet pointer via fwdma_v_addr which reads seg[0].v_addr 16 bytes past the allocation and uiomoves attacker bytes there; fw_read is the read twin. Guest-proven with production code: the OOB seg[0].v_addr was heap-groomed to a chosen kernel address and production fw_write delivered header+32-byte chosen payload, err=0, guest alive - a write-what-where for operator-group users on any FW-equipped machine, no bus traffic needed. Validate-bufreq fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5413 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| df5413.c | β | 21.5 KB | view raw | |
| Makefile | β | 1.8 KB | β download | |
| shim/ | β | 4.0 KB | β download | |
| build.sh | β | 103 B | view raw | |
| run.sh | β | 145 B | view raw | |
| build.log | β | 7.0 KB | view raw | |
| run.log | β | 2.0 KB | view raw | |
| fix.diff | β | 3.4 KB | view raw | |
| fix_build.log | β | 7.0 KB | view raw | |
| fix_run.log | β | 2.6 KB | view raw | |
| env.txt | β | 615 B | view raw | |
| verdict.json | β | 5.6 KB | view raw |
DF-5413 β FW_SSTBUF β 32-bit nchunk*npacket wrap β 0-segment DMA array β fwdma_v_addr() OOB β controlled kernel write via fw_write
File: sys/bus/firewire/fwdev.c Β· Severity: High Β· Bucket: memcorrupt Β· Confidence: certain
Status: reproduced on the QEMU guest (KLD harness with the production
firewire.c+fwdma.c+fwdev.c compiled in; DF-5302/DF-5321 evidence class β
no FireWire controller exists in QEMU, so the /dev/fwN.N entry point is
hardware-gated, but the vulnerable code itself was executed end-to-end).
Root cause (production path:line)
FW_SSTBUF(fwdev.c:504-506) copies the user'sstruct fw_isobufreq(nchunk,npacket,psizeβ allunsigned int, no validation at all) intod->bufreq.FW_STSTREAM/FW_SRSTREAMcallfwdev_allocbuf()(fwdev.c:458/486) which passesb->nchunk * b->npackettofwdma_malloc_multiseg()(fwdev.c:108-109). The multiply is 32-bit:0x10000 * 0x10000wraps to 0, sofwdma_malloc_multiseg()(fwdma.c:160,nseg = howmany(0, β¦) = 0) returns a 0-segmentstruct fwdma_alloc_multiβ a bare 32-bytekmalloc(fwdma.c:162-163) with noseg[]at all.q->bnchunk/q->bnpacketare nevertheless set to the attacker's huge values (fwdev.c:116-117) and the bulkxfer/stfree machinery is built.fw_write()(fwdev.c:388-392) computes the packet address withfwdma_v_addr(it->buf, stproc->poffset + queued)βfwdma.h:60-66indexesam->seg[0]16 bytes past the 32-byte allocation and uses the out-of-boundsseg[0].v_addras the destination pointer for twouiomove()s of fully user-controlled bytes (4-byte stream header +stream.lenpayload).fw_read()(fwdev.c:321-330) is the read-side twin: the same OOBseg[]lookup becomes an arbitrary-read/leak primitive once the DMA engine fills buffers.
Demonstration (guest, module df5413.ko, test 1)
- production
fw_ioctl(FW_SSTBUF {nchunk=0x10000, npacket=0x10000, psize=2048}) - production
fw_ioctl(FW_STSTREAM)β allocates the 0-segment buffer - the 32-byte heap slot following
amis groomed to contain the address of a module-global marker (kmallocadjacency pair trick,df5413.cbank code) - production
fw_write()β result:
df5413: &am->seg[0]=0xfffff8004f1026e0 (allocation is 32 bytes) -- seg[0] is OUT OF BOUNDS df5413: OOB-read seg[0].v_addr = 0xffffffff82606180 (want 0xffffffff82606180 == GROOMED) df5413: fw_write err=0 resid=0 marker after = 343546440020abcd 52544e4f432d3331 454b2d44454c4c4f df5413: payload="13-CONTROLLED-KERNEL-WRITE!!..."
i.e. a chosen kernel address received chosen attacker bytes through
100% production code: header 0x0020ABCD at V+0, then the 32-byte
payload at V+4 (fp->mode.stream.payload), exactly as fw_write would
for a real transmit. On the standard snapshot (no SMAP/SMEP/KASLR) this is
a direct path to uid=0 for anyone who can open /dev/fwN.N (mode 0660,
group operator): overwrite e.g. curthread->td_proc->p_ucred fields or any
function pointer reached from the same write.
What to run
./build.sh # compiles production firewire/fwdma/fwdev into df5413.ko sh run.sh # kern.df5413_test=1 -> marker before/after proof
Fix
fix.diff (shared with DF-5414/5415/5416 packs) validates the bufreq in
fwdev_allocbuf(). Validated: with fix.diff applied to the guest
/usr/src and the module rebuilt, the same trigger returns EINVAL
(STSTREAM err=22) and no write occurs (fix_run.log).
DF-5413 β VERDICT
reproduced Β· impact: arbitrary kernel write (memcorrupt; privesc-class primitive) Β· confidence: certain
What ran
The guest (DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, no FireWire hardware)
compiled the unmodified production /usr/src/sys/bus/firewire/firewire.c,
fwdma.c and fwdev.c into KLD df5413.ko (globals -D-renamed, guard
shims for the unguarded firewire headers β firewirereg.h, fwdma.h,
fwmem.h, iec68113.h, firewire_phy.h have no include guards). The
harness drives the production entry points with exactly the state devfs +
fwohci_attach would create (fw_drv1 via a fake cdev si_drv1,
firewire_comm with fc->it[]/ir[], dmach, nisodma, dmat,
irx_enable stub; a fake devclass/device pair makes
devclass_get_softc(firewire_devclass, 0) return the synthetic softc the
way an attached card would).
The chain that ran (all production code)
fw_ioctl(FW_SSTBUF, {tx={0x10000,0x10000,2048}})βfwdev.c:504-506bcopy of the user struct, no validation.fw_ioctl(FW_STSTREAM)βfwdev.c:447-467scansfc->it[], callsfwdev_allocbuf()(fwdev.c:95-137): *fwdev.c:109b->nchunk * b->npacketβ0x10000 * 0x10000wraps to 0 in 32 bits βfwdma_malloc_multiseg(fc, 4, 2048, 0, β¦)(fwdma.c:145-205) computesnseg = howmany(0, 2) = 0and returns a 32-bytefwdma_alloc_multiwith zero segments (kmalloc(32 + 0),fwdma.c:162-163); *fwdev.c:116-117setsbnchunk = bnpacket = 0x10000anyway.- Heap groom (harness-only): a bank of 64Γ32-byte
M_FWchunks is allocated, two adjacent slots are found, freed in LIFO order so that the productionkmallocinsidefwdma_malloc_multisegpops the lower slot foramand the harness's follow-upkmallocpops the upper slot, which is filled withV = &df5413_marker[0]. fw_write()(fwdev.c:348-406) β production:fp = fwdma_v_addr(buf, 0)(fwdev.c:388-389,fwdma.h:60-66) readsam->seg[0].v_addrfromam+40β 16 bytes past the 32-byte allocation β and returns the groomedV;uiomove(fp, 4)thenuiomove(fp->mode.stream.payload, fp->mode.stream.len)write the user's 4+32 bytes atV.
Decisive output (run.log)
df5413: attempt 0 bank ... pair=9
df5413: &am->seg[0]=0xfffff8004f1026e0 (allocation is 32 bytes) -- seg[0] is OUT OF BOUNDS
df5413: OOB-read seg[0].v_addr = 0xffffffff82606180 (want 0xffffffff82606180 == GROOMED)
df5413: fw_write err=0 resid=0 marker after = 343546440020abcd 52544e4f432d3331 454b2d44454c4c4f
df5413: payload="13-CONTROLLED-KERNEL-WRITE!!..."
df5413: DF-5413 REPRODUCED: controlled kernel write (header 0x0020ABCD + 32 payload
bytes) via OOB seg[0].v_addr to 0xffffffff82606180
0x343546440020abcd little-endian = cd ab 20 00 44 46 35 34 β the
4-byte header 0x0020ABCD followed by "DF54β¦", i.e. the payload starts at
V+4 exactly where fp->mode.stream.payload points. Bytes, address, and
length are all attacker-chosen; the write went through with err=0 and the
guest stayed up (corruption is silent β ideal for exploitation).
Why this is a userβroot primitive (not just a crash)
- The value read OOB (
seg[0].v_addr) is fully controllable by heap grooming (demonstrated); the data written is fully controlled (the uio bytes);fw_writeloops over the uio in packet-sized steps, giving a repeatable write-what-where perwrite(2)call. - The only reason the PoC stops at a module symbol is that the guest has no
FireWire controller, so the unprivileged
/dev/fwN.Nentry point cannot exist. On real hardware the exact same production path is reachable by any user in groupoperator(device mode 0660,fwdev.c:173-175/ FreeBSD-conditional clone path aside, DragonFly creates the nodes on attach), with no bus traffic required for the write direction. - With the write primitive,
uid=0on this class of guest (no KASLR/SMAP/ SMEP) is mechanical: overwrite the caller'sp_ucred(uid/gid fields) or acred-adjacent target, thenexecve("/bin/sh").
Fix validation
fix.diff (in this pack) adds bufreq validation to fwdev_allocbuf()
(nchunk/npacket/psize sane, product can no longer wrap). Applied to the
guest /usr/src, module rebuilt from the patched production source:
df5413: attempt 0 STSTREAM err=22 (EINVAL β request rejected) df5413: attempt 1 STSTREAM err=22 β¦ (no allocation, no fw_write, no write)
Guest alive after all tests (fix_run.log). Baseline vs patched:
controlled write present β gone.
Notes / deviations
- The harness's
fw_ioctlcalls go through the production dispatcher; the only synthetic state is the controller/softc scaffolding (fwohci_attach's job) and the heap groom. - Bring-up oddity (not a finding): adding an extra
DECLARE_MODULEin the same translation unit corrupts the modmetadata linker set on this kernel and panicsmodule_register()β the module therefore uses the included firewire.cDRIVER_MODULEas its only module entry (DF-5321 pattern).
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
fixedBaseline: production fw_write lands header 0x0020ABCD + 32 chosen payload bytes at the groomed kernel address (marker before 0 / after 0x343546440020abcd...). Patched (fix.diff on /usr/src, module rebuilt against patched production source, identical trigger): FW_STSTREAM returns EINVAL (err=22) on every attempt, no allocation, no write, guest alive after the full test battery.
fix_run.log (STSTREAM err=22 lines, no marker change), fix_build.log, fix.diff
Confirmed kernel references
- sys/bus/firewire/fwdev.c:504
- sys/bus/firewire/fwdev.c:506
- sys/bus/firewire/fwdev.c:108
- sys/bus/firewire/fwdev.c:109
- sys/bus/firewire/fwdev.c:116
- sys/bus/firewire/fwdev.c:117
- sys/bus/firewire/fwdev.c:388
- sys/bus/firewire/fwdev.c:390
- sys/bus/firewire/fwdev.c:391
- sys/bus/firewire/fwdev.c:392
- sys/bus/firewire/fwdev.c:321
- sys/bus/firewire/fwdev.c:329
- sys/bus/firewire/fwdma.c:160
- sys/bus/firewire/fwdma.c:162
- sys/bus/firewire/fwdma.h:60
- sys/bus/firewire/fwdma.h:63
- sys/bus/firewire/fwdma.h:65
Detail
Exploit chain
open /dev/fwN.M (operator group) -> ioctl FW_SSTBUF {nchunk=0x10000, npacket=0x10000, psize=2048} -> ioctl FW_STSTREAM (fwdev_allocbuf: 32-bit nchunk*npacket wraps to 0, 0-segment fwdma_alloc_multi allocated, bnchunk/bnpacket=0x10000) -> groom the 32-byte heap slot following am to contain the target kernel address -> write(2) -> production fw_write uiomove()s a 4-byte header + stream.len payload through the OOB-read seg[0].v_addr to the target address -> repeatable write-what-where with chosen bytes -> overwrite p_ucred (or any function pointer) -> uid=0.
Evidence (decisive lines)
run.log (GROOMED / marker before=0000... after=343546440020abcd + payload '13-CONTROLLED-KERNEL-WRITE!!' / status line 'DF-5413 REPRODUCED'), dmesg lines in run.log (&am->seg[0] = am+32 OUT OF BOUNDS, OOB-read seg[0].v_addr == GROOMED), fix_run.log (STSTREAM err=22 on all attempts, no write, guest alive), fix.diff, build.log, fix_build.log
PoC changes
Harness written fresh (DF-5302/DF-5321 KLD pattern extended to fwdma.c+fwdev.c): #include of the production files with -D-renamed globals (full exported-symbol list from nm /boot/kernel/kernel), include-guard shims for the unguarded firewire headers, fake devclass/device so production fw_ioctl/fw_close get their softc, fake cdev si_drv1 for the fw_drv1 devfs would create. Critical fixes during bring-up: (1) an extra DECLARE_MODULE in the same TU corrupts modmetadata on this kernel and panics module_register - removed, module entry is firewire.c's DRIVER_MODULE; (2) the kernel zeroes freed 32B chunks, so pre-spray grooming read zeros - replaced with an adjacency-pair groom (alloc bank, find hold[j+1]-hold[j]==32, free in LIFO order so fwdev_allocbuf's kmalloc pops the lower slot and the harness pops the upper one and fills it with the target address).
Verified recommended fix
Validate FW_SSTBUF bufreq in fwdev_allocbuf(): nchunk/npacket/psize nonzero and bounded (e.g. nchunk<=4096, npacket<=512, psize<=FWPMAX_S400) so the 32-bit nchunk*npacket product can no longer wrap and index seg[] out of bounds; optionally add an am->nseg bounds check in fwdma_v_addr().
Verdict
Reproduced on the QEMU guest with the production firewire.c+fwdma.c+fwdev.c compiled into KLD df5413.ko: FW_SSTBUF values nchunk=npacket=0x10000 wrap the 32-bit product at fwdev.c:109 to zero, so fwdma_malloc_multiseg() returns a 0-segment (32-byte) fwdma_alloc_multi while bnchunk/bnpacket stay 0x10000; production fw_write() then computes its DMA pointer with fwdma_v_addr() (fwdma.h:60-66), which reads am->seg[0].v_addr 16 bytes PAST the allocation and uiomoves attacker bytes to it. With the adjacent 32-byte heap slot groomed to hold the address of a module-global marker, the production code wrote header 0x0020ABCD + a 32-byte chosen payload to the chosen kernel address (err=0, guest stayed up): a demonstrated write-what-where. The unprivileged /dev/fwN.M entry point is hardware-gated in QEMU (no OHCI controller, no device node), so the final uid=0 step was not literally executed; on real hardware the identical production path is open to group operator (0660) and the write primitive makes cred overwrite mechanical on this no-SMAP/SMEP/KASLR class. Fix validated: bufreq validation in fwdev_allocbuf rejects the request with EINVAL and no write occurs.
No comments yet.