# DF-2294 — FW_SBINDADDR / FW_CBINDADDR NULL `d->ir` dereference

## Verdict (short)
REPRODUCED on the stock `fwdev.o` binary, both cited paths:

* **FW_SBINDADDR** (single open + single ioctl): `fwb->sub = ir->dmach;`
  (fwdev.c:617) loads from `NULL + offsetof(struct fw_xferq, dmach)`
  = **0x10** → `Fatal trap 12: page fault`, supervisor read,
  `fw_ioctl` ← `dev_dioctl` (panic.sbindaddr.txt).
* **FW_CBINDADDR** (two fds on the same controller): `STAILQ_REMOVE(
  &ir->binds, fwb, ...)` (fwdev.c:600) touches
  `NULL + offsetof(struct fw_xferq, binds)` = **0x38** → same fault
  (panic.cbindaddr.txt). FW_CBINDADDR only reaches the deref when
  `fw_bindlookup()` first finds an existing bind (fwdev.c:593-598
  returns EINVAL otherwise), so the reproducer registers a legitimate
  bind on fd1 (SSTBUF+SRSTREAM+SBINDADDR — which itself requires
  d->ir != NULL on that fd) and then issues FW_CBINDADDR from fd2
  whose `d->ir` is NULL.

Trigger is an operator-group user. Fix validated: NULL-check `ir` in
both cases → EINVAL, no panic, and the legitimate bind/unbind path on a
stream-equipped fd still works (fd1 of the A/B run).

## Files
* poc_2294.c — trigger, both modes (`sbind`, `cbind`)
* panic.sbindaddr.txt (fault va 0x10), panic.cbindaddr.txt (0x38)
* run_fixed.log — fixed-kernel A/B
* fix.diff — combined fix (this finding: FW_CBINDADDR/FW_SBINDADDR
  NULL guards)
* fwharness.c/fwharness.h/fwhctl.c — verification harness (see
  ../DF-2293/ for the harness README)
