sta_entry lifetime: three additional unlocked-use windows beyond DF-0395/0396 β PICK1ST match_bss after TABLE_UNLOCK, select_bss result used/joined after unlock, roam/assoc sta_lookup uses
Summary
Beyond the known DF-0395/0396 sites, sta_entry objects are used after the table lock is dropped at: (1) sta_add's PICK1ST tail - TABLE_UNLOCK at :397 then match_bss(se) at :403 runs on EVERY beacon of a PICK1ST scan whose entries keep failing match; (2) select_bss returns selbs after unlocking and sta_pick_bss/adhoc_pick_bss/mesh_pick_bss/sta_roam_check dereference it and pass &selbs->base to ieee80211_sta_join which copies the entry and its se_ies.data blob wholesale into a node; (3) roam/assoc callbacks via sta_lookup. The windows are live cross-thread: rx/ioctl hold wlan_global_serializer while the freeing contexts do not - ieee80211_node_timeout is a raw callout driving adhoc_age entry frees (table lock only) and the sw-scan state machine runs on ic_tq taskqueue threads under ic_comlock. NOT reproduced (180s two-thread race, 171k+ injections vs serializer-free flushes with junk-fill: no panic - sub-microsecond window and mapped-slab reads make runtime observation impractical); windows proven live by lock analysis. Ceiling: stale/garbage entry data steering join decisions, wholesale copy of a freed IE blob into a node, or a crash on a garbage se_chan pointer. Fix validated for window (1) (hold the lock across the PICK1ST tail); full remedy is entry refcounting.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3365 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fwscan.c | β | 29.5 KB | view raw | |
| Makefile | β | 263 B | β download | |
| build.sh | β | 87 B | view raw | |
| build.log | β | 5.6 KB | view raw | |
| run.log | β | 1.7 KB | view raw | |
| README.md | β | 2.8 KB | β raw | |
| fix.diff | β | 2.0 KB | view raw | |
| verdict.json | β | 3.6 KB | view raw | |
| manifest.json | β | 762 B | view raw |
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:
sta_add()PICK1ST tail β the lock is released at ieee80211_scan_sta.c:397 andmatch_bss(vap, ss, se, ...)at :403 then readsse->se_chan,se_capinfo,se_ssid,se_ies.*,se_ratesunlocked. Runs on every beacon while aIEEE80211_SCAN_PICK1STscan is active and the entry keeps failing match (GOTPICK never sets).select_bss()return value β released at :1210;sta_pick_bss(:1269-1272),adhoc_pick_bss(:1658-1669),mesh_pick_bss(:1920-1923) andsta_roam_check(:1353-1366) then dereferenceselbs->base.se_chan/se_flagsand hand&selbs->basetoieee80211_sta_join(), which copies ~the whole entry includingse_ies.data/se_ies.leninto a fresh node blob (ieee80211_node.c:832) β a freed blob would be read wholesale.sta_roam_check/sta_assoc_fail/sta_assoc_successusesta_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.
Fix verification
inconclusivethe PICK1ST lock reorder shipped in fix.diff built and ran clean across the full DF-3363/3364 PoC matrix (no deadlocks, no regressions), but since the race itself never reproduced at runtime, fix efficacy for the remaining windows cannot be behaviorally demonstrated; the select_bss/join windows need refcounting (recommendation only).
findings/poc/DF-3363/fix_validation.log
Confirmed kernel references
- sys/netproto/802_11/wlan/ieee80211_scan_sta.c:397
- sys/netproto/802_11/wlan/ieee80211_scan_sta.c:403
- sys/netproto/802_11/wlan/ieee80211_scan_sta.c:1210
- sys/netproto/802_11/wlan/ieee80211_scan_sta.c:1269
- sys/netproto/802_11/wlan/ieee80211_scan_sta.c:1353
- sys/netproto/802_11/wlan/ieee80211_scan_sta.c:1669
- sys/netproto/802_11/wlan/ieee80211_node.c:832
- sys/netproto/802_11/wlan/ieee80211_node.c:2239
Detail
Evidence (decisive lines)
['run.log: race attempts (150000 + 21800 injections survived, guest stable)', 'VERDICT.md lock analysis: wlan_global_serializer (ieee80211.c:720) vs table-lock-only callout/taskq contexts (ieee80211_node.c:2239, ieee80211_scan_sw.c:624)', 'fix.diff hunk: table lock held across the PICK1ST match_bss tail (validated in the DF-3363 fix kernel build - no regressions across the full PoC matrix)']
PoC changes
race threads built into the fwscan harness: injector under wlan_serialize (rx-path discipline) and flusher without the serializer (node_timeout discipline); PICK1ST kept armed by injecting rssi=1 beacons that always fail match so GOTPICK never sets.
Verified recommended fix
hold the table lock across the PICK1ST match_bss tail (done in fix.diff); add reference counting to struct sta_entry for the select_bss/join consumers.
Verdict
Three additional unlocked-use-after-unlock windows exist beyond the known DF-0395/0396: (a) sta_add's PICK1ST match_bss tail at ieee80211_scan_sta.c:403 runs after TABLE_UNLOCK at :397 on every beacon of a PICK1ST scan; (b) select_bss returns selbs after unlocking (:1210) and sta_pick_bss/adhoc_pick_bss/mesh_pick_bss/sta_roam_check dereference it and pass &selbs->base to ieee80211_sta_join which copies the entry and its se_ies blob wholesale; (c) roam/assoc callbacks via sta_lookup. Lock analysis proves the windows are live cross-thread on DragonFly: rx runs under wlan_global_serializer while node_timeout->adhoc_age frees and the taskqueue scan state machine run under the table/ic locks only. A 180s two-thread race (171k+ injections vs serializer-free flushes, INVARIANTS + use_weird_array junk-fill) produced no panic or visible corruption - the sub-microsecond window and mapped-slab reads make runtime demonstration impractical. Verdict: genuine UAF-read race, not reproduced.
No comments yet.