# DF-5416 — cross-node `FW_CBINDADDR` unbinds a foreign bind: unguarded `STAILQ_REMOVE` walks off NULL → kernel panic

**File:** `sys/bus/firewire/fwdev.c` · **Severity:** Medium · **Bucket:** base:hw (local DoS) · **Confidence:** certain
**Status: reproduced (kernel panic) on the QEMU guest.**

## Root cause (production path:line)

`FW_CBINDADDR` (`fwdev.c:592-602`) looks the bind up with
`fw_bindlookup()` — which searches the **global** `fc->binds` list
(`firewire.c:804-814`), i.e. binds created through *any* file descriptor on
*any* `/dev/fwN.M` node — and then unlinks it from **this** descriptor's
xferq:

```c
STAILQ_REMOVE(&sc->fc->binds, fwb, fw_bind, fclist);  /* ok: fwb is there */
STAILQ_REMOVE(&ir->binds, fwb, fw_bind, chlist);      /* fwdev.c:600: NOT there */
kfree(fwb, M_FW);
```

DragonFly's `STAILQ_REMOVE` (`sys/sys/queue.h:319-331`) has **no
absent-element guard**: the walk `while (STAILQ_NEXT(curelm, field) != elm)
curelm = STAILQ_NEXT(curelm, field);` runs off the end of the list and
dereferences `NULL->chlist` (offset 0x28).  With fd B's `ir->binds` empty
the very first `curelm` is NULL.

Sequence (all software — no FireWire bus traffic needed, only the device
nodes, which need the controller):

1. A: `open("/dev/fw0.0")`, `FW_SSTBUF`, `FW_SRSTREAM` (gets `fc->ir[0]`),
   `FW_SBINDADDR {start=0x1_00000000, len=0x1000}` — production
   `fw_bindadd()` links the bind into `fc->binds` *and*
   `fc->ir[0]->binds` (`firewire.c:847-848`).
2. B: `open("/dev/fw0.1")` (separate `si_drv1`), `FW_SSTBUF`,
   `FW_SRSTREAM` (gets `fc->ir[1]`).
3. B: `FW_CBINDADDR {same start}` → `fw_bindlookup` finds **A's** bind →
   `fwdev.c:600` removes it from B's **empty** bind list → NULL-walk →
   **panic**.

Beyond the panic, the same primitive is a *bind theft*: B removes A's bind
from `fc->binds` and frees it while A's `ir->binds` still references it
(A's later `fw_close` drain at `fwdev.c:224-229` would double-free it if
the walk survived) — ownership is never verified.

## Demonstration (guest, test 4, production fw_ioctl)

```
df5416: A SBINDADDR err=0 fwb=0xfffff8008d3e7f28 start=100000000 end=100001000 sub=0 ...
df5416: B SRSTREAM err=0 ir=0xfffff8008fa923a0 (binds list EMPTY: 0)
df5416: B FW_CBINDADDR -- expect Fatal trap 12 in fw_ioctl
Fatal trap 12: page fault while in kernel mode ... fault virtual address = 0x28
Stopped at      fw_ioctl+0x7f6: movq 0x28(%rdx),%rax      /* rdx = curelm = NULL */
```

`fwdev.c`'s own ioctl path pinned in the trap (`panic.txt`).  Guest down.

## What to run

```
./build.sh
sh run.sh      # kern.df5413_test=4  -> guest panics; capture serial console
```

## Fix

`fix.diff` (shared) makes `FW_CBINDADDR` verify the bind actually belongs
to this `ir` (`STAILQ_FOREACH` over `ir->binds`) before unlinking, wraps
the removals in `crit_enter()`, and frees the bind's `xferlist` xfer
(also the DF-5417 leak).  Validated: `CBINDADDR returned 22` (EINVAL), no
panic, guest alive (`fix_run.log`).
