โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0587

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

  1. The scan borrows ss->ss_vap for the entire scan lifetime with no reference count.

  2. scan_curchan_task holds IEEE80211_LOCK from :695, releases it at :741 to run driver callbacks ic_set_channel (:747), ieee80211_radiotap_chan_change (:748), and ic_scan_curchan (:760), then re-acquires it at :761.

  3. Concurrently, vap destruction runs ieee80211_scan_vdetach (sys/netproto/802_11/wlan/scan.c:137-153): under IEEE80211_LOCK it calls swscan_vdetach (sets ISCAN_ABORT if F_SCAN was set, sys/netproto/802_11/wlan/ieee80211_scan_sw.c:160), then because ss->ss_vap == vap it calls ss->ss_ops->scan_detach, sets ss->ss_ops = NULL, and sets ss->ss_vap = NULL โ€” all under the lock that scan_curchan_task is not holding between :741 and :761.

  4. After re-lock at :761 there is no NULL check on ss_vap/ss_ops:

  • :769-773 sees ISCAN_ABORT and does goto 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 on IEEE80211_DEBUG kernels.
  • On non-DEBUG kernels the same code at :705 routes to scan_end which at :784 takes vap = ss->ss_vap (NULL) and at :789 derefs it again in DPRINTF.
  • scan_done at :890 reads vap = ss->ss_vap (NULL) and, when scandone is true (scan had reached ss_next >= ss_last, :697), dereferences vap->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 by caps_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_zero is off by default on DragonFlyBSD.
  • Required config or capabilities: any DragonFly system using netproto/802_11 (i.e. any wlan(4) vap with a driver). The race is triggered during ordinary operational reconfiguration (vap destroy/restart, wpa_supplicant/NetworkManager restart, 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_channel latency, but that callback commonly sleeps, widening the window.

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's netproto/802_11 derives from FreeBSD sys/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).