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