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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5413 Β· 14 files
FileTypeDescriptionSize
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)

  1. FW_SSTBUF (fwdev.c:504-506) copies the user's struct fw_isobufreq (nchunk, npacket, psize β€” all unsigned int, no validation at all) into d->bufreq.
  2. FW_STSTREAM/FW_SRSTREAM call fwdev_allocbuf() (fwdev.c:458/486) which passes b->nchunk * b->npacket to fwdma_malloc_multiseg() (fwdev.c:108-109). The multiply is 32-bit: 0x10000 * 0x10000 wraps to 0, so fwdma_malloc_multiseg() (fwdma.c:160, nseg = howmany(0, …) = 0) returns a 0-segment struct fwdma_alloc_multi β€” a bare 32-byte kmalloc (fwdma.c:162-163) with no seg[] at all.
  3. q->bnchunk/q->bnpacket are nevertheless set to the attacker's huge values (fwdev.c:116-117) and the bulkxfer/stfree machinery is built.
  4. fw_write() (fwdev.c:388-392) computes the packet address with fwdma_v_addr(it->buf, stproc->poffset + queued) β€” fwdma.h:60-66 indexes am->seg[0] 16 bytes past the 32-byte allocation and uses the out-of-bounds seg[0].v_addr as the destination pointer for two uiomove()s of fully user-controlled bytes (4-byte stream header + stream.len payload).
  5. fw_read() (fwdev.c:321-330) is the read-side twin: the same OOB seg[] 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 am is groomed to contain the address of a module-global marker (kmalloc adjacency pair trick, df5413.c bank 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).

VERDICT.md
↓ download raw

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)

  1. fw_ioctl(FW_SSTBUF, {tx={0x10000,0x10000,2048}}) β€” fwdev.c:504-506 bcopy of the user struct, no validation.
  2. fw_ioctl(FW_STSTREAM) β€” fwdev.c:447-467 scans fc->it[], calls fwdev_allocbuf() (fwdev.c:95-137): * fwdev.c:109 b->nchunk * b->npacket β†’ 0x10000 * 0x10000 wraps to 0 in 32 bits β†’ fwdma_malloc_multiseg(fc, 4, 2048, 0, …) (fwdma.c:145-205) computes nseg = howmany(0, 2) = 0 and returns a 32-byte fwdma_alloc_multi with zero segments (kmalloc(32 + 0), fwdma.c:162-163); * fwdev.c:116-117 sets bnchunk = bnpacket = 0x10000 anyway.
  3. Heap groom (harness-only): a bank of 64Γ—32-byte M_FW chunks is allocated, two adjacent slots are found, freed in LIFO order so that the production kmalloc inside fwdma_malloc_multiseg pops the lower slot for am and the harness's follow-up kmalloc pops the upper slot, which is filled with V = &df5413_marker[0].
  4. fw_write() (fwdev.c:348-406) β€” production: fp = fwdma_v_addr(buf, 0) (fwdev.c:388-389, fwdma.h:60-66) reads am->seg[0].v_addr from am+40 β€” 16 bytes past the 32-byte allocation β€” and returns the groomed V; uiomove(fp, 4) then uiomove(fp->mode.stream.payload, fp->mode.stream.len) write the user's 4+32 bytes at V.

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_write loops over the uio in packet-sized steps, giving a repeatable write-what-where per write(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.N entry point cannot exist. On real hardware the exact same production path is reachable by any user in group operator (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=0 on this class of guest (no KASLR/SMAP/ SMEP) is mechanical: overwrite the caller's p_ucred (uid/gid fields) or a cred-adjacent target, then execve("/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_ioctl calls 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_MODULE in the same translation unit corrupts the modmetadata linker set on this kernel and panics module_register() β€” the module therefore uses the included firewire.c DRIVER_MODULE as 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline: 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
↓ fix.diffper-fix-DF-5413

Confirmed kernel references

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.