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)
PoC verification
Evidence pack
findings/poc/DF-5321 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
fw_rcv()response path (firewire.c:1757-1800, called fromfwohci_arcv()on an interrupt/serializing thread):fw_tl2xfer()(:1757) finds the xfer without taking a reference and without removing the tlabel, thenfw_rcv_copy()(:1779) writesxfer->recv.hdr/payload, and only afterwardsxfer->state = FWXF_RCVD(:1786) andfw_xfer_done()(:1789) βact.hand(xfer).firewire_xfer_timeout()(firewire.c:320-353, 10 Hz callout viafirewire_watchdog():356-377, own CPU; itscrit_enter()does not exclude other CPUs): skips onlystate == FWXF_START(:341), then unlinks the tlabel (:348) and callsfw_xfer_done(xfer)(:349) β forstate == FWXF_SENT(response in flight = the narrow window) and forstate == 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()'sfwmem_write_quad()firewire.c:2169-2170; also the fwmem read/write paths): the secondfw_xfer_done()callsfw_xfer_free()on an already-freed object β use-after-free + double kfree (slab duplicate pointers / KKASSERT panic). /dev/fwFW_ASYREQxfers (fwdev.c:tsleep(xfer)βact.hand == fw_asy_callbackβfw_xfer_free_buf()): the timeout'swakeup()releases the ioctl thread, which frees the xfer while the receiving context is still insidefw_rcv_copy()/ storingxfer->stateβ UAF write (wire-controlled quadlets + state/resp bytes) into a freed and possibly re-allocatedfw_xfer, plusxfer->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_ASYREQwaiter 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) beforefw_rcv_copy(); if the timeout already claimed it, drop the response.firewire_xfer_timeout(): claim by CAS before completing; if the xfer is alreadyFWXF_RCVD, drop only the tlabel and leave it to the owner; andkfree()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.
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:
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 writesxfer->recv.*and only afterwards storesstate = FWXF_RCVD(1786) and callsfw_xfer_done()(1789).firewire_xfer_timeout()(firewire.c:320-353): processes any xfer whose state is notFWXF_START(341) β i.e. alsoFWXF_SENT(response in flight: narrow window) andFWXF_RCVD(already delivered, owner not yet freed: wide window) β unlinking the tlabel (348) and callingfw_xfer_done()(349).- The owner's free path (
fwdev.cFW_ASYREQout-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()β forfw_asy_callback_freeusers a doublekfree()of thefw_xfer(duplicate slab pointers /KMALLOC_CHECK_DOUBLE_FREEKKASSERT) and for user xfers a benign double wakeup. - UAF write: the timeout's wakeup releases the
FW_ASYREQwaiter, whichfw_xfer_free_buf()s the xfer while the receiving context is still insidefw_rcv_copy()/storingxfer->stateβ wire-controlled quadlets + state/resp bytes written into freed, possibly re-allocated kernel memory, andact.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) issuingFW_ASYREQtransactions 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 onefw_xferβ turningact.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
fixedBaseline: 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
Confirmed kernel references
- sys/bus/firewire/firewire.c:320
- sys/bus/firewire/firewire.c:336
- sys/bus/firewire/firewire.c:341
- sys/bus/firewire/firewire.c:348
- sys/bus/firewire/firewire.c:349
- sys/bus/firewire/firewire.c:890
- sys/bus/firewire/firewire.c:910
- sys/bus/firewire/firewire.c:1757
- sys/bus/firewire/firewire.c:1779
- sys/bus/firewire/firewire.c:1786
- sys/bus/firewire/firewire.c:1789
- sys/bus/firewire/firewire.c:1078
- sys/bus/firewire/firewire.c:2169
- sys/bus/firewire/firewirereg.h:130
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.
No comments yet.