⬢ DragonFlyBSD Kernel Audit
DF-5547 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5547",
  "status": "reproduced",
  "reproduced": true,
  "impact": "kernel out-of-bounds WRITE of 1-3 fully responder-controlled bytes past the RPC destination buffer (proven cross-chunk into the adjacent 32-byte heap chunk via production fwmem_strategy->fwmem_read_block->fw_rcv/fw_rcv_copy); on current DFly physio the plain user read(2) path absorbs it into bounce-buffer slack, kernel-originated (UIO_SYSSPACE) reads / direct bio issuers get the full heap overrun",
  "confidence": "certain",
  "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.",
  "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": "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",
  "kernel_refs": [
    "sys/bus/firewire/fwmem.c:197",
    "sys/bus/firewire/fwmem.c:209",
    "sys/bus/firewire/fwmem.c:357",
    "sys/bus/firewire/fwmem.c:365",
    "sys/bus/firewire/firewire.c:1707",
    "sys/bus/firewire/firewire.c:1710",
    "sys/bus/firewire/firewire.c:1716",
    "sys/kern/kern_physio.c:90"
  ],
  "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.",
  "attempts": 3,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026  root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64",
  "runtime_sec": 120,
  "guest_dirty": 0,
  "build_cmd": "cd /root/df5547 && sh build.sh   # compiles production firewire.c + fwmem.c into df5547.ko",
  "run_cmd": "kldload /root/df5547/df5547.ko && sysctl kern.df5547_test=1 && sleep 1 && dmesg | grep -E bytes past buffer|N\\[0..3\\]",
  "code_hash": "e26e27bb0a2ca070737e8fccc2e330e0b6d94aa1dfeb0bd2f6a50b79d81e8cbe",
  "notes": "guest reset (with-src) after the run; /usr/src verified pristine (md5 737a55f06e1365e04c5ac2f4e3b96ec3)",
  "recommended_fix": "fwmem.c:197: pass len (not roundup2(len,4)) as the receive length: fwmem_xfer_req(fwdev, sc, spd, 0, len, hand)",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "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_evidence": "fix_validation.log (verbatim before/after matrix); fix.diff (single hunk)"
}