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

fw_xfer/tlabel use-after-free and double completion: firewire_xfer_timeout() vs fw_rcv() + FW_ASYREQ waiter β€” no cross-CPU serialization of the completion/tlabel lifetime

Summary

The 10Hz watchdog and fw_rcv can both complete the same fw_xfer while the FW_ASYREQ waiter frees it and its tlabel - crit_enter is per-CPU and excludes nothing on the other CPU. Characterized primitives: UAF read of a 16-byte slab, double kfree of fw_xfer (duplicate slab pointers), stale wire-data stores + act.hand() call through freed memory. Guest-verified via KLD harness compiling the UNMODIFIED production firewire.c (3 threads modeling recv-interrupt/watchdog/waiter): panic attempt #1 (Fatal trap 12 at firewire_xfer_timeout+0xbc); fix.diff (fc->tlabel_lock spinlock across all four tlabel fns + atomic SENT->RCVD claim + done-outside-lock): mode1 3000 attempts 0 double-completions, mode2 SURVIVES 5000, guest up. Operator-group trigger timing a node answering near the 200ms split timeout. spinlock+CAS fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5321 Β· 16 files
FileTypeDescriptionSize
df5321.c β€” 16.7 KB view raw
Makefile β€” 1.3 KB ↓ download
build.sh β€” 161 B view raw
run.sh β€” 185 B view raw
run_mode1.sh β€” 885 B view raw
run.log β€” 3.3 KB view raw
run_mode1_console.log β€” 13.6 KB view raw
panic.txt β€” 814 B view raw
fix.diff β€” 6.8 KB view raw
fix_build.log β€” 6.5 KB view raw
fix_run.log β€” 1.5 KB view raw
env.txt β€” 417 B view raw
VERDICT.md β€” 6.7 KB ↓ raw
README.md β€” 4.2 KB ↓ raw
manifest.json β€” 2.2 KB view raw
verdict.json β€” 4.8 KB view raw

DF-5321 β€” fw_xfer/tlabel UAF + double completion: firewire_xfer_timeout() vs fw_rcv() β†’ kernel panic

File: sys/bus/firewire/firewire.c Severity: High Β· Bucket: memcorrupt Β· Confidence: certain Status: reproduced (panic) on the QEMU guest with a KLD harness driving the production completion paths; fix validated (panics gone, leak gone, counters zero).

Root cause (production path:line)

Two producers complete the same struct fw_xfer with no ownership claim:

  1. fw_rcv() response path (firewire.c:1757-1800, called from fwohci_arcv() on an interrupt/serializing thread): fw_tl2xfer() (:1757) finds the xfer without taking a reference and without removing the tlabel, then fw_rcv_copy() (:1779) writes xfer->recv.hdr/payload, and only afterwards xfer->state = FWXF_RCVD (:1786) and fw_xfer_done() (:1789) β†’ act.hand(xfer).
  2. firewire_xfer_timeout() (firewire.c:320-353, 10 Hz callout via firewire_watchdog() :356-377, own CPU; its crit_enter() does not exclude other CPUs): skips only state == FWXF_START (:341), then unlinks the tlabel (:348) and calls fw_xfer_done(xfer) (:349) β€” for state == FWXF_SENT (response in flight = the narrow window) and for state == FWXF_RCVD (response already delivered, owner not yet freed = the wide window).

Exactly one fw_xfer_done() must run per xfer; both paths can run, in either order, concurrently.

