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