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)
PoC verification
Evidence pack
findings/poc/DF-5547 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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)setsxfer->recv.pay_len = roundup2(len, 4)whilexfer->recv.payload(fwmem.c:209, =bp->b_datafromfwmem_strategyfwmem.c:365-368) is onlylenbytes (iolen = MIN(bp->b_bcount, MAXLEN), oddb_bcount). - Sink:
firewire.c:1707-1722 fw_rcv_copy()caps the copy atrecv.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.
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)
- Production
fwmem_open()on a synthetic/dev/fwmem0.0cdev (0660 root:operator equivalent β the nodefwdev_clone()creates,fwdev.c:827-849). - A physio-shaped bio:
b_cmd = BUF_CMD_READ,b_bcount = 29(odd),b_data = C+3whereCis a groomed 32-byte heap chunk whose immediate neighbourN = C+32is guard-filled (0xAA). - Production
fwmem_strategy()(fwmem.c:325) βfwmem_read_block(len=29)βrecv.pay_len = 32,recv.payload = C+3β productionfw_asyreq()/fw_asystart()(TX side driven by a stand-in offwohci_start_atq/fwohci_txd). - An RRESB response with
data_length = 32injected through productionfw_rcv()(exactly whatfwohci_arcv()does,fwohci.c:2824-2835). - 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), plainread(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 offwmem_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
fixedwith 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)
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.
No comments yet.