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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5246 Β· 11 files
FileTypeDescriptionSize
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

sys/bus/u4b/wlan/if_run.c

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.

VERDICT.md
↓ download raw

DF-5246 VERDICT β€” beacon_mbuf write-after-free race

Finding

sys/bus/u4b/wlan/if_run.c:

  • 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 to IEEE80211_S_RUN and exit RUNβ†’INIT)
  • run_vap_delete() β€” if_run.c:997-1031
  • m_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 under IEEE80211_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) executes run_newstate β€” true concurrency on SMP;
  • the free happens on every RUN transition (2105 runs when entering RUN too, immediately before run_update_beacon_cb reallocates) 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; usbconfig shows 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: 0 per the honest-classification table (no in-guest kernel manifestation possible).

Exploit chain (what the class permits, hardware permitting)

  1. run(4) AP in hostap mode with power-save clients (TIM updates fire continuously) β€” all remote-802.11-peer triggerable.
  2. Force run_newstate RUN→INIT or a vap destroy at a chosen moment (ifconfig down/destroy, deauth-driven state change) while TIM churn is high.
  3. ieee80211_beacon_update writes into the freed 2 KB mbuf cluster between the free and reallocation.
  4. 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.

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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5246

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.