# DF-3295 — ieee80211_scanreq: signed `sr_ssid[i].len` passes the `> NWID_LEN` check; negative value reaches `memcpy` as `(size_t)-1` → unbounded kernel-heap copy

File: `sys/netproto/802_11/wlan/ieee80211_ioctl.c` (pass 2 audit, GLM 5.3)

## What

`ieee80211_scanreq()` validates each scan-request SSID length with a
**signed** comparison only (ieee80211_ioctl.c:2495-2497):

```c
	for (i = 0; i < sr->sr_nssid; i++)
		if (sr->sr_ssid[i].len > IEEE80211_NWID_LEN)   /* int len */
			return EINVAL;
```

`struct ieee80211_scan_req`'s embedded SSID is
`struct { int len; uint8_t ssid[IEEE80211_NWID_LEN]; }`
(ieee80211_ioctl.h:787-789), so `len = -1` (0xffffffff as int) passes
(`-1 > 32` is false). When the vap is in `IEEE80211_S_INIT` and the
parent is running, the stash branch copies the SSIDs into the vap
(ieee80211_ioctl.c:2536-2540):

```c
	for (i = 0; i < sr->sr_nssid; i++) {
		vap->iv_scanreq_ssid[i].len = sr->sr_ssid[i].len;   /* -1 */
		memcpy(vap->iv_scanreq_ssid[i].ssid,   /* 32-byte array inside struct ieee80211vap */
		    sr->sr_ssid[i].ssid,               /* 32-byte array inside kmalloc'd sr (copyin'd) */
		    sr->sr_ssid[i].len);               /* (size_t)(-1) */
	}
```

The negative `int` sign-extends to `SIZE_MAX` in `memcpy`'s `size_t`
parameter: an unbounded linear kernel-heap copy whose *destination*
starts inside the `ieee80211vap` (walking forward over the remainder of
the vap — including the `iv_newstate`/`iv_reset`/`iv_key_*` function
pointers — then adjacent heap objects) and whose *source* starts at the
attacker-supplied 128-byte `ieee80211_scan_req` (fully user-controlled
via the preceding `copyin`) continuing into adjacent heap.

In the *non-INIT* branch (ieee80211_ioctl.c:2544-2559) the same signed
`len` is handed to `ieee80211_start_scan()`/`ieee80211_check_scan()`,
which stash it via `ieee80211_scan_copy_ssid` (struct memcpy — no
immediate overflow there, but the negative `len` is now resident in
scan state for the probe-request builders).

Distinct from DF-0642 (signed **`sr_nssid`** → `ieee80211_scan.c`
`copy_ssid` memcpy, non-INIT branch): this finding is the signed
**`sr_ssid[].len`** field reaching the **ioctl.c** memcpy in the INIT
branch — different field, different sink, complementary preconditions.

## Reproduced (stock INVARIANTS kernel #0)

```
mlme DISASSOC -> 0            (vap S_SCAN -> S_INIT while UP; ic_nrunning stays 1)
firing SCAN_REQ: sr_nssid=1 sr_ssid[0].len=-1
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff80118830000     (kernel heap, write side)
fault code = supervisor write data, page not present
current process = 1229 (scanreq)
Stopped at memcpy+0xfa: repe movsq (%rsi),%es:(%rdi)
```

Guest down at the first INIT-branch attempt. (The first loop iteration
hit the non-INIT branch and returned EINPROGRESS=36, storing the
negative len in scan state — the second iteration, after the MLME
pushed the vap to S_INIT, hit the memcpy.)

## Primitive characterization

* **write**: linear overwrite from `&vap->iv_scanreq_ssid[0].ssid`
  forward across the whole tail of the `ieee80211vap` (function
  pointers!) and adjacent kernel heap, until the first unmapped page.
* **data**: first 128 bytes fully attacker-controlled (the copyin'd
  `ieee80211_scan_req`), beyond that adjacent-heap contents.
* **gate**: `SIOCS80211` requires `SYSCAP_NONET_WIFI`
  (ieee80211_ioctl.c:3472). No unprivileged path exists, so an
  unprivileged user→root chain is not reachable; the ceiling is a
  delegated-capability user converting wifi-admin into full kernel
  compromise (heap-groom a victim object behind the vap, overwrite
  with controlled bytes, pivot through `iv_newstate`-style calls), or
  trivially a guaranteed panic.

## Build / run (repo root)

```
sh findings/poc/DF-3295/build.sh    # fw.ko (software radio) + scanreq in guest
sh findings/poc/DF-3295/run.sh      # baseline: fatal trap 12 at the ioctl
```

Harness (`fw.c`) is DF-3282's software net80211 radio, unchanged:
registers `fw0` (STA/hostap/ibss capable), nop scan callbacks.
Trigger creates a STA vap, brings it UP (→ S_SCAN), uses MLME DISASSOC
to return it to S_INIT while UP, then fires the SCAN_REQ with
`sr_ssid[0].len = -1`, retrying.

## Fix validation

`fix.diff` rejects `sr_ssid[i].len < 0` (and `sr_nssid < 0`) with
EINVAL. Patched kernel (#1, built in-guest): every attempt returns
`EINVAL`, no panic, guest stays up, wlan0 functional, and a *valid*
scan request (len=4) is still accepted (rc=0 via the INIT stash path).
