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

fwmem_read_block() advertises a quadlet-rounded receive length for a shorter destination buffer β€” 1-3 responder-controlled bytes written past the end of the caller's buffer (kernel OOB write)

Summary

recv.pay_len = roundup2(len,4) while recv.payload is the caller's exact len-byte buffer, and the sink fw_rcv_copy caps at pay_len: any quadlet-padded block-read response (normal wire behavior; fully chosen by a malicious node) writes 1-3 controlled bytes past the buffer end. Production-code harness: 3 chosen bytes (41 42 43) landed in the adjacent groomed 32-byte chunk; all odd lengths 1..2047 overrun, multiples of 4 do not. User reads are bounce-buffered by current physio so the user-mode ceiling is slack, but the kernel-side corruption (cross-chunk, demonstrated) and the length-contract violation are unconditional; kernel-originated readers get the full overrun. pay_len=len fix validated (no change at every length, RPC still completes).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5547 Β· 13 files
FileTypeDescriptionSize
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
df5547.c β€” 24.3 KB view raw
Makefile β€” 1.8 KB ↓ download
shim/ β€” 4.0 KB ↓ download
build.sh β€” 85 B view raw
run.sh β€” 90 B view raw
build.log β€” 6.9 KB view raw
run.log β€” 2.3 KB view raw
fix_validation.log β€” 1.5 KB view raw
fix.diff β€” 489 B view raw
env.txt β€” 619 B view raw
verdict.json β€” 4.1 KB view raw

DF-5547 β€” fwmem_read_block() quadlet-roundup receive length exceeds the destination buffer

  • File: sys/bus/firewire/fwmem.c
  • Bug: fwmem.c:197 β€” fwmem_xfer_req(fwdev, sc, spd, 0, roundup2(len, 4), hand) sets xfer->recv.pay_len = roundup2(len, 4) while xfer->recv.payload (fwmem.c:209, = bp->b_data from fwmem_strategy fwmem.c:365-368) is only len bytes (iolen = MIN(bp->b_bcount, MAXLEN), odd b_bcount).
  • Sink: firewire.c:1707-1722 fw_rcv_copy() caps the copy at recv.pay_len, not at the buffer length β†’ responder payload bytes [len .. roundup2(len,4)) are written past the buffer end.

Build (guest, root)

sh build.sh        # compiles UNMODIFIED production firewire.c + fwmem.c
                   # into df5547.ko (globals -D-renamed)

Run

sh run.sh          # kldload + sysctl kern.df5547_test=1

Expected

Console shows, for the groomed cross-chunk layout (b_data = C+3, b_bcount = 29, guard chunk N = C+32):

df5547: bcount=29 pay_len=32 ... bytes past buffer: 41 42 43 AA -> OOB WRITE
df5547: N[0..3] now: 41 42 43 AA (was AA AA AA AA)

and sysctl kern.df5547_status reports DF-5547 REPRODUCED (cross-chunk …): up to 3 responder-controlled bytes written past the end of the caller's buffer β€” in the groomed layout, into the adjacent heap chunk. Negative controls (b_bcount multiple of 4) show no change.

Impact ceiling honestly noted: on current DFly physio() (sys/kern/kern_physio.c) user reads use a bounce buffer, so for plain read(fd, buf, odd) the extra bytes land in bounce-buffer slack; any kernel-side bio issuer / UIO_SYSSPACE read gets the full out-of-bounds write. The RPC contract violation (pay_len > buffer) is unconditional.

VERDICT.md
↓ download raw

DF-5547 VERDICT β€” fwmem_read_block() quadlet-rounded receive length overruns the destination buffer

Reproduced: YES (controlled cross-chunk heap OOB write, production code)

sys/bus/firewire/fwmem.c:197 β€” fwmem_read_block() sets xfer->recv.pay_len = roundup2(len, 4) (via fwmem_xfer_req(..., 0, roundup2(len, 4), ...)) while xfer->recv.payload (fwmem.c:209) is the caller's len-byte buffer (bp->b_data, fwmem_strategy() fwmem.c:357-368, iolen = MIN(bp->b_bcount, MAXLEN)). The receive sink fw_rcv_copy() (sys/bus/firewire/firewire.c:1707-1722) caps the copy at xfer->recv.pay_len, NOT at the buffer length, so a block-read response padded to the quadlet boundary (what FireWire responders do on the wire, and what a malicious node can always send) writes roundup2(len,4) - len (1..3) responder-controlled bytes past the end of the caller's buffer.

How it was proven (production firewire.c + fwmem.c compiled into df5547.ko)

  1. Production fwmem_open() on a synthetic /dev/fwmem0.0 cdev (0660 root:operator equivalent β€” the node fwdev_clone() creates, fwdev.c:827-849).
  2. A physio-shaped bio: b_cmd = BUF_CMD_READ, b_bcount = 29 (odd), b_data = C+3 where C is a groomed 32-byte heap chunk whose immediate neighbour N = C+32 is guard-filled (0xAA).
  3. Production fwmem_strategy() (fwmem.c:325) β†’ fwmem_read_block(len=29) β†’ recv.pay_len = 32, recv.payload = C+3 β†’ production fw_asyreq()/fw_asystart() (TX side driven by a stand-in of fwohci_start_atq/fwohci_txd).
  4. An RRESB response with data_length = 32 injected through production fw_rcv() (exactly what fwohci_arcv() does, fwohci.c:2824-2835).
  5. Result (verbatim, run.log):
