Ratectl registry has no in-use accounting: nrefs unload-guard exists but amrr/rssadapt/none never increment it β kldunload of an in-use algorithm leaves every vap->iv_rate dangling into unmapped module text; next ir_* indirect call (world-readable rate_stats sysctl, any TX, vap detach) executes unmapped code
Summary
ieee80211_ratectl_set stores a raw module-resident pointer in every vap and unregister NULLs the registry slot with no user check. The policy-module glue refuses MOD_UNLOAD with EBUSY when nrefs!=0 but amrr - the DEFAULT algorithm - rssadapt and none declare static int nrefs=0 and never touch it again, unlike the correctly-armed wep/tkip/scan_sta modules. kldunload wlan_amrr while vaps exist succeeds and unmaps the text; every vap->iv_rate dangles: an unpriv user reading sysctl net.wlan.0.rate_stats (CTLFLAG_RD) or any packet TX panics the kernel; if the vacated module range is reoccupied by a later kldload the call target becomes attacker-influenced module content. NOT runnable on the audit guest (wlan_amrr compiled into the stock kernel; no 802.11 hardware to attach a vap) - honestly inconclusive; mechanism proven from source (nrefs occurrence counts, guard macro, consumer chain in pack). Fix: arm the nrefs guard in amrr/rssadapt/none init/deinit like the sibling modules.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3470 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.1 KB | β raw | |
| analysis.txt | β | 3.2 KB | view raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| verdict.json | β | 3.8 KB | view raw |
DF-3470 β net80211 ratectl registry has no in-use accounting: kldunload of a
rate-control algorithm in use leaves every vap's iv_rate dangling into
unmapped module text
What
sys/netproto/802_11/wlan/ieee80211_ratectl.c keeps
static const struct ieee80211_ratectl *ratectls[IEEE80211_RATECTL_MAX]
and hands raw pointers (vap->iv_rate = ratectls[type],
ieee80211_ratectl.c:152) to vaps, which are then invoked through
unprotected inline wrappers on hot paths
(ieee80211_ratectl.h:69-124: ir_rate on every TX,
ir_node_init on association, ir_node_stats via the world-readable
net.wlan.<n>.rate_stats sysctl handler at ieee80211_ratectl.c:127-129 /
88-116, ir_deinit during vap detach).
ieee80211_ratectl_unregister() (ieee80211_ratectl.c:66-71) blindly NULLs
the registry slot; it neither checks for nor prevents unload while vaps
still reference the algorithm. The intended guard exists in the module
glue β _IEEE80211_POLICY_MODULE
(sys/netproto/802_11/ieee80211_dragonfly.h:316-336) refuses MOD_UNLOAD
with EBUSY when nrefs != 0 β but the rate-control algorithm modules
never increment it:
sys/netproto/802_11/wlan/ieee80211_amrr.c:77βstatic int nrefs = 0;and nothing else (grep: single occurrence).sys/netproto/802_11/wlan/ieee80211_rssadapt.c:97β same.sys/netproto/802_11/wlan/ieee80211_ratectl_none.c:96β same.
Contrast with the modules that got the pattern right:
sys/netproto/802_11/wlan_wep/ieee80211_crypto_wep.c:108,118-119
(nrefs++ / nrefs-- in attach/detach) and
sys/netproto/802_11/wlan/ieee80211_scan_sta.c:178,195-196.
amrr is the default algorithm (ieee80211_ratectl.c:121-122).
Consequence
kldunload wlan_amrr (or wlan_rssadapt / a module-built wlan_none) while
any vap exists succeeds (nrefs==0), unmaps the module, and leaves
vap->iv_rate pointing into unmapped memory. The next rate-control
operation β an unprivileged sysctl net.wlan.0.rate_stats read, any
packet TX, a new association, or vap teardown β performs an indirect call
through the dangling table: deterministic panic (execute-after-unmap). If
the vacated module address range is subsequently reoccupied by another
kldload, the call target becomes attacker-influenced module content.
amrr is the default ratectl, so every vap on a system where wlan_amrr
is a loadable module is exposed.
Verification status on the guest
Code-certain, runtime-not-demonstrable on this guest, honestly inconclusive:
- the guest kernel has wlan_amrr compiled in
(
kldstat -vlists it under kernel id 1;kldunload wlan_amrr-> "can't find file"), so the unload cannot even be attempted; - the guest has no 802.11 hardware, so no vap can be created to hold
iv_rate = &amrr(see VERDICT.md).
The mechanism is fully established by source: the guard exists, the increment is missing in exactly the three rate-control modules, and the registry (this file) provides no backstop.
Files
analysis.txtβ the decisive greps (guest + tree)VERDICT.mdβ full narrativeverdict.jsonβ machine verdict (status: inconclusive)
DF-3470 β VERDICT
Status: inconclusive on this guest (code-certain from source; impact by construction: panic / execute-after-unmap). Confidence: likely.
The defect
ieee80211_ratectl_set()stores a raw pointer into a loadable module into every vap:vap->iv_rate = ratectls[type](sys/netproto/802_11/wlan/ieee80211_ratectl.c:152). All later use is via unguarded inline indirect calls (sys/netproto/802_11/ ieee80211_ratectl.h:69-124), including the world-readablerate_statssysctl attached in this file (ratectl.c:127-129, handler at 88-116) andir_deiniton vap teardown.ieee80211_ratectl_unregister()(ratectl.c:66-71) has no in-use accounting: it NULLs the slot unconditionally when a module unloads.- The module glue
_IEEE80211_POLICY_MODULE(sys/netproto/802_11/ieee80211_dragonfly.h:316-336) does implement the right guard βif (nrefs) ... error = EBUSYon MOD_UNLOAD β but the three rate-control algorithm modules declarestatic int nrefs = 0;and never increment it (ieee80211_amrr.c:77 β the default algorithm, ieee80211_rssadapt.c:97, ieee80211_ratectl_none.c:96), while sibling modules show the intended pattern (wlan_wep/ieee80211_crypto_wep.c:108/118-119, ieee80211_scan_sta.c:178/195-196).
So kldunload wlan_amrr (or rssadapt / none, where built as modules)
succeeds while vaps actively use the algorithm: the module unmaps and
every vap->iv_rate on the system dangles. The next ratectl operation
(TX β ir_rate, association β ir_node_init, an unprivileged
sysctl net.wlan.0.rate_stats read β ir_node_stats, vap detach β
ir_deinit) is an indirect call into unmapped text β deterministic
panic; with the address range later reoccupied by another kldload, the
call target becomes attacker-influenced module content.
Why runtime reproduction was not possible on this guest
- The guest kernel has wlan_amrr compiled in (
kldstat -vlists it under kernel id 1;kldunload wlan_amrrβ "can't find file"), so the unload precondition cannot be created. - The guest has no 802.11 hardware (QEMU vtnet only) β no vap can be
attached, so no
iv_rate = &amrruser exists to dangle. All DF wifi drivers require PCI/USB radio hardware.
Honest classification: inconclusive β mechanism and guard-bypass proven
from source (analysis.txt), impact (panic) follows by construction, but
not demonstrated end-to-end on this guest.
Recommended fix (two layers)
- Arm the guard: each algorithm's
ir_init/ir_deinit(or the registry) must maintainnrefsexactly like wlan_wep does β or better, move the counter into the registry:ieee80211_ratectl_register/unregistertrack per-type user counts (incremented inieee80211_ratectl_set, decremented when a vap switches away / detaches) andieee80211_ratectl_unregister()refuses (leaves the module loaded) while users exist. - Defense in depth:
ieee80211_ratectl_unregister()should never run while vaps reference the algorithm; at minimum kprintf loudly and leak the registration rather than dangle every vap.
Fix validation
Not performed: the trigger cannot be constructed on this guest (no wifi hardware; algorithm compiled in). fix_status: not_testable.
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.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testableFix validation not possible on this guest: the unload precondition (loadable wlan_amrr + attached vap) cannot be constructed (algorithm compiled into the kernel; no 802.11 hardware).
n/a
Confirmed kernel references
- sys/netproto/802_11/wlan/ieee80211_ratectl.c:58
- sys/netproto/802_11/wlan/ieee80211_ratectl.c:66
- sys/netproto/802_11/wlan/ieee80211_ratectl.c:152
- sys/netproto/802_11/wlan/ieee80211_ratectl.c:127
- sys/netproto/802_11/ieee80211_dragonfly.h:316
- sys/netproto/802_11/wlan/ieee80211_amrr.c:77
- sys/netproto/802_11/wlan/ieee80211_rssadapt.c:97
- sys/netproto/802_11/wlan/ieee80211_ratectl_none.c:96
- sys/netproto/802_11/wlan_wep/ieee80211_crypto_wep.c:108
Detail
Exploit chain
root: kldunload wlan_amrr while any vap is attached (registry shows no users; unload succeeds) -> module text unmapped, vap->iv_rate dangling -> unprivileged: sysctl net.wlan.0.rate_stats (CTLFLAG_RD, no privilege) or any packet TX -> indirect call through unmapped function-pointer table -> kernel panic; if the vacated module address range is reoccupied by a later kldload, call target becomes attacker-influenced module content.
Evidence (decisive lines)
['analysis.txt (nrefs occurrence counts across netproto/802_11; the unused guard macro; the dangling consumer chain; guest kldstat/kldunload facts)', 'VERDICT.md']
PoC changes
No seed PoC; this is a source-proven lifetime defect. A runtime trigger would require (a) wlan_amrr as a loadable module and (b) one attached vap - neither exists on the audit guest.
Verified recommended fix
Arm the nrefs guard in amrr/rssadapt/none (mirror wep: nrefs++ in ir_init, nrefs-- in ir_deinit) or move per-type user accounting into the registry so ieee80211_ratectl_unregister refuses while vaps reference the algorithm.
Verdict
net80211 rate-control registry hands raw module-resident pointers to vaps (ratectl.c:152) with no in-use accounting in ieee80211_ratectl_unregister (ratectl.c:66-71); the POLICY_MODULE nrefs/EBUSY unload guard exists (ieee80211_dragonfly.h:316-336) but amrr/rssadapt/none never increment nrefs (amrr.c:77 single occurrence; wep.c:108/scan_sta.c:178 show the intended pattern), so kldunload of an in-use algorithm unmaps the module while every vap->iv_rate dangles - the next ir_* indirect call (unprivileged world-readable rate_stats sysctl read, any TX, association, vap detach) executes unmapped text => panic / execute-after-unmap. Runtime reproduction impossible on this guest: wlan_amrr is compiled into the kernel (kldunload: 'can't find file') and no 802.11 hardware exists to attach a vap; mechanism proven from source (analysis.txt).
No comments yet.