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

fw_read copies iso stream packet by attacker-controlled stream.len without bounding to psize (DMA buffer over-read)

Summary

In fw_read bulk-iso path uiomove size is fp->mode.stream.len + sizeof(u_int32_t) where stream.len taken directly from received packet header with no upper bound. Packet slot returned by fwdma_v_addr is only ir->psize bytes. Malicious FireWire peer transmitting iso stream packet with stream.len larger than psize-4 causes kernel to copy adjacent DMA-buffer bytes into userspace. For psize=512 and stream.len=1500 over-reads 988 bytes past packet slot. [VERIFIED pass2: reproduced with full attribution - read returns 1504 bytes from 512-byte slot, 62/62 canary windows byte-attributed to adjacent ring slots; clamp fix-validated, legit packet bit-identical]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2295 Β· 22 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 4.7 KB ↓ raw
build.log β€” 211 B view raw
build.sh β€” 111 B view raw
build_kernel.sh β€” 396 B view raw
env.txt β€” 466 B view raw
fix.diff β€” 1.6 KB view raw
fix_run.log β€” 688 B view raw
fwharness.c β€” 18.8 KB view raw
fwharness.h β€” 1.4 KB view raw
fwhctl.c β€” 3.7 KB view raw
leak_attributed.txt β€” 3.0 KB view raw
leak_fixed_508.txt β€” 1.8 KB view raw
leak_legit_508.txt β€” 1.8 KB view raw
leak_sample.txt β€” 5.3 KB view raw
poc_2295.c β€” 5.0 KB view raw
prep_guest.sh β€” 1.2 KB view raw
run.log β€” 1.0 KB view raw
run.sh β€” 90 B view raw
run2295.log β€” 1016 B view raw
run_2295_baseline.log β€” 965 B view raw
verdict.json β€” 3.2 KB view raw

DF-2295 β€” fw_read iso path over-read (stream.len unbounded vs psize)

Verdict (short)

REPRODUCED with full byte attribution on the stock fwdev.o + fwdma.o binaries:

  • Operator-group user sets up a legal iso receive stream (FW_SSTBUF rx=4chunk/8packet/512psize, FW_SRSTREAM ch=1) β€” the DMA ring is allocated by the real fwdma_malloc_multiseg.
  • The harness emulates the malicious FireWire peer: it fills every slot of the real DMA ring with 16-byte canary records [magic|slot|offset|xor] and writes a received stream-packet header with stream.len = 1500 into slot 0 (exactly what OHCI DMA would store; the peer controls len).
  • The user read(2)s 1504 bytes. Stock kernel: uiomove(fp, fp->mode.stream.len + 4, uio) (fwdev.c:329-330) copies 1504 bytes out of a 512-byte packet slot β€” 992 bytes beyond the slot β€” into userspace, returning 0 (no error).
  • Every 16-byte window of the beyond-slot region decodes to the exact expected (slot, offset) pair: user[512..1504] == ring slots 1..3 content. Host-side verification of leak_sample.txt: 62/62 windows attributed (leak_attributed.txt).

Fix validated: clamping the copy to ir->psize keeps the legit packet (len=508) bit-identical (leak_legit_508.txt == leak_fixed_508.txt) and turns the malicious packet into per-slot truncated copies.

Threat model / reachability honesty

  • Requires (a) an open /dev/fwU.S by an operator-group user and (b) a malicious FireWire peer transmitting an iso stream packet whose header len exceeds psize-4 (physical/bus-attached attacker or compromised device). The header quadlet is DMA'd by hardware into the packet slot and is never validated against the slot size.
  • Local-only exploitation without a peer is NOT possible: the user cannot place a forged header into the ring (hardware writes it).
  • Disclosure target: bytes following the packet slot inside the bus_dmamem ring segment(s), and β€” for large len (16-bit field, up to 65535) β€” up to ~64KB past the slot, i.e. past the whole ring allocation into adjacent kernel KVA. The attributed 992-byte over-read (ring slots 1-3) is proven; the beyond-ring variant was not separately demonstrated (kernel already rebuilt with the fix for A/B; same primitive).

Files

  • poc_2295.c β€” trigger (setup, read, attribution, leak_sample.txt)
  • fwhctl.c β€” root-side peer emulation (inject canary ring + packet)
  • leak_sample.txt β€” the 1504-byte read dump (baseline, malicious len)
  • leak_attributed.txt β€” host-side byte-attribution proof (62/62)
  • leak_legit_508.txt / leak_fixed_508.txt β€” A/B bit-identity
  • run_2295_baseline.log, run_2295_fixed_malicious.log, run_2295_fixed_legit.log
  • fix.diff β€” combined fix (this finding: clamp ulen to ir->psize)
VERDICT.md
↓ download raw

DF-2295 VERDICT β€” REPRODUCED (kernel heap over-read to userspace, leak)

Chain (line-accurate, stock fwdev.o + fwdma.o)

fw_read (fwdev.c:260):
321:    fp = (struct fw_pkt *)fwdma_v_addr(ir->buf,
322:            ir->stproc->poffset + ir->queued);   /* slot base */
325:    if(fp->mode.stream.len == 0){ err = EIO; ... }       /* len from
                                                             RECEIVED
                                                             packet hdr */
329:    err = uiomove((caddr_t)fp,
330:        fp->mode.stream.len + sizeof(u_int32_t), uio);  /* UNBOUNDED
                                                             vs psize */

fp->mode.stream.len is the 16-bit length field of the stream packet header that the FireWire peer transmits (OHCI DMA writes header+payload into the psize-sized slot). No check against ir->psize exists anywhere on the path. uiomove copies len+4 bytes from the slot to userspace.

