Node teardown dereferences the freed ieee80211vap when the last node reference outlives vap destruction: _ieee80211_free_node AID_CLR wild write + node_free ratectl/cleanup UAF
Summary
ieee80211_node_table_reset() drops a vap's table references at vap detach WITHOUT clearing ni_associd and without performing the vap-dependent cleanup; ic_vap_delete then frees the vap memory. Any node holding other references - any in-flight frame: ic_raw_xmit mgmt output, M_ENCAP data awaiting tx-complete - survives unlinked with ni_associd set and ni_vap dangling. The eventual final ieee80211_free_node() dereferences the freed vap in three places: AID_CLR reads vap->iv_aid_bitmap from freed memory and bit-clear-writes through it; ratectl_node_deinit derefs iv_ratectl; node_cleanup derefs iv_opmode/iv_ps_sta/iv_set_tim. The code's own comment warns 'the vap may be gone if the last reference was held by a driver' - but the derefs happen anyway (same upstream in FreeBSD). VERIFIED on guest (fw3 software radio with async tx-complete + spray reclaiming the freed vap with 0xb0): 'Fatal trap 9 ... Stopped at _ieee80211_free_node+0x2d: andl %esi,(%r8,%rax,4)' - the AID_CLR write through the 0xb0b0.. pointer read from freed memory; one unsprayed variant showed silent corruption. Reproduced twice from clean resets. With heap grooming the primitive is a constrained arbitrary write (clear one bit of one 32-bit word at attacker-chosen base) repeatable per association/destroy cycle; uid=0 assessed not achieved (needs a set-gadget). Any wifi driver with async tx completions (all real 11n radios): destroying/resetting a vap under load (any local user can be the traffic source). Fix validated (VAPGONE flag: full cleanup at table_reset while vap alive + late-free skips vap derefs + belt-and-braces associd clear in reclaim): byte-identical PoC passes, guest up.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3314 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fw3.c | β | 21.8 KB | view raw | |
| Makefile | β | 254 B | β download | |
| build.sh | β | 87 B | view raw | |
| run.sh | β | 371 B | view raw | |
| build.log | β | 832 B | view raw | |
| run.log | β | 4.1 KB | view raw | |
| panic.txt | β | 4.7 KB | view raw | |
| panic.2.txt | β | 4.7 KB | view raw | |
| patched_run.txt | β | 5.0 KB | view raw | |
| fix.diff | β | 2.7 KB | view raw | |
| env.txt | β | 205 B | view raw | |
| README.md | β | 4.9 KB | β raw | |
| VERDICT.md | β | 6.6 KB | β raw | |
| verdict.json | β | 6.2 KB | view raw |
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:
_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 clearingni_associd; onlynode_leave()/node_cleanup()clear it, and neither runs for the bss node of a vap being destroyed:table_resetonly doesia_node_leave+AID_CLRand leaves the field set.)node_free():1117 βieee80211_ratectl_node_deinit(ni)derefsni->ni_vap->iv_ratectl(proved live: with defect 1 alone patched, the panic moved tonode_free+0x18).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):
kldload fw3.ko; STA-mode vap up:ifconfig wlan create wlandev fw3_0 wlanmode sta ssid fw3sta mode 11g channel 1 up.sysctl kern.fw3_sta=1β the vap's bss node getsni_associd = 0xc001(exactly whatieee80211_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).ifconfig wlan0 destroyβ vap teardown runs; the bss node survives withni_associd == 0xc001,ni_table == NULL; the 2640-byte vap block is kfree'd.sysctl kern.fw3_spray=1β reclaim the freed vap block with 0xb0 markers (proves the stale read pulls attacker-influenced memory).sysctl kern.fw3_release=1β the late "tx completions" drop the refs; the finalieee80211_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.diffin guest/usr/src,make nativekernel && make installkernel, reboot, same sequence β no trap, guest survives.
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)= theIEEE80211_AID_CLRwrite throughiv_aid_bitmapread from the freed vap), stock INVARIANTS kernel #0, from a freshvm.sh reset with-srcboot. Guest down atdb>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 0x0at 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.
- STA vap up; its bss node is given
ni_associd = 1|0xc000β the exact valueieee80211_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.) ifconfig wlan0 destroy:ieee80211_vap_destroyβiv_vdetachβieee80211_node_table_resetdrops the table ref; the bss node survives (driver refs),ni_associdstays0xc001;ic_vap_deletekfree's the 2640-byte vap block.- Marker spray reclaims the freed vap block with
0xb0bytes. - Late tx-complete drops the last ref β
_ieee80211_free_nodereadsni_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] &= ~bitwhere the base pointer is read from freed-and-reclaimable vap memory. An attacker who can groom the 2640-byteM_80211_VAP-class heap so the staleiv_aid_bitmapfield contains a chosen value gets a constrained arbitrary write: clear one bit of one 32-bit word atbase + 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=0from a bit-clear primitive was not achieved this run: candidates (credentialcr_uidclear β 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_beaconends withIEEE80211_VERIFY_ELEMENT(rates, IEEE80211_RATE_MAXSIZE=15, β¦)(input.c:667) and everyieee80211_add_neighbor/ieee80211_init_neighborcaller checks the status (adhoc.c:697, mesh.c:1904, tdma.c:336); hostap.c:2061-2064 and sta.c:1666-1670 VERIFY_ELEMENT directly beforesetup_rates. Live proof on the guest: injecting a beacon with a 200-byte RATES IE into a running IBSS cell yieldsdiscard beacon frame, bad scan->rates len 200and no overflow. DF-0581's "latent" classification stands. - The AUTH/ASSOC injection harness worked on hostap
(
associd 0xc001assigned to the attacker node), but the hostap destroy path clearsni_associdvianode_leaveβsta_leaveβnode_cleanupbefore the last ref can drop, so the hostap variant of this UAF does not fire; the STA-vap bss node is the vulnerable case (nonode_leaveever runs for it βnode_leaveKASSERTsopmode != 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_freeratectl + cleanup derefs)sys/netproto/802_11/wlan/ieee80211_node.c:1033-1110(node_cleanupvap derefs)sys/netproto/802_11/wlan/ieee80211_node.c:1909-1938(node_reclaimunlink branch that strandsni_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_reclaimonly): insufficient β rebuilt kernel trapped atnode_free+0x18(movq 0x30(%rax),%rax, the ratectlni_vapderef). This usefully proved defects 2/3. - v2 (
fix.diff, validated):IEEE80211_NODE_VAPGONEflag; full vap-dependent cleanup attable_resetwhile the vap is alive;node_freeskips cleanup for flagged nodes. Exact PoC passes; guest survives; module unloads. Recommend upstream also auditsic_node_cleanupcallers-under-lock semantics for HT nodes (the PoC's node was legacy;node_cleanupis now additionally invoked underIEEE80211_NODE_LOCKfromtable_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; hostsys/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.
Fix verification
fixedfix.diff v2 applied in guest /usr/src, make nativekernel + installkernel + reboot (#1 15:53:57); byte-identical PoC re-run: RELEASE_RC=0, associd cleared at detach (0x0 in release log), guest up, dmesg trap-free, module unloads. Baseline on stock #0 traps at _ieee80211_free_node+0x2d. v1 associd-only fix insufficient (trap moved to node_free+0x18 ratectl deref) -- documented in VERDICT.md.
findings/poc/DF-3314/patched_run.txt; findings/poc/DF-3314/panic.txt; findings/poc/DF-3314/VERDICT.md (fix narrative incl. v1 failure)
Confirmed kernel references
- sys/netproto/802_11/wlan/ieee80211_node.c:1786-1790
- sys/netproto/802_11/wlan/ieee80211_node.c:1112-1122
- sys/netproto/802_11/wlan/ieee80211_node.c:1033-1110
- sys/netproto/802_11/wlan/ieee80211_node.c:1909-1938
- sys/netproto/802_11/wlan/ieee80211_node.c:1977-2012
- sys/netproto/802_11/wlan/ieee80211_sta.c:1683
Detail
Exploit chain
STA vap associated (associd set at sta.c:1683) + driver-held node ref in flight -> ifconfig destroy: table_reset drops table ref, node survives unlinked with ni_associd intact, vap kfree'd -> late tx-complete -> ieee80211_free_node -> _ieee80211_free_node reads iv_aid_bitmap from freed vap -> bit-clear write through stale/reclaimed pointer -> fatal trap (or silent corruption of whatever occupies the stale bitmap target). Grooming the freed vap block yields a constrained arbitrary bit-clear write; full uid=0 chain not developed (clear-only primitive).
Evidence (decisive lines)
["panic.txt: 'Fatal trap 9 ... Stopped at _ieee80211_free_node+0x2d: andl %esi,(%r8,%rax,4)' (baseline run 1, with 0xb0 spray of the freed vap block)", 'panic.2.txt: same trap/IP reproduced from a fresh vm.sh reset with-src (deterministic spray variant)', "run.log: full command sequence incl. the negative-evidence rates-IE run ('discard beacon frame, bad scan->rates len 200') and the fw3.sta/spray/release transcript (associd 0xc001, ni_vap freed address)", 'patched_run.txt: fix-v2 kernel, exact PoC, RELEASE_RC=0, associd 0x0 at every late completion, guest up', 'fix.diff: NODE_VAPGONE fix (node.c + node.h), applied in-guest only', 'fw3.c: software-radio KLD (beacon injector, AUTH/ASSOC injector, async-driver ref parking, exact-reclaim spray)']
PoC changes
Seed concept (from pass-2 analysis) was a remote rates-IE overflow via init_neighbor; live testing killed it (parse_beacon VERIFY_ELEMENT guard) and the effort pivoted to the vap-lifetime UAF, which static pass-2 analysis had flagged from the code's own hazard comment. fw3.c written from scratch on the DF-3282/3312 harness pattern; the associd state is set directly on the bss node (substitutes for a full MLME dance with a fake AP; the AUTH/ASSOC injector was also built and used to prove over-the-air associability on hostap). An associd-only fix was tried first and failed (trap moved to node_free+0x18), driving the NODE_VAPGONE v2 fix.
Verified recommended fix
At vap detach, run all vap-dependent node cleanup while ni_vap is still valid (ic_node_cleanup + ratectl deinit in ieee80211_node_table_reset) and mark nodes IEEE80211_NODE_VAPGONE so node_free() skips those calls; clear ni_associd in node_reclaim's unlink branch (fix.diff).
Verdict
Use-after-free of the ieee80211vap when the last node reference outlives vap destruction: ieee80211_node_table_reset() (node.c:1977) unlinks a vap's nodes without clearing ni_associd or performing vap-dependent cleanup, the vap memory is then freed by ic_vap_delete, and the eventual final ieee80211_free_node() dereferences ni->ni_vap in three places (_ieee80211_free_node:1786 AID_CLR bit-clear write through a pointer read from the freed vap; node_free:1117 ratectl deinit; node_cleanup:1041-1082). Reproduced deterministically on the stock INVARIANTS guest with a software-radio KLD emulating an async driver whose tx completions arrive after ifconfig destroy: 'Fatal trap 9 ... Stopped at _ieee80211_free_node+0x2d: andl %esi,(%r8,%rax,4)' -- exactly the AID_CLR write, with the base pointer read from marker-sprayed (0xb0) freed vap memory. A one-line associd-only fix moved the trap to node_free+0x18 (ratectl deref), proving the broader free-path deref family; the validated fix.diff (NODE_VAPGONE flag + full cleanup at table_reset while the vap is alive) makes the byte-identical PoC pass cleanly (RELEASE_RC=0, guest up, no traps). Primitive beyond the panic: a clear-one-bit-of-one-32bit-word write at a small attacker-influenced offset through an attacker-groomable base pointer (freed 2640-byte vap block is reclaimable); uid=0 escalation from a clear-only write was assessed and not achieved this run. Precondition (vap destroy/reset racing outstanding tx completions -- routine admin under load on async-driver hardware) keeps severity at Medium.
No comments yet.