NULL vap deref in scan_curchan_task: scan state not re-validated after dropping IEEE80211_LOCK across ic_set_channel
| Field | Value |
|---|---|
| ID | DF-0587 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-476 NULL Pointer Dereference |
| File | sys/netproto/802_11/wlan/ieee80211_scan_sw.c |
| Lines | 741-772, 890-920 |
| Area | netproto/802_11 (wlan scanning) |
| Confidence | likely |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
scan_curchan_task releases IEEE80211_LOCK at line 741 to run driver
callbacks ic_set_channel, ieee80211_radiotap_chan_change, and
ic_scan_curchan (sys/netproto/802_11/wlan/ieee80211_scan_sw.c:747-760),
which commonly sleep during hardware channel programming. During that window,
a concurrent vap detach โ ieee80211_scan_vdetach in scan.c โ acquires the
same lock, sets ISCAN_ABORT, and nulls ss->ss_vap and ss->ss_ops.
When scan_curchan_task re-acquires the lock at :761 it never re-validates
ss_vap; control flows through IEEE80211_DPRINTF(ss->ss_vap, โฆ) (:700,
:724, :775 โ unconditional crash on IEEE80211_DEBUG kernels) and, on the
aborted-scan path through scan_done, dereferences vap->iv_flags_ext
(:916), vap->iv_sta_ps (:917), and ieee80211_notify_scan_done(vap) (:920)
unconditionally โ page-fault panic. The code's own comment at :763 admits it:
/* XXX scan state can change! Re-validate scan state! */
Root cause
-
The scan borrows
ss->ss_vapfor the entire scan lifetime with no reference count. -
scan_curchan_taskholdsIEEE80211_LOCKfrom :695, releases it at :741 to run driver callbacksic_set_channel(:747),ieee80211_radiotap_chan_change(:748), andic_scan_curchan(:760), then re-acquires it at :761. -
Concurrently, vap destruction runs
ieee80211_scan_vdetach(sys/netproto/802_11/wlan/scan.c:137-153): underIEEE80211_LOCKit callsswscan_vdetach(setsISCAN_ABORTifF_SCANwas set, sys/netproto/802_11/wlan/ieee80211_scan_sw.c:160), then becausess->ss_vap == vapit callsss->ss_ops->scan_detach, setsss->ss_ops = NULL, and setsss->ss_vap = NULLโ all under the lock thatscan_curchan_taskis not holding between :741 and :761. -
After re-lock at :761 there is no NULL check on
ss_vap/ss_ops:
- :769-773 sees
ISCAN_ABORTand doesgoto end, re-entering the loop at :696-700:IEEE80211_DPRINTF(ss->ss_vap, IEEE80211_MSG_SCAN, โฆ)โieee80211_msg(_vap)expands to(_vap)->iv_debug(sys/netproto/802_11/wlan/ieee80211_var.h), a NULL-vap deref onIEEE80211_DEBUGkernels. - On non-DEBUG kernels the same code at :705 routes to
scan_endwhich at :784 takesvap = ss->ss_vap(NULL) and at :789 derefs it again in DPRINTF. scan_doneat :890 readsvap = ss->ss_vap(NULL) and, whenscandoneis true (scan had reachedss_next >= ss_last, :697), dereferencesvap->iv_flags_ext(:916) /vap->iv_sta_ps(:917) /ieee80211_notify_scan_done(vap)(:920) โ page fault.
ic_set_channel is a driver callback that commonly sleeps while programming
hardware channel state, so the race window is wide and reliably hittable.
Threat model & preconditions
- Attacker position: privileged local user. Vap destruction
(
SIOCIFDESTROY/ network-config caps) is gated bycaps_priv_check_self(SYSCAP_NONET_WIFI). - Privileges gained or impact: kernel panic (local DoS). No code-execution
primitive: the fault is a NULL-vap dereference, and
map_at_zerois off by default on DragonFlyBSD. - Required config or capabilities: any DragonFly system using
netproto/802_11(i.e. anywlan(4)vap with a driver). The race is triggered during ordinary operational reconfiguration (vap destroy/restart,wpa_supplicant/NetworkManagerrestart, driver reload, shutdown) rather than adversarial input. - Reachability: race a vap detach (e.g.
ifconfig wlan0 destroy) against an in-progress scan (IEEE80211_IOC_SCAN_REQ).
Proof of concept
PoC source: findings/poc/DF-0587/race.c
Build & run
cc -O2 -Wall findings/poc/DF-0587/race.c -o findings/poc/DF-0587/race sudo ./findings/poc/DF-0587/race wlan0
Expected output
Kernel panic with the faulting instruction inside scan_done
(vap->iv_flags_ext deref at sys/netproto/802_11/wlan/ieee80211_scan_sw.c:916)
on non-DEBUG kernels, or inside IEEE80211_DPRINTF(ss->ss_vap,โฆ) at :700 on
DEBUG kernels:
Fatal trap 12: page fault while in kernel mode cpuid = ...; apic id = ... fault virtual address = 0x.. [code] scan_done+0x...: mov ...
Reproduces within seconds-to-minutes depending on driver set_channel
latency.
Impact
- Blast radius: any DragonFly system with a
wlan(4)vap. The race fires during ordinary operational teardown/reconfiguration โ including shutdown of a system with an active wireless scan โ making this a real reliability hazard, not merely an adversarial bug. - Severity rationale: Low. Privileged attacker, high race complexity, impact limited to DoS (panic), no code execution.
- Reliability: race window depends on driver
set_channellatency, but that callback commonly sleeps, widening the window.
Recommended fix
After re-acquiring IEEE80211_LOCK at scan_curchan_task:761, re-validate
ss_vap/ss_ops and, if they were cleared by a concurrent vap detach, bail
directly through scan_done() instead of routing through scan_end()/DPRINTF
that dereference vap. Additionally harden scan_done to tolerate vap == NULL
so any other path that reaches it with a cleared ss_vap cannot fault.
--- a/sys/netproto/802_11/wlan/ieee80211_scan_sw.c
+++ b/sys/netproto/802_11/wlan/ieee80211_scan_sw.c
@@ -760,6 +760,20 @@ scan_curchan_task(void *arg, int pending)
ic->ic_scan_curchan(ss, maxdwell);
IEEE80211_LOCK(ic);
+ /* XXX scan state can change! Re-validate scan state! */
+ /*
+ * The vap may have been detached (and ss_vap/ss_ops cleared by
+ * ieee80211_scan_vdetach) while the lock was dropped above for
+ * the driver channel-change callback. Bail straight through
+ * scan_done() rather than dereferencing a NULL vap in the
+ * scan_end()/DPRINTF/scan_done paths.
+ */
+ if (ss->ss_vap == NULL || ss->ss_ops == NULL) {
+ ss_priv->ss_iflags &= ~ISCAN_RUNNING;
+ ss_priv->ss_iflags |= ISCAN_ABORT;
+ scan_done(ss, 1);
+ return;
+ }
+
ss_priv->ss_chanmindwell = ticks + ss->ss_mindwell;
/* clear mindwell lock and initial channel change flush */
ss_priv->ss_iflags &= ~ISCAN_REP;
@@ -908,9 +922,12 @@ scan_done(struct ieee80211_scan_state *ss, int scandone)
* the beacon indicates we have frames
* waiting for us.
*/
- if (scandone) {
+ /*
+ * vap may be NULL if we got here via the scan_curchan_task
+ * re-validation bail-out after the owning vap was detached.
+ */
+ if (scandone && vap != NULL) {
if ((vap->iv_flags_ext & IEEE80211_FEXT_SCAN_OFFLOAD) == 0)
vap->iv_sta_ps(vap, 0);
if (ss->ss_next >= ss->ss_last)
Longer-term the right fix is to acquire a reference on ss_vap for the
duration of the scan (mirroring ieee80211_node refcounts) so the borrowed
pointer cannot be invalidated under the task; the guard above closes the
immediate NULL-deref window with minimal change.
References
- FreeBSD
r288725/ similar scan-state-vs-teardown races in the wlan stack (historical context; DragonFly'snetproto/802_11derives from FreeBSDsys/net80211). - The
XXX scan state can change!comment at sys/netproto/802_11/wlan/ieee80211_scan_sw.c:763 โ original author acknowledgment of the hazard.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).