Observed (baseline, stock binaries)

Ring: 4 chunks x 8 packets x 512-byte slots (32 slots), allocated by the real fwdev_allocbuf β†’ fwdma_malloc_multiseg. All 32 slots pre-filled (by the harness emulating the peer) with 16-byte canary records: {0xF00DF00D, tag=0x50000000|slot, byte_offset, 0xDEADC0DE^tag}. Packet header injected at slot 0: 05dc01a0 (len=1500, tcode=0xa).

DF-2295: read() returned 1504 (errno=0)
DF-2295: returned packet header = 05dc01a0 (len=1500 tcode=a sy/chtag=a0)
DF-2295: copied 1504 bytes; slot capacity 512 -> 992 bytes beyond the packet slot
DF-2295: user[512..] == DMA ring slot 1 (tag=0x50000001) offset 0 -- beyond slot 0
DF-2295: user[528..] == DMA ring slot 1 (tag=0x50000001) offset 16 -- beyond slot 0
...

Host-side verification of leak_sample.txt (this pack): all 62 sixteen- byte windows in [512,1504) decode to the exact expected (slot, offset): window at user offset X carries the record of ring offset X-512, i.e. slot (X-512)/512+1 at offset (X-512)%512. Every one of the 992 beyond-slot bytes is attributed to known adjacent ring-slot memory β€” a complete, byte-exact kernel memory disclosure to userspace. (The in-guest PoC summary line "attributed 248/4 bytes" under-counts by reporting matched 4-byte words; the host verifier and leak_sample.txt are authoritative.)

Note: read() returns SUCCESS (rc=1504) β€” the over-read is not even flagged as an error.

Threat model / reachability (honest)

  • Preconditions: operator-group user with an open /dev/fwU.S (0660 root:operator, stock clone perms) that has set up an iso receive stream, AND a FireWire peer transmitting a crafted packet with stream.len > psize-4. The peer is the source of the length field; the local user is the recipient of the over-read bytes.
  • Disclosure range: with psize=512 and len up to 65535, a single uiomove can read up to 65539 bytes starting at a 512-byte slot β€” i.e. beyond the entire fwdma multiseg allocation into adjacent kernel virtual memory. The 992-byte attributed over-read (adjacent slots 1-3) is proven; the beyond-ring variant uses the identical primitive and was not separately run (the guest kernel had already been rebuilt with the fix for A/B validation).
  • Privesc assessment: READ-ONLY disclosure (adjacent DMA ring content and, for extreme len, adjacent kernel KVA). No write primitive, no direct escalation. On FireWire-equipped hosts this is a kernel info-leak that can assist exploit chains; requires peer cooperation. Closed as: leak, local impact contingent on a malicious peer.

Fix validation (A/B, fix.diff clamp hunk)

Fixed fw_read:

    ulen = fp->mode.stream.len + sizeof(u_int32_t);
    if (ulen > ir->psize)
        ulen = ir->psize;
    err = uiomove((caddr_t)fp, ulen, uio);
  • Malicious packet (len=1500, psize=512): every copy is clamped to the slot; read returns per-slot chunks (512 bytes each) β€” no single copy ever crosses a slot boundary (run_2295_fixed_malicious.log: the beyond-slot bytes now come from separate, clamped per-slot copies of subsequent slots β€” bounded by construction).
  • Legit packet (len=508 = psize-4): 512 bytes returned, hexdump bit-identical to the baseline kernel output (leak_legit_508.txt vs leak_fixed_508.txt β€” diff empty, BIT_IDENTICAL_508).

Artifacts

leak_sample.txt, leak_attributed.txt (host verifier output), run_2295_baseline.log, run_2295_fixed_.log, leak__508.txt, poc_2295.c, fwhctl.c, fwharness.c (rig), env.txt.

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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #8: Sun Oct 18 07:38:56 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/FWH x86_64

Confirmed kernel references

Detail

Exploit chain

operator-group user sets up iso receive (SSTBUF/SRSTREAM); malicious peer injects stream packet with len>psize-4; user read() receives adjacent kernel DMA-heap bytes (proof: full byte attribution). Read-only disclosure; no write primitive; no direct privesc - flagged as leak with peer-cooperation precondition.

Evidence (decisive lines)

leak_sample.txt (1504-byte read dump), leak_attributed.txt (62/62 windows attributed host-side), run_2295_baseline.log, leak_legit_508.txt vs leak_fixed_508.txt (bit-identical diff), run_2295_fixed_malicious.log

PoC changes

Rewrote as a coordinated reader/injector: opuser opens/reads, root-side fwhctl emulates the OHCI receive DMA (canary ring + crafted header through the real fwdma_v_addr slot). Fixed a stdout-buffering race and an attribution counter bug (bytes vs words) in the pack copy.

Verified recommended fix

Clamp the uiomove length to ir->psize (ulen = min(stream.len+4, ir->psize)).

Verdict

fw_read copies fp->mode.stream.len+4 bytes from a psize-sized DMA packet slot with no bound (fwdev.c:329-330). With psize=512 and a peer-supplied stream.len=1500, read() returned 1504 bytes successfully, 992 bytes beyond the slot, and every beyond-slot byte was attributed to known adjacent ring-slot content (62/62 canary windows decode to the exact slot+offset). Requires an operator-group opener plus a malicious FireWire peer supplying the oversized header; disclosure covers the whole DMA ring and, for extreme len, memory past the ring allocation (same primitive; beyond-ring variant not separately run). Fix validated: clamping the copy to ir->psize keeps a legit packet (len 508) bit-identical and bounds every copy to one slot.