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

Beacon mbuf use-after-free write: urtwn_update_beacon's unlocked ieee80211_beacon_update window races setup_beacon/vap_delete across disjoint lock domains

Summary

update_beacon RELEASES sc_mtx around ieee80211_beacon_update (TIM/CSA writes INTO the mbuf under only the com lock) while setup_beacon frees/swaps under sc (com lock dropped at :2551) and vap_delete frees with NO lock at all - write-after-free on a 2KB mbuf cluster at attacker-paced frequency. IBSS variant remotely triggerable: crafted beacons with larger TSF -> ibss_merge -> newstate(S_RUN) -> setup_beacon free/swap, timed against PS-traffic TIM writes. Twin of the if_run.c DF-5246 family. Guest race_sim (exact lock choreography, zone-allocator emulation, unpriv user): 6/6 genuine write-after-frees incl. the vap_delete variant; hardware-gated -> inconclusive. Hold-lock-across-update + lock-the-vap_delete-free fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5254 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 5.3 KB ↓ raw
race_sim.c β€” 7.6 KB view raw
build.sh β€” 99 B view raw
run.sh β€” 93 B view raw
build.log β€” 230 B view raw
run.log β€” 3.4 KB view raw
run.2.log β€” 669 B view raw
env.txt β€” 343 B view raw
fix.diff β€” 979 B view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 4.3 KB view raw

DF-5254 β€” urtwn beacon mbuf use-after-free (disjoint lock domains)

What

urtwn_update_beacon() manipulates uvp->bcn_mbuf with the driver mutex (sc_mtx) released while it calls ieee80211_beacon_update() (sys/bus/u4b/wlan/if_urtwn.c:2206-2212). Two other paths free/replace that same mbuf under different (or no) lock domains:

  • urtwn_setup_beacon() β€” m_freem(uvp->bcn_mbuf); uvp->bcn_mbuf = m; (if_urtwn.c:2168-2171), reached from urtwn_newstate() after it drops the net80211 com lock (IEEE80211_UNLOCK(ic), if_urtwn.c:2551) and takes sc_mtx. Runnable S_RUN re-entry is attacker-reachable in IBSS mode: a crafted beacon with a larger TSF triggers ieee80211_ibss_merge() β†’ ieee80211_sta_join1() β†’ full state-machine re-entry β†’ RUN (netproto/802_11/wlan/ieee80211_node.c:610-630).
  • urtwn_vap_delete() β€” m_freem(uvp->bcn_mbuf) with no lock at all (if_urtwn.c:840-841).

Meanwhile iv_update_beacon is invoked with the com lock held (e.g. ieee80211_set_tim(), netproto/802_11/wlan/ieee80211_power.c:310-318, driven by station power-save traffic β€” i.e. remotely paced) but not sc_mtx, and ieee80211_beacon_update() writes TIM/CSA fields into the mbuf. No lock orders the writer (A) against the freer (B/C): A holds com, B holds sc, C holds nothing β†’ classic use-after-free write on an mbuf cluster (m_freem returns the cluster to the mbuf cluster pool immediately).

Reproduce

Hardware-gated on-guest (no RTL8188CUS/EU USB device attached to the QEMU guest, kldstat shows urtwn not loaded), so this pack contains a faithful two-lock-domain race simulation of the exact interleaving instead:

./build.sh          # cc -O2 -pthread -Wall -o race_sim race_sim.c
./run.sh            # 3-5 runs x 5s; exit code 3 == UAF write detected

Expected (observed 6/6 clean runs on the guest, unprivileged user):

[UAF-WRITE] thread A wrote into a freed beacon mbuf (gen NNN) --
   freed by concurrent setup_beacon/vap_delete while A was inside
   ieee80211_beacon_update() with sc_mtx released (if_urtwn.c:2206-2212)
RESULT: USE-AFTER-FREE WRITE reproduced
exit=3

run.2.log shows the vap_delete variant firing (C frees = 1).

Fix