df5547: bcount=29 pay_len=32 ... | bytes past buffer: 41 42 43 aa -> OOB WRITE
df5547: N[0..3] now: 41 42 43 aa (was AA AA AA AA)

3 responder-chosen bytes landed in the ADJACENT heap chunk. All odd lengths (1, 2, 3, 5, 2047) show the corresponding 3/2/1/3/1-byte OOB; lengths that are multiples of 4 (28, 32) show no change (negative controls). Full RPC completion is unaffected (biodone=1, b_error=0, b_resid=0), i.e. the request path works and the overflow rides along silently.

Impact ceiling (honest)

  • The written bytes are fully chosen by the responder; the write is past the buffer the kernel handed the RPC, in the KLD harness into a neighbouring heap chunk (proven).
  • On current DFly physio() (sys/kern/kern_physio.c), plain read(fd, ubuf, odd) uses a bounce buffer, so for USER reads the 1-3 extra bytes land in bounce-buffer slack and are discarded on copyout β€” the user-path manifestation is mostly benign today.
  • Any UIO_SYSSPACE/kernel-originated read on /dev/fwmem*, or any future direct bio issuer of fwmem_strategy(), gets the full out-of-bounds write into adjacent kernel heap (exactly what the harness demonstrates). The length contract violation itself (pay_len > buffer) is unconditional and is the DF-5413 length-math class surfacing in the fwmem block-RPC assembly.
  • Reachability: local operator-group user with a FireWire bus present doing an odd-length read(2) + any responder that pads (or a malicious node, unauthenticated); kernel callers as above.

Fix validation

fix.diff (one line): advertise len, not roundup2(len, 4), as the receive length. Applied to the guest /usr/src/sys/bus/firewire/fwmem.c, module rebuilt from the patched production file, identical trigger re-run (fix_validation.log):

case baseline fixed
bcount 29 (cross-chunk groom) 41 42 43 aa -> OOB WRITE aa aa aa aa -> no change
bcount 1/2/3/5/2047 OOB WRITE no change
bcount 28/32 (controls) no change no change
RPC completion biodone=1, resid=0 biodone=1, resid=0

fix_status: fixed.

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

with fix.diff applied to the guest /usr/src and the module rebuilt from the patched production file, the identical trigger shows zero writes past any buffer (all lengths 'no change') while the RPC still completes (biodone=1, b_error=0, resid=0)

fix_validation.log (verbatim before/after matrix); fix.diff (single hunk)
↓ fix.diffper-fix-DF-5547

Confirmed kernel references

Detail

Exploit chain

odd-length read on /dev/fwmem* (operator-group user, FW bus present) -> fwmem_strategy -> fwmem_read_block(len=N%4!=0) -> recv.pay_len=roundup2(N,4) -> padded/malicious RRESB via fw_rcv -> fw_rcv_copy copies pay_len bytes into the N-byte buffer -> 1-3 controlled bytes past the end; kernel bio issuers / UIO_SYSSPACE get the corruption in kernel heap (demonstrated cross-chunk in the KLD harness); user-path physio bounce absorbs it today (honest ceiling: contract violation + latent corruption, deterministic for kernel-side callers)

Evidence (decisive lines)

run.log: the decisive lines 'bcount=29 pay_len=32 ... bytes past buffer: 41 42 43 aa -> OOB WRITE' and 'N[0..3] now: 41 42 43 aa'; fix_validation.log: all lengths 'no change' after the one-line fix; df5547.c test 1 = production fwmem_open/fwmem_strategy/fw_asyreq/fw_rcv driving the real RPC state machine

PoC changes

seed had no runnable PoC; built the DF-5413-family KLD harness: unmodified production firewire.c+fwmem.c #included with -D-renamed globals, synthetic fc/fw_device/devclass state (fwohci attach equivalent), a TX-start stand-in replicating fwohci_start_atq/fwohci_txd dequeue+FWXF_SENT, and RRESB injection through production fw_rcv (fwohci_arcv equivalent). Fixed vs the first draft: fwdev.fc must be set (fwmem_xfer_req copies it), _hw_firewire_fwmem and fwmem_debug sysctl renames, rresb has no dest_hi/lo members, kthread_create signature.

Verified recommended fix

fwmem.c:197: pass len (not roundup2(len,4)) as the receive length: fwmem_xfer_req(fwdev, sc, spd, 0, len, hand)

Verdict

Reproduced on the production code path: fwmem_read_block (fwmem.c:197) advertises recv.pay_len=roundup2(len,4) for a len-byte destination buffer and fw_rcv_copy (firewire.c:1707-1722) honors pay_len, not the buffer size, so a quadlet-padded RRESB writes up to 3 attacker/responder-chosen bytes past bp->b_data+b_bcount. Verified twice on independent boots with a length matrix (all odd lengths 1..2047 overrun; multiples of 4 do not) and a groomed adjacent-chunk layout showing 0x41 0x42 0x43 landing in the neighbour. Fix (advertise len, not the roundup) validated: identical trigger produces zero overruns and the RPC still completes.