# DF-0587 — VERDICT

**Verdict: INCONCLUSIVE (runtime) — code-level trace CONFIRMS the bug is real.**
**Status: `not_reproduced` (impact: panic if reachable) — bug is unreachable on this guest.**

The PoC cannot trigger on the audit guest because the QEMU/KVM guest has **no
IEEE 802.11 radio hardware** — there is no `wlan(4)` vap and no parent
`ieee80211com` device to create one against, so the cited code path
(`scan_curchan_task` ↔ `ieee80211_scan_vdetach` race) is **never reached at
runtime**. The wlan framework *is* compiled into the default GENERIC kernel
(`options IEEE80211_DEBUG`, `device wlan` — verified in
`sys/config/X86_64_GENERIC:255-258`), so the vulnerable code is present and
would be live on any DragonFly system with a real wireless adapter; it is just
not exercisable on this particular VM.

A line-by-line trace of the cited source confirms the bug is genuine — this is
classification **(d) "genuinely not reachable on this guest at runtime"**, not
a false positive.

---

## Mechanism (code-level trace)

The finding's race is exactly as described. Every hop verified against the
audited `sys/` tree:

1. **`scan_curchan_task`** (`sys/netproto/802_11/wlan/ieee80211_scan_sw.c:686`)
   acquires `IEEE80211_LOCK(ic)` at **:695**, then at **:741** drops it to call
   driver callbacks `ic_set_channel` (:747), `ieee80211_radiotap_chan_change`
   (:748), and `ic_scan_curchan` (:760) — all of which commonly sleep while
   the radio is re-tuned. It re-acquires the lock at **:761**.

2. **Concurrent vap detach** runs `ieee80211_scan_vdetach(vap)`
   (`sys/netproto/802_11/wlan/ieee80211_scan.c:137-155`) — verified present
   (the finding cites `scan.c` but the function actually lives in
   `ieee80211_scan.c`; the body is identical to the description). Under
   `IEEE80211_LOCK(ic)` it:
   - calls `ic->ic_scan_methods->sc_vdetach(vap)` = `ieee80211_swscan_vdetach`
     (`ieee80211_scan_sw.c:151-161`), which does
     `scan_signal_locked(ss, ISCAN_ABORT)` if `IEEE80211_F_SCAN` is set —
     setting the abort flag in `ss_priv->ss_iflags` (:602).
   - then, because `ss->ss_vap == vap`, calls `ss->ss_ops->scan_detach(ss)`,
     sets `ss->ss_ops = NULL`, and sets `ss->ss_vap = NULL`
     (`ieee80211_scan.c:147-153`).
   All under the same lock the task is **not** holding between :741 and :761.

3. **No re-validation after re-lock.** At `scan_curchan_task:761` the lock is
   reacquired and the comment `/* XXX scan state can change! Re-validate scan
   state! */` sits at :763 — but **no re-validation follows**. Control flows
   to :769 where the freshly-set `ISCAN_ABORT` triggers `goto end` (:772),
   looping back to the `end:` label at :696. The next statement is:

   ```c
   IEEE80211_DPRINTF(ss->ss_vap, IEEE80211_MSG_SCAN, "%s: loop start; ...");
   ```

   On **`IEEE80211_DEBUG`** kernels (which the default `X86_64_GENERIC`
   **is** — `sys/config/X86_64_GENERIC:255`, verified),
   `IEEE80211_DPRINTF` expands to
   `if (((_vap)->iv_debug & (_m))) ieee80211_note(...)`
   (`sys/netproto/802_11/ieee80211_var.h:956-960`). With `ss->ss_vap == NULL`
   this is a NULL page-fault — **panic**.

