# DF-3365 — `sta_entry` has no reference count: three more unlocked-use windows beyond DF-0395/0396

File: `sys/netproto/802_11/wlan/ieee80211_scan_sta.c` (pass 2)

## What

`struct sta_entry` objects are protected only by the scan table lock.
Besides the known DF-0395 (`sta_lookup` callers) and DF-0396
(`sta_iterate` callback), pass 2 found three additional sites that use
an entry **after** dropping the table lock:

1. **`sta_add()` PICK1ST tail** — the lock is released at
   ieee80211_scan_sta.c:397 and `match_bss(vap, ss, se, ...)` at :403
   then reads `se->se_chan`, `se_capinfo`, `se_ssid`, `se_ies.*`,
   `se_rates` unlocked. Runs on **every beacon** while a
   `IEEE80211_SCAN_PICK1ST` scan is active and the entry keeps failing
   match (GOTPICK never sets).
2. **`select_bss()` return value** — released at :1210; `sta_pick_bss`
   (:1269-1272), `adhoc_pick_bss` (:1658-1669), `mesh_pick_bss`
   (:1920-1923) and `sta_roam_check` (:1353-1366) then dereference
   `selbs->base.se_chan`/`se_flags` and hand `&selbs->base` to
   `ieee80211_sta_join()`, which copies ~the whole entry **including
   `se_ies.data`/`se_ies.len`** into a fresh node blob
   (ieee80211_node.c:832) — a freed blob would be read wholesale.
3. `sta_roam_check` / `sta_assoc_fail` / `sta_assoc_success` use
   `sta_lookup()` results unlocked (the DF-0395 family, listed for
   completeness).

These are genuine cross-thread windows on DragonFly, not paper locks:
the beacon rx path runs under `wlan_global_serializer`
(ieee80211.c:720: `ether_ifattach(ifp, mac, &wlan_global_serializer)`),
while the freeing contexts do **not** — `ieee80211_node_timeout` is a
raw callout (ieee80211_node.c:2239-2264) that calls
`ieee80211_scan_timeout -> scan_age -> adhoc_age` frees under the table
lock only, and the sw-scan state machine runs on `ic->ic_tq` taskqueue
threads under `ic_comlock` (ieee80211_scan_sw.c:624-699). Nothing
serializes rx vs purge/join.

## Verification attempt (not reproduced at runtime)

`fwscan.ko` raced thread A (serializer-held beacon injection loop with
PICK1ST armed, rssi=1 so match always fails and the :403 tail runs on
every frame) against thread B (serializer-free `scan_flush` loop, the
same table-lock-only discipline as `node_timeout`). 60s run: 150,000
injections; 120s run with `debug.use_weird_array=1` (kfree junk-fill
0xdeadc0de): 21,800 injections. No panic, no observable corruption —
the window is a few hundred ns inside a multi-µs path and even on a
hit the reads land in mapped stale/junked slab memory.

## Fix

`fix.diff` (validated with the DF-3363 kernel build) closes window (1)
by keeping the table lock across the PICK1ST `match_bss` tail. The
full remedy for (2) is entry reference counting (upstream-style
`se_refcnt`); the pragmatic hardening in fix.diff documents this.
