β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3470

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3470 Β· 4 files
FileTypeDescriptionSize
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:

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 -v lists 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 narrative
  • verdict.json β€” machine verdict (status: inconclusive)
VERDICT.md
↓ download raw

DF-3470 β€” VERDICT

Status: inconclusive on this guest (code-certain from source; impact by construction: panic / execute-after-unmap). Confidence: likely.

The defect

  1. 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-readable rate_stats sysctl attached in this file (ratectl.c:127-129, handler at 88-116) and ir_deinit on vap teardown.
  2. ieee80211_ratectl_unregister() (ratectl.c:66-71) has no in-use accounting: it NULLs the slot unconditionally when a module unloads.
  3. The module glue _IEEE80211_POLICY_MODULE (sys/netproto/802_11/ieee80211_dragonfly.h:316-336) does implement the right guard β€” if (nrefs) ... error = EBUSY on MOD_UNLOAD β€” but the three rate-control algorithm modules declare static 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 -v lists 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 = &amrr user 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.

  1. Arm the guard: each algorithm's ir_init/ir_deinit (or the registry) must maintain nrefs exactly like wlan_wep does β€” or better, move the counter into the registry: ieee80211_ratectl_register/unregister track per-type user counts (incremented in ieee80211_ratectl_set, decremented when a vap switches away / detaches) and ieee80211_ratectl_unregister() refuses (leaves the module loaded) while users exist.
  2. 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_testable
baseline no→ patch + rebuild →patched clean

Fix 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
per-fix-DF-3470

Confirmed kernel references

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