See fix.diff: hold URTWN_LOCK(sc) across ieee80211_beacon_update() (matching the driver's own locking for urtwn_tx_beacon), and take URTWN_LOCK(sc) around the free in urtwn_vap_delete().

Files

  • race_sim.c β€” pthread model of the interleaving (zone-allocator emulation, generation counters; stale-generation write == write-after-free)
  • build.sh, run.sh, build.log, run.log, run.2.log, env.txt
  • fix.diff, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-5254 β€” VERDICT

Finding: use-after-free write on the urtwn beacon mbuf (struct urtwn_vap.bcn_mbuf) because urtwn_update_beacon() releases the driver mutex around ieee80211_beacon_update() while urtwn_setup_beacon() / urtwn_vap_delete() free-and-replace the same mbuf from lock domains that are not ordered against it.

Reproduced?

On-guest kernel impact: NOT reproduced β€” hardware-gated. The guest has no Realtek RTL8188CUS/EU/8192CU USB device (kldstat shows if_urtwn.ko present at /boot/kernel/if_urtwn.ko but not loaded; no USB WLAN NIC in usbdevs). urtwn_attach() cannot run, so the kernel race cannot be fired directly. Per the audit protocol this is recorded as inconclusive with strong simulated evidence.

Simulated evidence: reproduced 6/6 clean runs (100%). race_sim.c models the exact interleaving with three pthreads mirroring the three code paths:

sim thread kernel path locks modelled
A thread_update_beacon ieee80211_set_tim (com held) β†’ urtwn_update_beacon if_urtwn.c:2196-2216 com held; sc taken/dropped exactly at 2206/2214
B thread_newstate IBSS merge β†’ urtwn_newstate (com dropped at 2551) β†’ urtwn_setup_beacon 2168-2171 sc held, com dropped
C thread_vap_delete urtwn_vap_delete if_urtwn.c:840-841 no locks

A zone-allocator emulation (free-list + generation counters) marks a cluster dead at m_freem(); any subsequent write is a genuine write-after-free. Observed on guest (DragonFly 6.5-DEVELOPMENT, gcc 8.3, 6 vCPU, unprivileged user):

run.log   5/5 runs exit=3 (UAF write detected; gens 220,184,186,404,403)
run.2.log 1/1  exit=3, killer = thread C (vap_delete variant, "vap_delete frees (C): 1")

~200 newstate re-entries and ~5000 update iterations per 5s run are enough to hit the window every time; on real hardware the B path is paced by remote IBSS merge beacons and the A path by station power-save TIM updates (both attacker-controllable timing), so the window (the whole duration of ieee80211_beacon_update() on the old pointer, if_urtwn.c:2212) is realistic.

Why it is a bug (source trace)

  1. sys/netproto/802_11/wlan/ieee80211_power.c:310-318 β€” ieee80211_set_tim() holds IEEE80211_LOCK(ic) (com) and calls vap->iv_update_beacon(vap, IEEE80211_BEACON_TIM).
  2. sys/bus/u4b/wlan/if_urtwn.c:2196-2216 β€” urtwn_update_beacon(): takes sc_mtx (2196), ensures uvp->bcn_mbuf != NULL (2197-2205), releases sc_mtx (2206), then ieee80211_beacon_update(ni, uvp->bcn_mbuf, mcast) (2212) writes into the mbuf, then re-takes sc_mtx (2214) for urtwn_tx_beacon.
  3. sys/bus/u4b/wlan/if_urtwn.c:2551-2552 β€” urtwn_newstate() drops the com lock before taking sc_mtx, so its call into urtwn_setup_beacon() (2161-2171: m_freem(uvp->bcn_mbuf); uvp->bcn_mbuf = m;) is not serialized against step 2 by either lock: A holds com (not sc), B holds sc (not com).
  4. sys/bus/u4b/wlan/if_urtwn.c:2667-2676 β€” setup_beacon runs for both HOSTAP and IBSS opmodes on every S_RUN entry; IBSS S_RUN re-entry is remotely triggerable via ieee80211_ibss_merge() β†’ ieee80211_sta_join1() (netproto/802_11/wlan/ieee80211_node.c:630).
  5. sys/bus/u4b/wlan/if_urtwn.c:840-841 β€” urtwn_vap_delete() frees uvp->bcn_mbuf with no lock whatsoever (Race variant C).

ieee80211_beacon_update() writes TIM bitmap / seq / CSA values into the mbuf (netproto/802_11/wlan/ieee80211_output.c:3339+), i.e. the use-after-free is a write primitive into a freed mbuf cluster (2 KB), repeated at attacker-paced frequency. The twin of this bug family was confirmed in the if_run.c pass-2 audit (beacon lifecycle / disjoint-lock-domain).

Exploitability sketch (why High)

  • Object: mbuf cluster (2 KB) in a global cluster pool β€” freed clusters are recycled aggressively on any network load, so the write lands in a live adjacent object quickly (cross-object corruption, not a write-to-free-page).
  • Control: attacker controls when (TIM update pacing via PS traffic + merge beacons) and content partially (TIM bitmap bytes, beacon contents from the vap's own beacon template).
  • Gate: needs a urtwn USB NIC in hostap/ibss operation; attacker is a nearby peer (IBSS: fully unauthenticated).
  • Ceiling: kernel memory corruption from an unauthenticated remote peer β†’ panic now, potentially more with cluster-pool grooming.

Fix validation

Not kernel-tested (no hardware on the guest). fix.diff keeps URTWN_LOCK(sc) held across ieee80211_beacon_update() and locks the free in urtwn_vap_delete(); the simulated harness run with the fix's locking (sc held across A's write phase) shows 0 detections by construction, since B/C can no longer free while A writes.

Bottom line

status: inconclusive (hardware-gated kernel PoC), reproduced=false, impact on-guest=none; the code-level defect is certain to exist from the source trace and the simulated interleaving reproduces the UAF write in 6/6 runs. Recommended severity: High (memcorrupt bucket).

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 is git-apply-able against sys/ and removes the unlocked window by construction (B/C serialize on sc_mtx with the writer); kernel build+device test not possible on this guest (no USB WLAN hardware) so not_testable

findings/poc/DF-5254/fix.diff (git apply --check PASSED); locking argument in VERDICT.md
↓ fix.diffper-fix-DF-5254

Confirmed kernel references

Detail

Exploit chain

unauthenticated remote peer (IBSS) or local admin-triggered state churn -> (1) attacker sends crafted beacons with larger TSF to force ieee80211_ibss_merge -> newstate(S_RUN) re-entry -> urtwn_setup_beacon m_freem(bcn_mbuf) under sc_mtx; concurrently (2) station power-save traffic drives ieee80211_set_tim -> urtwn_update_beacon -> ieee80211_beacon_update writes TIM/CSA bytes into the freed 2KB mbuf cluster -> cross-object heap corruption (cluster pool recycles immediately) -> crash today, controlled-content write into recycled kernel object with pool grooming. Simulated chain proven at interleaving level (exit=3, 6/6); hardware chain not exercised.

Evidence (decisive lines)

race_sim.c + run.log (5/5 runs exit=3 UAF write detected, gens 220/184/186/404/403), run.2.log (vap_delete variant, C frees=1), VERDICT.md source trace with path:line for all three racing paths, fix.diff (git apply --check clean)

PoC changes

No kernel PoC possible (hardware-gated); built race_sim.c from scratch: pthread model of the exact lock choreography (com held across update path, sc dropped at 2206, com dropped at 2551, no-lock vap_delete), zone-allocator emulation with generation counters so a stale-generation write is a true write-after-free. Iterations: fixed aligned_alloc size-misuse crash, made vap_delete thread leave the field dangling (as the kernel does) instead of NULLing it.

Verified recommended fix

Hold URTWN_LOCK(sc) across ieee80211_beacon_update() in urtwn_update_beacon() and take URTWN_LOCK(sc) around m_freem(uvp->bcn_mbuf) in urtwn_vap_delete() (see fix.diff)

Verdict

Use-after-free WRITE on struct urtwn_vap.bcn_mbuf: urtwn_update_beacon() releases sc_mtx around ieee80211_beacon_update() (if_urtwn.c:2206-2212, writer holds only the net80211 com lock via iv_update_beacon callers such as ieee80211_set_tim) while urtwn_setup_beacon() frees-and-swaps the same mbuf under sc_mtx with the com lock dropped (urtwn_newstate if_urtwn.c:2551-2552, setup at 2168-2171; IBSS S_RUN re-entry is remotely triggerable via ieee80211_ibss_merge->ieee80211_sta_join1), and urtwn_vap_delete() frees it with no lock at all (840-841). No lock orders writer against freer. On-guest kernel PoC is hardware-gated (no RTL8188 USB NIC on the QEMU guest; if_urtwn.ko present but unloadable without a device), so kernel-level reproduction is honestly recorded as inconclusive; a faithful three-thread two-lock-domain simulation of the exact interleaving reproduced the write-after-free in 6/6 runs on the guest as an unprivileged user, including the vap_delete variant (run.2.log). Severity High (memcorrupt), twin of the if_run.c pass-2 beacon-lifecycle family.