# DF-3295 — VERDICT

**Status: reproduced** (impact: panic — memory-corruption primitive
characterized; escalation ceiling is delegated-cap → kernel compromise,
there is no unprivileged entry path).

## Finding

`ieee80211_scanreq()` (sys/netproto/802_11/wlan/ieee80211_ioctl.c)
validates scan SSID lengths with a signed-only upper bound:

* ieee80211_ioctl.c:2495-2497 — `if (sr->sr_ssid[i].len > IEEE80211_NWID_LEN) return EINVAL;`
  (`sr_ssid[i].len` is `int`, ieee80211_ioctl.h:788)

Sink, INIT branch (vap in `IEEE80211_S_INIT`, `ic_nrunning != 0`,
ieee80211_ioctl.c:2526-2543):

* ieee80211_ioctl.c:2538-2540 — `memcpy(vap->iv_scanreq_ssid[i].ssid, sr->sr_ssid[i].ssid, sr->sr_ssid[i].len)` with `len = -1` → `SIZE_MAX`.

Secondary (non-INIT branch): ieee80211_ioctl.c:2544-2559 passes the
signed `len` into the scan machinery (`ieee80211_start_scan` /
`ieee80211_check_scan`), where `ieee80211_scan_copy_ssid`
(ieee80211_scan.c:305) struct-copies it into scan state (no immediate
overflow; negative `len` resident for later probe construction).
The signed **`sr_nssid`** sibling on that scan.c sink is DF-0642 —
this finding is the **`sr_ssid[].len`** field and the **ioctl.c**
memcpy, i.e. not a re-report.

## How it was reproduced

1. `sh findings/poc/DF-3295/build.sh` — builds DF-3282's software
   radio KLD (`fw.ko`: registers `fw0`, STA/hostap/ibss capable, nop
   scan callbacks) + `scanreq` trigger on the guest (stock INVARIANTS
   kernel `#0`, DragonFly 6.5-DEVELOPMENT).
2. `ifconfig wlan create wlandev fw0 wlanmode sta up` — vap UP;
   state machine reaches S_SCAN (`status: no carrier`).
3. `./scanreq wlan0`:
   * `ioctl(SIOCS80211, MLME DISASSOC)` → 0 — STA-mode DISASSOC does
     `ieee80211_new_state(vap, IEEE80211_S_INIT, reason)`
     (ieee80211_ioctl.c:1396-1400) while the interface stays UP, so
     `ic_nrunning` stays 1.
   * `ioctl(SIOCS80211, SCAN_REQ, {nssid=1, ssid[0].len=-1})`:
     first iteration hit the SCAN-state branch (returned EINPROGRESS=36
     — the negative len entered scan state via copy_ssid); after the
     next MLME the vap was in S_INIT and the stash-branch memcpy ran.
4. Result (run.log / panic.txt): `Fatal trap 12: page fault while in
   kernel mode`, `fault virtual address = 0xfffff80118830000`,
   `supervisor write data`, `current process = 1229` (scanreq),
   `Stopped at memcpy+0xfa: repe movsq` — the destination walk off the
   vap hit an unmapped kernel page. Guest down; `vm.sh status` ⇒ down.

## Primitive

* Destination: `&vap->iv_scanreq_ssid[0].ssid` — 32-byte array inside
  the heap-allocated `ieee80211vap`; the copy walks the entire vap tail
  (`iv_scanreq_ssid[1..2]`, and everything after: including function
  pointers `iv_newstate`, `iv_reset`, `iv_key_alloc`, `iv_key_set`, …)
  and then adjacent kernel heap.
* Source: the 128-byte `kmalloc`'d `ieee80211_scan_req`, fully
  attacker-controlled (copyin at ieee80211_ioctl.c:2583), then adjacent
  heap.
* Length: `SIZE_MAX` — copy runs until the fault; on this guest ~the
  distance to the next unmapped kernel page.

**uid0?** Not reachable by an unprivileged user: the only entry is
`SIOCS80211`, gated by `caps_priv_check_self(SYSCAP_NONET_WIFI)`
(ieee80211_ioctl.c:3472). Escalation ceiling = a user delegated
`SYSCAP_NONET_WIFI` (wifi administrator) can groom the heap so a
chosen object follows the vap and convert this into controlled-pointer
overwrite → kernel code exec → full root; or trivially panic. Filed
Medium (privileged heap corruption; deterministic local DoS is the
demonstrated floor).

## Fix validation (fix.diff)

`vm.sh reset with-src` → applied fix.diff (reject `sr_ssid[i].len < 0`
and `sr_nssid < 0` with EINVAL) in the guest `/usr/src` →
`make nativekernel KERNCONF=X86_64_GENERIC` → `make installkernel` →
reboot (kernel #1, Thu Sep 10 07:24:45 UTC 2026) → re-ran the exact
PoC:

* All 11 attempts: `scanreq -> -1 (errno 22)` (EINVAL) — the fix's
  rejection path. No trap, no panic; `vm.sh status` ⇒ up; wlan0 still
  responds (`ifconfig wlan0` fine).
* Regression: valid `SCAN_REQ` (`len=4`) accepted (rc=0 via the INIT
  stash path; normal behavior), guest healthy.

Baseline vs patched: fatal trap 12 + guest down → clean EINVAL +
guest up. **fix_status: fixed.**

## Notes / honesty

* `errno 36` (EINPROGRESS) on the non-INIT first iteration is normal
  scan behavior, not an error of the PoC.
* The guest was reset (`vm.sh reset with-src`) after validation; no
  dirty state left behind.

## 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.