4. **On non-DEBUG kernels** the DPFIFs at :700/:724/:775 are no-ops
   (`ieee80211_var.h:1028`: `#define IEEE80211_DPRINTF(...) do { } while (0)`),
   so the code instead falls through :705 (`ISCAN_ABORT` set) to
   `scan_end(ss, scandone)` (:709), which reads `vap = ss->ss_vap` (NULL) at
   :784. `scan_end` sees `ISCAN_ABORT` (:791) and routes to
   `scan_done(ss, scandone)` (:792). `scan_done` (:886) reads
   `vap = ss->ss_vap` (NULL) at :890, and if `scandone` is true (which the
   loop set from `ss->ss_next >= ss->ss_last` at :697) it dereferences
   `vap->iv_flags_ext` (:916), `vap->iv_sta_ps(vap, 0)` (:917), and calls
   `ieee80211_notify_scan_done(vap)` (:920) — page fault, **panic**.

The race window is wide because `ic_set_channel` is a driver callback that
commonly sleeps while programming the radio. The bug admits NULL-deref → panic
only; `map_at_zero` is not a thing on DragonFly (`vm.map_at_zero` does not
exist as a sysctl; the zero page is unmapped), so there is no path from this
primitive to code execution. Impact ceiling = local DoS / panic. Severity Low
is appropriate (privileged attacker — vap destroy is gated by
`caps_priv_check_self(SYSCAP_NONET_WIFI)` — and a narrow race window).

## Exploit chain

**Not applicable** — this is a NULL-deref / CWE-476. No memory-corruption
primitive is acquired; the only effect is a kernel panic (DoS). No escalation
chain to develop.

## Why the PoC cannot trigger on this guest

- The guest is QEMU/KVM with virtio-net (`vtnet0`) and loopback (`lo0`) only.
- `ifconfig wlan0` ⇒ "interface wlan0 does not exist".
- Creating a vap requires `ifconfig wlan create wlandev <parent>` where
  `<parent>` is a WiFi radio driver (`run0`, `ath0`, `iwm0`, …). The guest
  has no such parent device (`SIOCIFCREATE2: Device not configured`).
- All wifi driver modules in `/boot/kernel/` are firmware blobs or hardware
  drivers that require real PCI/USB adapters; none bind to emulated QEMU HW.
- Result: `scan_curchan_task` is never scheduled, `ss->ss_vap` never becomes
  non-NULL inside a real scan, the unlock/lock window never opens. The PoC's
  ioctl loops return `ENXIO`/`ENODEV` silently and time out.

This is a runtime reachability limitation of the audit guest, **not** a
property of the bug. On any DragonFly system with a wlan vap (which is the
default deployment target of the wlan stack — `wpa_supplicant`/NetworkManager
restarts, system shutdown with active scan, vap destroy/recreate), the race
fires during ordinary operational teardown.

## PoC changes

- **`race.c:18`** — fixed include path
  `<netproto/802_11/wlan/ieee80211_ioctl.h>` →
  `<netproto/802_11/ieee80211_ioctl.h>` (the header lives one level up from
  `wlan/`; verified at `/usr/include/netproto/802_11/ieee80211_ioctl.h` and
  `/usr/src/sys/netproto/802_11/ieee80211_ioctl.h`).
- **build line** — added `-lpthread` (DragonFly's `cc` does not auto-link
  pthreads; without it the link fails with `undefined reference to
  'pthread_create'`).
