⬢ DragonFlyBSD Kernel Audit
DF-5414 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5414",
  "status": "reproduced",
  "reproduced": true,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "Reproduced on the QEMU guest with the production code path: after one FWACT_CH-queued xfer is placed in ir->q exactly as firewire.c:1893-1896 does, the first production fw_read() frees the xfer while it remains linked (dequeue is #if 0 at fwdev.c:305-317; fw_xfer_free at :318 is live) - the queue head after read#1 is verifiably the freed pointer; the second fw_read() frees the same address again (unconditional double free through fw_xfer_unload/fw_xfer_free on freed memory), and the allocator then returned the same chunk to two owners (p1==p2==xfer), proving the double free and the overlapping-object takeover primitive. Demonstrated impact: silent UAF + double kfree (deterministic crash under load, exploitable overlap demonstrated); the trigger requires one inbound FireWire packet on a FW_SBINDADDR-bound address (any remote bus peer - FireWire is unauthenticated) plus read(2) on /dev/fwN.M. Fix validated: with the dequeue restored, read#1 leaves ir->q empty (head=0, queued=0), read#2 times out with an error, no duplicate allocation.",
  "exploit_chain": "remote FireWire peer sends one async request to a locally bound (FW_SBINDADDR) address -> fw_rcv FWACT_CH queues the binds xfer into ir->q (firewire.c:1893-1896) -> local read(2): fw_read frees the xfer without unlinking (fwdev.c:318, dequeue #if 0) -> read(2) again: fw_xfer_free on the dangling head -> double kfree / UAF read of freed fw_xfer -> slab hands the chunk to two live owners -> object overlap -> code-pointer or cred overwrite (uid=0); a single node also self-triggers via fwmem/kernel async traffic",
  "evidence": "run.log (df5414 block: STILL THE FREED XFER after read#1; DUPLICATE ALLOCATION (double free proven) p1==p2==xfer), fix_run.log (fixed: read#1 err=0 -- ir->q head=0 (?) queued=0, read#2 err=35, p2 != p1), fix.diff, build.log, fix_build.log",
  "kernel_refs": [
    "sys/bus/firewire/fwdev.c:281",
    "sys/bus/firewire/fwdev.c:304",
    "sys/bus/firewire/fwdev.c:305",
    "sys/bus/firewire/fwdev.c:317",
    "sys/bus/firewire/fwdev.c:318",
    "sys/bus/firewire/firewire.c:1874",
    "sys/bus/firewire/firewire.c:1893",
    "sys/bus/firewire/firewire.c:1896",
    "sys/bus/firewire/firewire.c:995",
    "sys/bus/firewire/firewire.c:1049"
  ],
  "poc_changes": "Same shared harness as DF-5413 (df5413.c test 2); the injection replicates firewire.c:1883-1897 byte-for-byte (crit_enter, queued++, STAILQ_INSERT_TAIL, wakeup) so the production fw_read path is what corrupts.",
  "attempts": 3,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 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": 1380,
  "guest_dirty": 0,
  "build_cmd": "sh build.sh  (cd /root/df5413 && make; compiles production firewire.c+fwdma.c+fwdev.c into df5413.ko)",
  "run_cmd": "sh run.sh  (kldload df5413.ko; sysctl kern.df5413_test=2; read kern.df5413_status)",
  "code_hash": "528bb42b35a2e37de5d5e534e5a6eebd9ac13c30ec32c5b5f9313ca96624931a",
  "notes": "Impact recorded as panic (the conservative demonstrated ceiling for a double free on this guest without an inbound-packet source); the duplicate-allocation proof shows the stronger overlapping-objects primitive an attacker with bus access gets. The dangling-queue-head proof is allocator-independent. Hardware-gated only insofar as the inbound packet needs a FireWire peer; the local read side ran as production code.",
  "recommended_fix": "fw_read per-packet branch: move the dequeue out of #if 0 (crit_enter; ir->queued--; STAILQ_REMOVE_HEAD(&ir->q, link); crit_exit;) before fw_xfer_free(xfer) - as in fix.diff.",
  "fix_status": "fixed",
  "fix_baseline_reproduced": true,
  "fix_patched_reproduced": false,
  "fix_verdict": "Baseline: read#1 leaves the freed xfer linked (head == freed pointer), read#2 double-frees it, allocator returns the same chunk twice. Patched (fix.diff, dequeue restored): read#1 leaves ir->q empty (head=0, queued=0), read#2 returns an error after timeout, p2 != p1 - no UAF, no double free.",
  "fix_evidence": "fix_run.log (df5414 block), fix_build.log, fix.diff"
}