# DF-3314 — node teardown dereferences the freed vap when the last node reference outlives vap destruction

File: `sys/netproto/802_11/wlan/ieee80211_node.c` (pass-2 audit, GLM 5.3)

## What

`struct ieee80211_node` caches a back-pointer to its owning vap
(`ni->ni_vap`).  When a vap is destroyed, `ieee80211_node_vdetach()`
(:203) → `ieee80211_node_table_reset()` (:1977) drops the **table**
reference of every node of that vap, and the driver then frees the vap
memory (`ic_vap_delete`).  Any node that still has **other** references
survives — unlinked (`ni_table = NULL`) but alive — and its eventual
final `ieee80211_free_node()` walks a free path that dereferences the
**freed vap** in three places:

1. `_ieee80211_free_node()` :1786-1790 — `vap = ni->ni_vap;
   if (vap->iv_aid_bitmap != NULL) IEEE80211_AID_CLR(vap, ni_associd)`
   — a pointer **read** from freed memory followed by a bit-clear
   **write** through it.  (`node_reclaim()` :1909-1938 unlinks the node
   without clearing `ni_associd`; only `node_leave()` / `node_cleanup()`
   clear it, and neither runs for the bss node of a vap being
   destroyed: `table_reset` only does `ia_node_leave` + `AID_CLR` and
   leaves the field set.)
2. `node_free()` :1117 — `ieee80211_ratectl_node_deinit(ni)` derefs
   `ni->ni_vap->iv_ratectl` (proved live: with defect 1 alone patched,
   the panic moved to `node_free+0x18`).
3. `node_free()` :1118 → `node_cleanup()` :1033+ — `vap->iv_opmode`
   (:1041), `vap->iv_ps_sta--` (:1042), `vap->iv_set_tim` (:1082), …

The code's own comment at :1774-1779 admits the hazard ("NB: careful
about referencing the vap as it may be gone if the last reference was
held by a driver") — but the derefs happen anyway.  (Same code exists
upstream in FreeBSD net80211.)

## Who holds the last reference

Any in-flight frame: `ic_raw_xmit(ni, …)` mgmt output and M_ENCAP data
frames carry a node ref that the driver drops at tx-complete.  On async
radios (ath, iwn, …) completions lag; destroying/resetting a vap under
traffic leaves the window open.  The PoC emulates exactly this with a
software-radio KLD whose `raw_xmit` parks its node references and
"completes" them after `ifconfig destroy`.

## Reproduction (stock INVARIANTS guest, kernel #0)

Software radio `fw3.c` (DF-3282/3286/3312 harness pattern):

1. `kldload fw3.ko`; STA-mode vap up: `ifconfig wlan create wlandev
   fw3_0 wlanmode sta ssid fw3sta mode 11g channel 1 up`.
2. `sysctl kern.fw3_sta=1` — the vap's bss node gets `ni_associd =
   0xc001` (exactly what `ieee80211_recv_assoc`, sta.c:1683, assigns
   after a completed MLME association) and one+ driver-held refs are
   parked (in the wild: any in-flight frame).
3. `ifconfig wlan0 destroy` — vap teardown runs; the bss node survives
   with `ni_associd == 0xc001`, `ni_table == NULL`; the 2640-byte vap
   block is kfree'd.
4. `sysctl kern.fw3_spray=1` — reclaim the freed vap block with 0xb0
   markers (proves the stale read pulls attacker-influenced memory).
5. `sysctl kern.fw3_release=1` — the late "tx completions" drop the
   refs; the final `ieee80211_free_node()` executes:

```
fw3.release: late tx-complete of ni 0xfffff80118740400 (associd 0xc001, ni_vap 0xfffff80118718d00)
Fatal trap 9: general protection fault while in kernel mode
Stopped at      _ieee80211_free_node+0x2d:      andl    %esi,(%r8,%rax,4)
```

The faulting instruction is precisely `IEEE80211_AID_CLR`'s
`aid_bitmap[aid/32] &= ~bit` with `%r8` = `iv_aid_bitmap` read from the
freed (reused) vap.  Reproduced twice from clean resets; one
non-sprayed variant showed the silent form (stale pointer still mapped
→ bit-clear write into other freed memory, no trap) — the UAF occurs on
every run, the fault depends on reuse.

## Fix (validated)

`fix.diff`: at vap detach (`ieee80211_node_table_reset`), while
`ni_vap` is still valid, run the full vap-dependent cleanup
(`ic_node_cleanup` + `ieee80211_ratectl_node_deinit`) on every
surviving node and mark it `IEEE80211_NODE_VAPGONE`; `node_free()`
skips those calls for marked nodes (plus a belt-and-braces associd
clear in `node_reclaim()`).  Patched kernel (#1, 15:53:57): the exact
PoC completes (`RELEASE_RC=0`), release log shows `associd 0x0`,
guest stays up, `dmesg` clean.  Stock kernel: fatal trap 9.

An earlier one-line fix (associd clear only) was **insufficient** — the
panic moved to `node_free+0x18` (ratectl deref), which is how defects
2 and 3 were proved.

## Build / run

- `sh findings/poc/DF-3314/build.sh` — compiles the KLD in the guest
  (`/root/fw3`, `make SYSDIR=/usr/src/sys`).
- Baseline: load module → STA vap up → `sysctl kern.fw3_sta=1` →
  `ifconfig wlan0 destroy` → `sysctl kern.fw3_spray=1` →
  `sysctl kern.fw3_release=1` → expect fatal trap 9 at
  `_ieee80211_free_node+0x2d`.
- Fix check: `patch -p1 < fix.diff` in guest `/usr/src`, `make
  nativekernel && make installkernel`, reboot, same sequence → no trap,
  guest survives.