- `build.sh`, `run.sh` — added (exact runnable commands).
- `fix.diff` — added (authored post-verification; supersedes/refines the
  finding markdown's `## Recommended fix` proposal — see below).

## fix.diff

Authored against the actual file (the finding's diff context did not match
the on-disk layout, which has the `/* XXX scan state can change! */` comment
between the re-lock and `ss_chanmindwell`). The verified fix:

1. **`scan_curchan_task:774`** (after re-lock at :761) — add a NULL check on
   `ss->ss_vap` / `ss->ss_ops`. If either was cleared by a concurrent vap
   detach during the unlocked `ic_set_channel` window, clear `ISCAN_RUNNING`,
   set `ISCAN_ABORT`, and bail straight through `scan_done(ss, 1)` — which
   only manipulates `ic->ic_flags` and `ss_priv->ss_iflags`, never touching
   `vap`. This avoids the loop-top DPRINTF at :700 (DEBUG kernels) and the
   `scan_end`/`scan_done` vap-deref path (any kernel).
2. **`scan_done:931`** — defense-in-depth: harden `if (scandone)` to
   `if (scandone && vap != NULL)` so that *any* other path that reaches
   `scan_done` with a cleared `ss_vap` cannot fault at :916/:917/:920.

This **matches the finding proposal** in intent and structure, with the diff
context corrected to apply cleanly (`git apply --check` passes;
`patch -p1 --forward` applies both hunks: "Hunk #1 succeeded at 762.",
"Hunk #2 succeeded at 924.").

## Fix validation (Phase 8)

- `git apply --check` on host: PASS.
- Applied to in-guest `/usr/src`, both hunks applied cleanly.
- Built single-fix kernel `make -j6 nativekernel KERNCONF=X86_64_GENERIC`
  → `rc=0`, no errors (`fix_build.log`, 35707 lines).
- Installed `/usr/obj/.../kernel.stripped` → `/boot/kernel/kernel` (sha256
  `fe5df37c792efe5437521e6e60d8cf3b799661c91d2ac2454e1f29c752365ee1`),
  booted `kern.version = DragonFly 6.5-DEVELOPMENT #1: Mon Jul 13 01:25:53
  UTC 2026`.
- **`fix_status = not_testable`**: empirical before/after re-test is
  impossible because the bug is unreachable on this guest (no WiFi hardware).
  Validated that the diff applies, compiles, and the patched source contains
  the expected guards (`grep` confirms
  `774: if (ss->ss_vap == NULL || ss->ss_ops == NULL)` and
  `931: if (scandone && vap != NULL)` in
  `/usr/src/sys/netproto/802_11/wlan/ieee80211_scan_sw.c`). The static trace
  shows the patched code closes the cited path.

## References (verified path:line)

- `sys/netproto/802_11/wlan/ieee80211_scan_sw.c:686` — `scan_curchan_task`
- `sys/netproto/802_11/wlan/ieee80211_scan_sw.c:741` — `IEEE80211_UNLOCK(ic)` (race window opens)
- `sys/netproto/802_11/wlan/ieee80211_scan_sw.c:747-760` — driver callbacks run unlocked
- `sys/netproto/802_11/wlan/ieee80211_scan_sw.c:761` — `IEEE80211_LOCK(ic)` (race window closes)
- `sys/netproto/802_11/wlan/ieee80211_scan_sw.c:763` — `/* XXX scan state can change! Re-validate scan state! */` (admits the hazard; no check follows)
- `sys/netproto/802_11/wlan/ieee80211_scan_sw.c:700,724,775` — `IEEE80211_DPRINTF(ss->ss_vap, ...)` (NULL-vap deref on DEBUG)
- `sys/netproto/802_11/wlan/ieee80211_scan_sw.c:151-161` — `ieee80211_swscan_vdetach` (sets `ISCAN_ABORT`)
- `sys/netproto/802_11/wlan/ieee80211_scan_sw.c:886-927` — `scan_done` (NULL-vap deref at :916/:917/:920 on any kernel)
- `sys/netproto/802_11/wlan/ieee80211_scan.c:137-155` — `ieee80211_scan_vdetach` (clears `ss_vap`/`ss_ops`)
- `sys/netproto/802_11/ieee80211_var.h:955-960` — `IEEE80211_DEBUG` macros (NULL-vap deref)
- `sys/netproto/802_11/ieee80211_var.h:1027-1033` — non-DEBUG macros (no-ops)
- `sys/config/X86_64_GENERIC:255` — `options IEEE80211_DEBUG` (default GENERIC IS a DEBUG kernel)
