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

fw_read frees the FWACT_CH xfer while it stays linked in ir->q (dequeue is #if 0) -> use-after-free + double free on every subsequent read

Summary

The per-packet branch's dequeue is #if 0'd ('XXX broken') while fw_xfer_free is live: read#1 frees the xfer and leaves it linked (ir->queued never decremented), read#2 fetches the dangling head and frees it again. Guest-proven with production fw_read: after read#1 the queue head verifiably equals the freed pointer; read#2 double-freed and the allocator handed the same chunk to two owners (p1==p2==xfer) - the overlapping-objects takeover primitive. One inbound FW async packet (unauthenticated remote peer) + two local reads. Restore-the-dequeue fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5414 Β· 14 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 2.2 KB ↓ raw
df5413.c β€” 21.5 KB view raw
Makefile β€” 1.8 KB ↓ download
shim/ β€” 4.0 KB ↓ download
build.sh β€” 103 B view raw
run.sh β€” 145 B view raw
build.log β€” 7.0 KB view raw
run.log β€” 2.0 KB view raw
fix.diff β€” 3.4 KB view raw
fix_build.log β€” 7.0 KB view raw
fix_run.log β€” 2.6 KB view raw
env.txt β€” 615 B view raw
verdict.json β€” 4.2 KB view raw

DF-5414 β€” fw_read frees a queued xfer without dequeuing it (dequeue is #if 0) β†’ use-after-free + double free

File: sys/bus/firewire/fwdev.c Β· Severity: High Β· Bucket: memcorrupt Β· Confidence: certain Status: reproduced on the QEMU guest (production firewire.c+fwdma.c+ fwdev.c in KLD df5413.ko, DF-5302/DF-5321 evidence class).

Root cause (production path:line)

The FWACT_CH receive path (firewire.c:1874-1901) takes the bind's xfer off bind->xferlist and appends it to fc->ir[sub]->q (firewire.c:1893-1896), then wakes the reader. fw_read() takes the first xfer (fwdev.c:281) and enters the per-packet branch (fwdev.c:304) β€” where the dequeue code is inside #if 0 /* XXX broken */ (fwdev.c:305-317) but fw_xfer_free(xfer) (fwdev.c:318) is not:

} else if(xfer != NULL) {
#if 0 /* XXX broken */
    /* per packet mode or FWACT_CH bind?*/
    crit_enter();
    ir->queued --;
    STAILQ_REMOVE_HEAD(&ir->q, link);
    crit_exit();
    ...
#endif
    fw_xfer_free( xfer);          /* <-- frees, never unlinks */
}

Read #1 frees the xfer while it stays linked in ir->q (and ir->queued never decrements). Read #2 fetches the same (dangling) head and frees it again β†’ use-after-free read of xfer->state/…/xfer->malloc inside fw_xfer_unload() (firewire.c:995-1020) and a double kfree (firewire.c:1049). Every subsequent read frees it yet again.

Trigger: any asynchronous packet that lands in a FW_SBINDADDR-bound address range β€” an unauthenticated remote FireWire peer (the bus has no authentication) β€” followed by local read(2)s on the open /dev/fwN.M. fwmem.c's kernel-side fwmem traffic targets the same machinery.

Demonstration (guest, test 2)

The harness queues exactly one xfer the way the production FWACT_CH path does (firewire.c:1883-1897), then calls the production fw_read twice:

df5414: injected xfer=0xfffff801176997c0, ir->q head=0xfffff801176997c0 queued=1
df5414: read#1 err=0 -- ir->q head=0xfffff801176997c0 (STILL THE FREED XFER) queued=1
df5414: realloc p1=0xfffff801176997c0 (same address as freed xfer)
df5414: read#2 err=0 -- p2=0xfffff801176997c0 p1=0xfffff801176997c0 -> DUPLICATE ALLOCATION (double free proven)

After read#1 the queue head is the freed pointer (dangling, provable without allocator assumptions); read#2 unconditionally frees it again; the allocator then hands the same chunk to two owners (p1 == p2 == xfer) β€” the classic double-free consequence and the takeover primitive (overlap two live kernel objects).

What to run

./build.sh
sh run.sh      # kern.df5413_test=2

Fix

fix.diff (shared) un-#if-0s the dequeue (crit + queued-- + STAILQ_REMOVE_HEAD) before the free. Validated: with the fix, read#1 leaves ir->q head=0 and queued=0, read#2 sleeps and errors, and no duplicate allocation occurs (fix_run.log).

VERDICT.md
↓ download raw

DF-5414 β€” VERDICT

reproduced Β· impact: UAF + double free (panic-class; overlap primitive proven) Β· confidence: certain

Production fw_read() (fwdev.c:260-345) was driven twice against an ir->q populated exactly the way the production FWACT_CH receive path populates it (firewire.c:1883-1897: bind xfer, crit, queued++, STAILQ_INSERT_TAIL, wakeup). Result on the guest (fresh boot, fresh module load):

  • read#1 returned 0 and left the freed xfer linked: ir->q head == 0xfffff801176997c0 (STILL THE FREED XFER), queued=1 β€” the dequeue is #if 0 (fwdev.c:305-317) while fw_xfer_free() (fwdev.c:318) is live.
  • fw_xfer_alloc() immediately re-occupied the chunk (LIFO), proving the allocator considers it free while ir->q still points at it.
  • read#2 called fw_xfer_free() on the same address again (UAF reads of xfer->state/xfer->fc/xfer->tl inside fw_xfer_unload, then kfree), and the two follow-up allocations returned the same pointer twice (p1 == p2 == xfer) β€” duplicate allocation = overlapping live kernel objects, the standard takeover primitive.

No allocator assumptions are needed for the core claim: the queue head after read#1 is the freed pointer (deterministic, printed from the live list), so read#2's free of it is unconditionally a double free.

Real-world trigger: one inbound FireWire async packet addressed to a FW_SBINDADDR-bound range (any remote bus peer β€” FireWire is unauthenticated), then read(2) twice on /dev/fwN.M. The remote side is hardware-gated in QEMU; the local corruption path ran as production code.

Fix validated (fix.diff: dequeue restored before free): read#1 err=0 -- ir->q head=0 queued=0 (dequeued!), read#2 errs out, p2 != p1, no duplicate allocation, guest alive (fix_run.log).

Harness quirks documented in DF-5413/VERDICT.md (module entry via the included firewire.c DRIVER_MODULE; guard shims; fake devclass).

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

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_run.log (df5414 block), fix_build.log, fix.diff
↓ fix.diffper-fix-DF-5414

Confirmed kernel references

Detail

Exploit chain

remote FireWire peer sends one async request to a locally bound (FW_SBINDADDR) address -> fw_rcv FWACT_CH queues the bind's 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 (decisive lines)

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

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.

Verified 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.

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.