# DF-3314 VERDICT — reproduced (panic / kernel UAF wild write), fix validated

## Status

- **reproduced: 1** — `Fatal trap 9 (general protection fault)` at
  `_ieee80211_free_node+0x2d` (`andl %esi,(%r8,%rax,4)` = the
  `IEEE80211_AID_CLR` write through `iv_aid_bitmap` read from the freed
  vap), stock INVARIANTS kernel #0, from a fresh `vm.sh reset with-src`
  boot.  Guest down at `db>` after the trap (`vm.sh status` => down).
- **fix_status: fixed** — with `fix.diff` (v2) applied in the guest and
  a rebuilt kernel (#1 15:53:57), the byte-identical PoC sequence
  completes: `RELEASE_RC=0`, `associd 0x0` at every late completion,
  guest stays up, zero traps in dmesg, module unloads cleanly.

## How the reproduction works

Threat model: a wifi driver whose tx completions can lag vap
destruction (every async radio: mgmt output through `ic_raw_xmit` and
M_ENCAP data frames each carry a node reference, dropped at
tx-complete).  The software-radio KLD (`fw3.c`) parks the references
its `raw_xmit` receives and drops them on demand, i.e. a perfectly
legal driver behaviour, just late.

1. STA vap up; its bss node is given `ni_associd = 1|0xc000` — the
   exact value `ieee80211_recv_assoc` (sta.c:1683) assigns a station
   that completed MLME association.  This is the one artificial step:
   it substitutes for driving the full AUTH/ASSOC exchange with a fake
   AP.  Everything downstream is the genuine teardown path.  (The
   AUTH/ASSOC exchange itself was implemented and exercised during the
   run — see "negative results" — proving the assoc state is normally
   reachable over the air; only the AP side had to be faked.)
2. `ifconfig wlan0 destroy`: `ieee80211_vap_destroy` → `iv_vdetach` →
   `ieee80211_node_table_reset` drops the table ref; the bss node
   survives (driver refs), `ni_associd` stays `0xc001`;
   `ic_vap_delete` kfree's the 2640-byte vap block.
3. Marker spray reclaims the freed vap block with `0xb0` bytes.
4. Late tx-complete drops the last ref → `_ieee80211_free_node` reads
   `ni_vap->iv_aid_bitmap` = `0xb0b0b0b0b0b0b0b0` (non-canonical) →
   the bit-clear write GP-faults.  Deterministic given reclamation of
   the freed block; without reclamation the same UAF write lands in
   whatever now occupies the stale target (silent corruption — observed
   once: 35 refs dropped, no trap, guest up).

## Why the panic is not the whole story (primitive characterization)

- The faulting write is `aid_bitmap[word] &= ~bit` where the base
  pointer is read from freed-and-reclaimable vap memory.  An attacker
  who can groom the 2640-byte `M_80211_VAP`-class heap so the stale
  `iv_aid_bitmap` field contains a chosen value gets a **constrained
  arbitrary write**: clear one bit of one 32-bit word at
  `base + 4*(aid/32)`, `aid ∈ [1, iv_max_aid)` — i.e. a small fixed
  offset window set by the AID the vap allocated (attacker-influenced
  via association order).  Repeatable per association/destroy cycle.
- Escalation to `uid=0` from a bit-clear primitive was **not achieved**
  this run: candidates (credential `cr_uid` clear — wrong direction,
  needs bit SET; refcount zeroing — clear-only; pointer tag clearing —
  plausible but target-specific) all need a separate write or a
  set-gadget.  The panic + demonstrated controlled-base write is the
  honest ceiling here.  No KASLR/SMEP/SMAP on this guest, so the limit
  is the clear-only primitive, not hardening.
- Trigger preconditions keep severity at Medium: needs (a) a vap
  destroy/reset while (b) frame completions are still outstanding —
  routine admin under load on async-driver hardware; not directly
  forceable by an unprivileged local user, though any local user can be
  the traffic source.

## Negative results recorded during verification (valuable)

- **The "remote rates-IE heap overflow via node.c" candidate is DEAD.**
  `ieee80211_parse_beacon` ends with `IEEE80211_VERIFY_ELEMENT(rates,
  IEEE80211_RATE_MAXSIZE=15, …)` (input.c:667) and every
  `ieee80211_add_neighbor`/`ieee80211_init_neighbor` caller checks the
  status (adhoc.c:697, mesh.c:1904, tdma.c:336); hostap.c:2061-2064 and
  sta.c:1666-1670 VERIFY_ELEMENT directly before `setup_rates`.  Live
  proof on the guest: injecting a beacon with a 200-byte RATES IE into
  a running IBSS cell yields `discard beacon frame, bad scan->rates len
  200` and no overflow.  DF-0581's "latent" classification stands.
- The AUTH/ASSOC injection harness worked on hostap
  (`associd 0xc001` assigned to the attacker node), but the hostap
  destroy path clears `ni_associd` via `node_leave` → `sta_leave` →
  `node_cleanup` before the last ref can drop, so the hostap variant of
  this UAF does not fire; the STA-vap bss node is the vulnerable case
  (no `node_leave` ever runs for it — `node_leave` KASSERTs
  `opmode != STA`).

## Kernel refs

- `sys/netproto/802_11/wlan/ieee80211_node.c:1786-1790` (AID_CLR deref)
- `sys/netproto/802_11/wlan/ieee80211_node.c:1112-1122` (`node_free`
  ratectl + cleanup derefs)
- `sys/netproto/802_11/wlan/ieee80211_node.c:1033-1110`
  (`node_cleanup` vap derefs)
- `sys/netproto/802_11/wlan/ieee80211_node.c:1909-1938` (`node_reclaim`
  unlink branch that strands `ni_associd`)
- `sys/netproto/802_11/wlan/ieee80211_node.c:1977-2012`
  (`table_reset` — fix site)
- `sys/netproto/802_11/wlan/ieee80211_sta.c:1683` (associd assignment)

## Fix narrative

- v1 (associd clear in `node_reclaim` only): **insufficient** —
  rebuilt kernel trapped at `node_free+0x18` (`movq 0x30(%rax),%rax`,
  the ratectl `ni_vap` deref).  This usefully proved defects 2/3.
- v2 (`fix.diff`, validated): `IEEE80211_NODE_VAPGONE` flag; full
  vap-dependent cleanup at `table_reset` while the vap is alive;
  `node_free` skips cleanup for flagged nodes.  Exact PoC passes; guest
  survives; module unloads.  Recommend upstream also audits
  `ic_node_cleanup` callers-under-lock semantics for HT nodes (the
  PoC's node was legacy; `node_cleanup` is now additionally invoked
  under `IEEE80211_NODE_LOCK` from `table_reset`).

## Guest

- baseline: `DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
  (X86_64_GENERIC)` — stock INVARIANTS.
- patched: `DragonFly 6.5-DEVELOPMENT #1: Thu Sep 10 15:53:57 UTC 2026`
  (fix.diff applied in-guest only; host `sys/` untouched).
- guest left in clean state (`vm.sh reset with-src`, status up).

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