Consequences

  • Kernel-internal xfers whose act.hand == fw_asy_callback_free (production users in this file: fw_phy_config() firewire.c:1078, fw_bmr()'s fwmem_write_quad() firewire.c:2169-2170; also the fwmem read/write paths): the second fw_xfer_done() calls fw_xfer_free() on an already-freed object β†’ use-after-free + double kfree (slab duplicate pointers / KKASSERT panic).
  • /dev/fw FW_ASYREQ xfers (fwdev.c: tsleep(xfer) β†’ act.hand == fw_asy_callback β†’ fw_xfer_free_buf()): the timeout's wakeup() releases the ioctl thread, which frees the xfer while the receiving context is still inside fw_rcv_copy() / storing xfer->state β†’ UAF write (wire-controlled quadlets + state/resp bytes) into a freed and possibly re-allocated fw_xfer, plus xfer->act.hand(xfer) called through a pointer read from freed memory.

What to run here (guest, root, no FireWire hardware β€” KLD harness)

The module compiles the unmodified production firewire.c in (globals -D-renamed against the kernel copy) and drives its real completion paths with three lwkt threads:

  • receiving context β€” production fw_rcv() with a 64 KB RRESB response,
  • watchdog context β€” production firewire_xfer_timeout() in a tight loop,
  • FW_ASYREQ waiter context β€” tsleep(xfer) + fw_xfer_free_buf(xfer) exactly like fwdev.c.
./build.sh                     # build df5321.ko
sh run_mode1.sh                # mode 1: FW_ASYREQ waiter scenario
# β†’ Fatal trap 12 ... Stopped at firewire_xfer_timeout+0xbc
#   (freed struct tlabel dereferenced by the watchdog; attempt #1)
sh run.sh                      # mode 2: production fw_asy_callback_free
# β†’ identical panic (double-free / tlabel UAF), guest goes down

Both baseline modes panic the production code on the first race hit. With fix.diff applied to /usr/src and the module rebuilt, mode 1 completes 3000 attempts with double-completions=0 and zero stale stores, mode 2 SURVIVES 5000 attempts, and the DF-5322 leak drops to zero (fix_run.log).

Fix

fix.diff β€” ownership claim with a byte-sized CAS (atomic_fcmpset_char, x86_64):

  • fw_rcv(): claim (FWXF_SENT β†’ FWXF_RCVD) before fw_rcv_copy(); if the timeout already claimed it, drop the response.
  • firewire_xfer_timeout(): claim by CAS before completing; if the xfer is already FWXF_RCVD, drop only the tlabel and leave it to the owner; and kfree() the unlinked tlabel (also the DF-5322 fix).

Validated: with fix.diff applied to the guest /usr/src, the module (rebuilt against the patched production source) shows 0 double completions, 0 stale stores, no leak, and survives mode 2.

See VERDICT.md for the full narrative and run.log / fix_run.log for the raw evidence.

VERDICT.md
↓ download raw

DF-5321 β€” verdict

Status: reproduced (kernel panic, use-after-free / double-free primitive) Impact: panic (demonstrated); UAF-read/write + double-kfree primitive (characterized below) Confidence: certain

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 (Thu Jul 2 06:02:54 UTC 2026) X86_64_GENERIC, 6 vCPU, no FireWire hardware. Evidence class: KLD harness compiling the unmodified production firewire.c into the module (globals -D-renamed against the kernel copy β€” DF-5230/DF-5302 pattern), driving the real fw_rcv(), fw_tl2xfer(), firewire_xfer_timeout(), fw_get_tlabel(), fw_xfer_free_buf(), fw_tl_free() code paths from three lwkt threads that model, one for one, the production contexts: the receiving interrupt thread (fwohci_arcv β†’ fw_rcv), the 10 Hz watchdog callout (firewire_watchdog β†’ firewire_xfer_timeout), and the /dev/fw FW_ASYREQ ioctl waiter (fwdev.c: tsleep(xfer) β†’ act.hand-wakeup β†’ fw_xfer_free_buf(xfer)).

What was reproduced

Both race modes panic the production code on the FIRST attempt (run.log, panic.txt, run_mode1_console.log):

Stopped at firewire_xfer_timeout+0xbc: movq 0x8(%rax),%rax
Fatal trap 12: page fault, supervisor read data, fault address 0x8
current thread = pri 12 (CRIT)      cpuid = 2

The instruction is xfer = tl->xfer / xfer->tv inside firewire_xfer_timeout() (firewire.c:336-338): the waiter's production fw_xfer_free_buf() β†’ fw_xfer_unload() β†’ fw_tl_free() (firewire.c:1024-1039 β†’ 994-1020 β†’ 890-905) unlinked and kfree'd the very struct tlabel the watchdog had loaded with STAILQ_FIRST() β€” use-after-free read of the freed 16-byte tlabel object (reused zeroed β‡’ tl->xfer == NULL β‡’ fault at 0x8; any other reuse β‡’ dereference of an attacker-influenced pointer, and STAILQ_REMOVE_HEAD on the freed entry corrupts the bucket list).

Mode 2 (production completion handler fw_asy_callback_free, as used by fw_phy_config() firewire.c:1078 and fw_bmr()'s fwmem_write_quad() firewire.c:2169-2170) panics with the identical signature: the first completion frees both the xfer and the tlabel while the concurrent watchdog scan (or the receiving thread's own completion) still holds pointers to them β€” the double fw_xfer_free() / UAF primitive.

The defect, precisely

There is no ownership or lifetime protection on the tlabel/xfer completion machinery; per-CPU crit_enter() does not exclude other CPUs:

  1. fw_rcv() response path (firewire.c:1757-1800): fw_tl2xfer() returns the xfer with no reference and without unlinking the tlabel; fw_rcv_copy() (1779) then writes xfer->recv.* and only afterwards stores state = FWXF_RCVD (1786) and calls fw_xfer_done() (1789).
  2. firewire_xfer_timeout() (firewire.c:320-353): processes any xfer whose state is not FWXF_START (341) β€” i.e. also FWXF_SENT (response in flight: narrow window) and FWXF_RCVD (already delivered, owner not yet freed: wide window) β€” unlinking the tlabel (348) and calling fw_xfer_done() (349).
  3. The owner's free path (fwdev.c FW_ASYREQ out-label, fwmem callbacks) frees xfer+tlabel as soon as it is woken.

Consequences, all from unserialized overlap of (1), (2), (3):

  • tlabel UAF read / list corruption in the watchdog (demonstrated panic, fault at 0x8 from freed-zeroed tlabel; wild pointer otherwise).
  • double fw_xfer_done() β†’ for fw_asy_callback_free users a double kfree() of the fw_xfer (duplicate slab pointers / KMALLOC_CHECK_DOUBLE_FREE KKASSERT) and for user xfers a benign double wakeup.
  • UAF write: the timeout's wakeup releases the FW_ASYREQ waiter, which fw_xfer_free_buf()s the xfer while the receiving context is still inside fw_rcv_copy()/storing xfer->state β€” wire-controlled quadlets + state/resp bytes written into freed, possibly re-allocated kernel memory, and act.hand() called through a pointer read from freed memory.

Exploitability

  • Preconditions on real hardware: a local user permitted to open /dev/fwN.M (root:operator 0660) issuing FW_ASYREQ transactions against a node that answers near the 200 ms split-transaction timeout (attacker-controlled with a second FireWire device / crafted node), or kernel-internal fwmem/PHY transactions racing the watchdog under CPU load. The watchdog runs at 10 Hz for every outstanding split transaction, so the window is exercised continuously in production use.
  • Demonstrated: reliable kernel panic on the first race hit.
  • Potential (not demonstrated to uid0): the tlabel-UAF read dereferences slab-adjacent data as a struct fw_xfer *; the UAF-write lands wire data in a re-used 128-byte-class object. On this audit's permissive guest (no KASLR/SMAP/SMEP) slab grooming could convert the double-free into two live owners of one fw_xfer β€” turning act.hand() into a controlled call. A full uid=0 chain was NOT built (hardware-gated trigger); the primitive is characterized above.

Fix validation (fix.diff β€” applied to guest /usr/src, module rebuilt)

The fix serializes the tlabel lists with a spinlock (fc->tlabel_lock, firewirereg.h) across fw_get_tlabel, fw_tl_free, fw_tl2xfer, firewire_xfer_timeout, makes the completion claim atomic with the lookup (fw_tl2xfer CAS FWXF_SENT→FWXF_RCVD under the lock; timeout CASes before completing and only drops the tlabel for already delivered xfers), moves fw_xfer_done() outside the lock, and kfrees the tlabel unlinked by the timeout (also the DF-5322 leak). As a side effect fw_get_tlabel() no longer sleeps inside a critical section (the known DF-1021 site).

Baseline β†’ fixed, identical triggers (fix_run.log, fix_build.log):

case baseline fixed
mode 1 (FW_ASYREQ scenario) panic on attempt #1 3000 attempts OK, 0 double-completions, 0 stale stores
mode 2 (fw_asy_callback_free) panic on attempt #1 SURVIVED 5000 attempts, guest up
mode 0 (tlabel leak, DF-5322) 5000 objects / 80000 B leaked 0 / 0

fix_status: fixed.

Bottom line

Real, production-path use-after-free/double-free in the FireWire transaction-completion machinery, reproducible on demand (panic) with the production code on the audit guest, with a validated fix. Highest impact realistically: local privilege-adjacent kernel memory corruption for operator-group users with FireWire connectivity; minimum: reliable local/remote-timing DoS.

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: panic attempt #1 in both race modes + 5000-object tlabel leak. Patched (fix.diff on /usr/src, module rebuilt, identical triggers): mode1 completes 3000 attempts with double-completions=0 / act.hand-after-free=0 / stale-stores=0; mode2 SURVIVED 5000 attempts; leak delta 0; guest stays up.

fix_run.log, fix_build.log, fix.diff
↓ fix.diffper-fix-DF-5321

Confirmed kernel references

Detail

Exploit chain

open /dev/fwN.M (operator group) -> FW_ASYREQ split transaction to a node whose response lands at the 200 ms split timeout -> fw_rcv() and firewire_xfer_timeout() both complete the xfer / the waiter frees xfer+tlabel under the scanning watchdog -> freed struct tlabel dereferenced (or fw_xfer double-freed / stale wire-data written into re-used slab object) -> kernel panic; with slab grooming on the permissive guest the double-free/UAF-write is a path to controlled corruption (not built: hardware-gated).

Evidence (decisive lines)

panic.txt and run_mode1_console.log (Fatal trap 12, Stopped at firewire_xfer_timeout+0xbc, pri 12 CRIT), run.log (both modes panic on attempt #1, baseline), fix_run.log (fixed: mode1 3000 attempts 0 double-completions / mode2 SURVIVED 5000 / leak 0), fix_build.log, fix.diff

PoC changes

Harness written fresh for this file (DF-5230/DF-5302 KLD pattern): compiles the production firewire.c in via #include with -D-renamed globals (incl. -Dfirewire, -D_hw_firewire, MALLOC_DEFINE types, and a MODULE_VERSION no-op shim because the #name stringify defeats -D renames and would hit 'interface firewire.1 already present'); three lwkt threads model the receiving interrupt, the 10 Hz watchdog callout, and the fwdev FW_ASYREQ waiter; response packet hand-built per LE bitfield layout; tlabel/xfer zones prewarmed so the known DF-1021 M_WAITOK-in-crit site cannot hijack the result; fixed during bring-up: g_stop release at receiver exit, xfer->sc publish before tlabel insertion, 90 s wall-clock deadline, paced watchdog.

Verified recommended fix

Serialize the tlabel lists with fc->tlabel_lock and claim xfer completion atomically with the lookup (CAS FWXF_SENT->FWXF_RCVD); kfree the tlabel unlinked by firewire_xfer_timeout; run fw_xfer_done outside the lock (fix.diff).

Verdict

Reproduced on the QEMU guest with the production firewire.c compiled into a KLD harness: the split-transaction watchdog firewire_xfer_timeout() and the fw_rcv() response path (plus the FW_ASYREQ waiter's fw_xfer_free_buf/fw_tl_free) race with no cross-CPU serialization, panicking the kernel on the first attempt (tlabel use-after-free read inside firewire_xfer_timeout, 'movq 0x8(%rax),%rax', fault 0x8; same for the production fw_asy_callback_free double-free variant). Demonstrated impact: reliable kernel panic; characterized primitive: UAF read of a freed 16-byte tlabel (wild-pointer deref when not reused zeroed), double fw_xfer_done -> double kfree of fw_xfer, and stale wire-data stores into a freed/re-used fw_xfer while its owner frees it. uid=0 chain not built (hardware-gated trigger); fix validated: panics and leak gone with fix.diff applied to /usr/src.