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)
PoC verification
Evidence pack
findings/poc/DF-5414 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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) whilefw_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
fixedBaseline: 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
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.
No comments yet.