Write-after-free race on run_vap::beacon_mbuf: run_update_beacon (ic lock) vs run_newstate/run_vap_delete (sc lock) - disjoint lock domains
Summary
run_update_beacon touches rvp->beacon_mbuf under ONLY the net80211 ic lock while run_newstate/run_vap_delete free it under ONLY the sc lock - no common lock: ieee80211_beacon_update writes seq/capinfo/TIM (CSA path: whole beacon) into a freed 2KB mbuf cluster. TIM churn driven by power-save traffic = continuously remote-peer-influenceable in hostap/ibss. Guest simulation (exact code regions, both lock domains): 343k-444k write-after-free interleavings per 10s (~0.35% of updates); slab-groomable cluster cache -> panic-to-uid0 potential on the CSA large overwrite. Hardware-gated (no run adapter in QEMU) -> inconclusive. RUN_LOCK wrap fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5246 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.0 KB | β raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| beacon_race.c | β | 5.7 KB | view raw | |
| build.sh | β | 109 B | view raw | |
| run.sh | β | 123 B | view raw | |
| build.log | β | 262 B | view raw | |
| run.log | β | 811 B | view raw | |
| env.txt | β | 199 B | view raw | |
| fix.diff | β | 696 B | view raw | |
| manifest.json | β | 849 B | view raw | |
| verdict.json | β | 3.1 KB | view raw |
DF-5246 β if_run(4) run_vap::beacon_mbuf write-after-free race
What
run_update_beacon() accesses rvp->beacon_mbuf while holding only the
net80211 ic lock; run_newstate() (RUNβINIT / RUN-entry) and
run_vap_delete() free the same pointer while holding only the driver
sc lock. The two lock sets are disjoint, so the NULL-check
(if_run.c:4887) races against the free (if_run.c:2105-2106 / 1013-1014),
and ieee80211_beacon_update() (if_run.c:4892) then writes through the
stale pointer β write-after-free on a kernel mbuf cluster.
Simulated reproduction (hardware-gated: needs a real run(4) USB adapter)
The guest has if_run.ko and the driver loads, but the QEMU guest has no
USB controller / no Ralink device, so the in-kernel path cannot be
exercised (QEMU cannot emulate a RT2870 vendor-protocol WLAN device).
beacon_race.c instead models the exact code regions and lock domains
(two pthread mutexes standing in for IEEE80211_LOCK(ic) and
RUN_LOCK(sc)) and shows the interleaving is trivially reachable:
build: cc -O2 -pthread -o beacon_race beacon_race.c (build.sh) run: ./beacon_race 10 (run.sh, 3 runs)
Observed on the guest (6 vCPU, DF 6.5-DEVELOPMENT):
run 1: 343,159 UAF hits / 92.5M clean updates run 2: 383,831 UAF hits / 99.2M clean updates run 3: 443,967 UAF hits /122.0M clean updates
i.e. ~0.3β0.4 % of all beacon updates would have written into freed
memory when a state transition / vap destroy lands concurrently.
Every hit is, in the kernel, ieee80211_beacon_update() writing
wh->i_seq (offset 22), bo_caps, TIM bytes β and on the CSA path a
full-beacon rewrite via ieee80211_beacon_construct() β into a freed
mbuf cluster.
Verdict
inconclusive (simulated only; kernel path is hardware-gated),
confidence likely: the missing-lock-intersection is proven statically
from the source; only the dynamic confirmation needs the hardware.
DF-5246 VERDICT β beacon_mbuf write-after-free race
Finding
run_update_beacon()β if_run.c:4861-4901- NULL-check + alloc: 4887-4891
- use:
ieee80211_beacon_update(ni, rvp->beacon_mbuf, mcast)β 4892 - deferred re-arm: 4894-4898
run_newstate()β if_run.c:2048-2180- drops the ic lock, takes only the sc lock: 2067-2068
m_freem(rvp->beacon_mbuf); rvp->beacon_mbuf = NULL;β 2105-2106 (executed on every entry toIEEE80211_S_RUNand exit RUNβINIT)run_vap_delete()β if_run.c:997-1031m_freem(rvp->beacon_mbuf)under the sc lock only: 1013-1014
Callers that reach run_update_beacon with only the ic lock held:
ieee80211_set_tim()β sys/netproto/802_11/wlan/ieee80211_power.c:290-319 (IEEE80211_LOCK(ic)β¦vap->iv_update_beacon(vap, IEEE80211_BEACON_TIM)), invoked from the TX path (ieee80211_pwrsave) for every power-save buffered-frame TIM change in hostap/ibss mode β i.e. continuously, attacker-influenced (a PS client triggers it remotely).ieee80211_beacon_notify()callers (ERP/HT/CSA/WME updates): sys/netproto/802_11/ieee80211_var.h:865, ieee80211_node.c:2444, ieee80211_proto.c:1278/1769, ieee80211_ht.c:1288 β all underIEEE80211_LOCK.
The write side: ieee80211_beacon_update() (sys/netproto/802_11/wlan/
ieee80211_output.c:3339+) writes wh->i_seq, *bo->bo_caps, TIM, and on
the CSA path rewrites the entire beacon via ieee80211_beacon_construct()
through the passed-in mbuf β a write-after-free into a freed mbuf cluster
(MCLEX cluster / pkthdr mbuf, typically 2 KB heap object).
Lock sets at the two conflicting accesses:
| path | locks held | touches beacon_mbuf |
|---|---|---|
| run_update_beacon | {ic} | read + write-through |
| run_newstate | {sc} (ic dropped) | free + NULL |
| run_vap_delete | {sc} | free + NULL |
Intersection is empty β no serialization β classic check-then-use race.
Why it is reachable
- hostap/ibss mode on run(4) (
ifconfig wlan0 create ... mode hostap, hostapd) is a stock configuration; - TIM updates fire from the TX path of other threads while the net80211
state machine task (
iv_nstate_task, sys/netproto/802_11/wlan/ ieee80211_proto.c:2224) executesrun_newstateβ true concurrency on SMP; - the free happens on every RUN transition (2105 runs when entering
RUN too, immediately before
run_update_beacon_cbreallocates) and on every vap destroy.
Simulated verification
See README.md. beacon_race.c models the two lock domains and the exact
check/use/free regions. Result on the guest (3Γ10 s): 343k / 384k / 444k
write-after-free interleavings per run β the window is hit on ~0.35 % of
beacon updates. In-kernel this means a single RUNβINIT transition or
ifconfig wlanX destroy concurrent with beacon traffic has a high
probability of corrupting freed heap memory (contents written: 802.11 seq
numbers, capinfo, TIM bitmap, and on CSA a full beacon frame β partially
attacker/attacker-peer influenced).
Status
status: inconclusiveβ the kernel path itself is hardware-gated (no Ralink USB device can be attached to the QEMU guest;usbconfigshows no USB controller, and QEMU has no RT2870-class device model). The simulation is faithful to the code but is not an in-kernel repro.impact: none (observed); the finding itself is kernel heap write-after-free (severity High, bucket memcorrupt).reproduced: 0per the honest-classification table (no in-guest kernel manifestation possible).
Exploit chain (what the class permits, hardware permitting)
- run(4) AP in hostap mode with power-save clients (TIM updates fire continuously) β all remote-802.11-peer triggerable.
- Force
run_newstateRUNβINIT or a vap destroy at a chosen moment (ifconfig down/destroy, deauth-driven state change) while TIM churn is high. ieee80211_beacon_updatewrites into the freed 2 KB mbuf cluster between the free and reallocation.- Heap grooming (mbuf clusters are a predictable slab) turns the stale write into corruption of a re-allocated victim object; the CSA path gives a large, structured overwrite. Realistic ceiling: kernel memory corruption β panic β potential privesc with sufficient control of post-free reallocation.
Recommended fix
Serialize all beacon_mbuf accesses on the sc lock: have
run_update_beacon() take RUN_LOCK around the check/alloc/update
(usb-control work already happens under the sc lock elsewhere), or move
the free in run_newstate/run_vap_delete behind the same lock the
update path holds (see fix.diff).
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
not_testablefix.diff authored and verified with 'git apply --check' against the pristine sys/ tree, but cannot be kernel-tested: the QEMU guest has no USB controller and no run(4) device model exists, so neither baseline nor patched kernel behavior is exercisable. Hardware-gated: not_testable.
fix.diff (git-apply clean); VERDICT.md
Confirmed kernel references
Detail
Exploit chain
hostap/ibss vap + power-save client TIM churn (remote peer triggerable) racing a RUN->INIT transition or ifconfig destroy -> ieee80211_beacon_update writes into freed 2KB mbuf cluster -> heap corruption; CSA path gives a large structured overwrite -> panic to potential privesc with post-free reallocation control
Evidence (decisive lines)
findings/poc/DF-5246/run.log lines 'UAF hits: 343159/383831/443967' across 3 runs; VERDICT.md lock-intersection table; if_run.c:2067-2068 (ic lock dropped) vs ieee80211_power.c:310-316 (set_tim under ic lock)
PoC changes
Harness written from scratch (no seed): pthread mutexes model IEEE80211_LOCK(ic) and RUN_LOCK(sc); free() poisons via a live flag so the process survives while counting kernel-equivalent UAF writes
Verified recommended fix
Take RUN_LOCK around the beacon_mbuf check/alloc/use in run_update_beacon()
Verdict
Proven-by-lock-analysis use-after-free: run_update_beacon() (if_run.c:4887-4892) touches rvp->beacon_mbuf under only the net80211 ic lock, while run_newstate() (2105-2106) and run_vap_delete() (1013-1014) free the same mbuf under only the driver sc lock - the lock sets are disjoint, so ieee80211_beacon_update() writes seq/capinfo/TIM (and on CSA a full beacon rewrite) into a freed mbuf cluster. Simulated on the guest with a harness that mirrors the exact code regions and lock domains: 343k-444k write-after-free interleavings per 10 s run (~0.35% of beacon updates). In-kernel reproduction is hardware-gated (no run(4) adapter can be attached to the QEMU guest), hence inconclusive.
No comments yet.