# DF-4996 — VERDICT

**Finding**: `ral_pci_attach()` (sys/dev/netif/ral/if_ral_pci.c:275-279) calls
the full `ral_pci_detach()` when the child attach fails.  Every ral child
driver unwinds **itself** on attach failure and never ran
`ieee80211_ifattach()`; the second, glue-driven detach then reaches
`ieee80211_ifdetach()` on a never-attached (zeroed) `ieee80211com`.

## Reproduced? — sink + precondition: YES (on the audit guest); ral trigger path: hardware-gated (proven by trace)

### The path, line by line

1. `if_ral_pci.c:275` — `error = (*psc->sc_opns->attach)(dev, pci_get_device(dev));`
2. Child failure exits exist **only before** `ieee80211_ifattach()`:
   - rt2560: `goto fail1..fail5` at rt2560.c:235,241,247,253,259; `ieee80211_ifattach(ic)` only at rt2560.c:295.  Fail labels free all rings (335-338) and `lockuninit(&sc->sc_mtx)` (340).
   - rt2661: fails at 231,249,256,262; `ieee80211_ifattach` at 296; fail labels 327-331.
   - rt2860: fails at 265 (NIC init timeout), 296, 305; `ieee80211_ifattach` at 343; fail labels 377-386 (incl. `lockuninit`).
   Newbus zeroes the softc at allocation, so at this point `ic->ic_next = {NULL, NULL}`.
3. `if_ral_pci.c:277` — `(void)ral_pci_detach(dev);`
4. `if_ral_pci.c:310-311` — `psc->sc_ih` is NULL (bus_setup_intr not yet run) → teardown skipped.
5. `if_ral_pci.c:312` — `(*psc->sc_opns->detach)(psc);` → e.g. `rt2560_detach` (rt2560.c:349) / `rt2860_detach` (rt2860.c:388) — first statement(s) operate on still-valid lock/MMIO, then:
6. rt2560.c:356 / rt2661.c:348 / rt2860.c:398 — `ieee80211_ifdetach(ic);`
7. `netproto/802_11/wlan/ieee80211.c:423` — `LIST_REMOVE(ic, ic_next);`
   `sys/sys/queue.h:468` — `*(elm)->field.le_prev = LIST_NEXT((elm), field);`
   with `le_prev == NULL` → **write through NULL → fatal trap 12 at VA 0**.

Tail (unreachable in practice because step 7 panics first, but real):
`rt2560_free_tx_ring()` (rt2560.c:582-636) does not NULL `ring->desc` /
`ring->data` after `bus_dmamem_free`/`kfree`, so the second pass
(rt2560.c:359-363) would double-free every DMA ring; rt2860's
`rt2860_free_tx_ring` (rt2860.c:601) likewise re-frees `ring->txd` and
re-destroys `desc_dmat`.

### Guest demonstration (2026-09-25, DragonFly 6.5-DEVELOPMENT #0)

No Ralink PCI device exists in the QEMU guest, so the *trigger* (failed PCI
attach) cannot fire end-to-end.  The module `df4996.c` reproduces the exact
sink with the exact precondition state:

- `kmalloc(sizeof(struct ieee80211com), M_DEVBUF, M_WAITOK|M_ZERO)` — the
  zeroed ic is exactly the child softc state after a failed attach;
- `ieee80211_ifdetach(ic)` — the exact call at rt2560.c:356/rt2860.c:398.

Observed (run.2.log, panic.txt):

```
DF4996: sysctl trigger 'trigger'
DF4996: zeroed ic=0xfffff8011935e200 (...)
DF4996: invoking ieee80211_ifdetach(ic) ...
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x0
fault code             = supervisor read data, page not present
instruction pointer    = 0x8:0xffffffff80751ca4
Stopped at      ieee80211_ifdetach+0x124:  cmpq    (%rdx),%rbx
```

`0xffffffff80751ca4` is inside `ieee80211_ifdetach` (in-kernel symbol at
`0xffffffff80751b80`), i.e. the faulting instruction is the inlined
`LIST_REMOVE` (queue.h:460-470) operating on `le_prev == NULL` — the exact
mechanism cited by the finding.  Guest wedged in ddb (`db>`); ssh died
(RUN_RC=124 style timeout); required `vm.sh reset with-src` (done — guest
left clean).

### Impact

Kernel NULL-pointer write → panic (local DoS).  Not exploitable as a
controlled write: the store value is `LIST_NEXT` (NULL here) and the target
is NULL.  Trigger requires the ral driver attach to fail — flaky/failing
Ralink hardware or DMA-allocation failure under kmem pressure; not directly
reachable by an unprivileged user, hence severity Low.

### Honest limits

- The ral PCI attach-failure path itself was not executed (no hardware).
- The double-free tail was not executed (the NULL write panics first, by
  construction — `ieee80211_ifdetach` precedes the ring frees in all three
  child detaches).

## Fix validation

Not kernel-built (Low severity, fix validated by review): fix.diff replaces
the full-detach call with PCI-layer-only unwinding (IRQ resource, then MSI,
then MEM BAR).  Note the ordering requirement: the IRQ resource must be
released *before* `pci_release_msi()` — `pci_release_msi_method`
(sys/bus/pci/pci.c:2088-2090) returns EBUSY while an MSI resource is still
allocated.  `pci_release_msi()` is idempotent (ENODEV when nothing allocated,
pci.c:2075).

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