# VERDICT — DF-0291

**Finding:** heap buffer overflow in `setmlme_assoc_adhoc`
(`sys/netproto/802_11/wlan/ieee80211_ioctl.c:1568`), with a stack-overflow
amplification in the GET `IEEE80211_IOC_SSID` handler.

**Verdict:** **INCONCLUSIVE — the bug is REAL and shipped in the audited master
DEV kernel, but it is NOT reachable from userspace on the KVM audit guest**
(no wifi radio → no wlan vap → the ioctl is never delivered).

---

## 1. The overflow is REAL — confirmed line-by-line

### Call chain (SET path)

`ieee80211_ioctl()` (`ieee80211_ioctl.c:3377`) is the `if_ioctl` of wlan vap
interfaces. For `SIOCS80211` it runs, at `:3471-3475`:

```c
case SIOCS80211:
    error = caps_priv_check_self(SYSCAP_NONET_WIFI);   /* root / wifi cap */
    if (error == 0)
        error = ieee80211_ioctl_set80211(vap, cmd, (struct ieee80211req *) data);
```

→ `ieee80211_ioctl_setmlme()` (`:1611`) `copyin`s a `struct ieee80211req_mlme`
(`:1618`) and, for `IBSS`/`AHDEMO` opmode + `IEEE80211_MLME_ASSOC`, calls
(`:1628-1629`):

```c
return setmlme_assoc_adhoc(vap, mlme.im_macaddr, mlme.im_ssid_len, mlme.im_ssid);
```

`im_ssid_len` is `uint8_t` (`ieee80211_ioctl.h:309`) → **attacker-controlled
0..255**. `im_ssid` is `uint8_t im_ssid[IEEE80211_NWID_LEN]` (32 bytes) and is
the **last** field of the struct.

### The missing guard

`setmlme_assoc_adhoc()` (`:1568`) at `:1580`:

```c
if (ssid_len == 0)
    return EINVAL;
```

**It checks only `ssid_len == 0`, never `ssid_len > IEEE80211_NWID_LEN`.**
Compare the sibling `IEEE80211_IOC_SSID` SET handler at `:2671-2674`:

```c
case IEEE80211_IOC_SSID:
    if (ireq->i_val != 0 ||
        ireq->i_len > IEEE80211_NWID_LEN)      /* <-- correct bound */
        return EINVAL;
```

So the adhoc path is the **only** unguarded sink.

### The overflows

With `ssid_len = 255`:

* `:1594`  `vap->iv_des_ssid[0].len = ssid_len;` → stores 255.
  `iv_des_ssid` is `struct ieee80211_scan_ssid iv_des_ssid[1]`
  (`ieee80211_var.h:400`), so `iv_des_ssid[0].ssid` is exactly 32 bytes.
* `:1595`  `memcpy(vap->iv_des_ssid[0].ssid, ssid, ssid_len);`
  → **HEAP OVERFLOW #1**: 255 bytes into a 32-byte field inside the
  `struct ieee80211vap` heap object → **223 bytes past the end**, corrupting the
  following vap fields (and, depending on slab layout, the adjacent slab object).
* `:1600`  `memcpy(sr->sr_ssid[0].ssid, ssid, ssid_len);`
  `sr` is `kmalloc(sizeof(*sr), M_TEMP, …)` (`:1584`); `sr_ssid[3]` each with a
  32-byte `ssid` (`ieee80211_ioctl.h:790`). → **HEAP OVERFLOW #2**: 255 bytes
  into `sr_ssid[0].ssid`, spilling across `sr_ssid[1]`, `sr_ssid[2]` and past
  the end of the kmalloc'd object. (A *later* check at `:2496`
  `if (sr->sr_ssid[i].len > IEEE80211_NWID_LEN)` does **not** save you — the
  overflow at `:1600` has already happened before `ieee80211_scanreq()` is
  called at `:1604`.)

Source content: `ssid` is `&mlme.im_ssid`; the first 32 bytes are the
attacker-supplied SSID, bytes 33..255 are **kernel-stack residue** (the read
runs past the last field of the stack `mlme`). So the first 32 bytes of each
overflow are attacker-controlled; the rest is kernel memory. That is enough to
corrupt adjacent heap objects with attacker-chosen data in the leading 32 bytes.

### GET-path stack-overflow amplification

`ieee80211_ioctl_get80211()` (`:794`), `IEEE80211_IOC_SSID` INIT/SCAN branch
(`:807-811`):

```c
char tmpssid[IEEE80211_NWID_LEN];                    /* :801 — 32-byte stack buffer */
...
case IEEE80211_S_INIT:
case IEEE80211_S_SCAN:
    ireq->i_len = vap->iv_des_ssid[0].len;           /* :809 — reads corrupted 255 */
    memcpy(tmpssid, vap->iv_des_ssid[0].ssid, ireq->i_len);   /* :810 — STACK OVERFLOW */
```

After the SET path stores `len = 255`, the GET handler copies 255 bytes into the
32-byte `tmpssid` → **223-byte stack overflow** (smashes saved frame pointer /
return address / other locals). This is a stronger primitive than the heap
overflow alone: a controlled return-address overwrite is a direct
code-execution / LPE path. **The amplification is real.** (The default branch at
`:813-814` is also reachable via `ni_esslen`, which is itself derived from the
corrupted `iv_des_ssid[0].len` at `ieee80211_node.c:354-355`, so both branches
of the GET handler can overflow `tmpssid`.)

### Code-level proof

`overflow_proof.c` reproduces the exact vulnerable logic against buffers laid
out like the kernel structs and prints the concrete byte counts. `run.log`:

```
HEAP OVERFLOW #1 (iv_des_ssid[0].ssid, :1595): 223 bytes past the 32-byte field ...
HEAP OVERFLOW #2 (sr->sr_ssid[0].ssid, :1600): 223 bytes past the 32-byte field ...
GET IOC_SSID: memcpy 255 bytes into 32-byte stack tmpssid -> 223 bytes written past end
```

## 2. Why it is NOT reachable on this guest

`ieee80211_ioctl` is only the `if_ioctl` of **wlan vap** interfaces
(`ieee80211.c:567  ifp->if_ioctl = ieee80211_ioctl`). To reach it you must have
a `wlanN` interface, which is created via the `wlan` cloner
`wlan_clone_create()` (`ieee80211_dragonfly.c:80`). At `:88-94`:

```c
error = copyin(params, &cp, sizeof(cp));           /* cp.icp_parent = radio name */
...
ic = ieee80211_find_com(cp.icp_parent);            /* :92 */
if (ic == NULL)
    return ENXIO;                                  /* :94 — no radio => fail */
```

A vap therefore **requires a registered radio parent** (`struct ieee80211com`),
which only exists when a wifi radio driver (`if_ath`, `if_run`, `if_iwm`, …)
has attached to real hardware. The KVM guest has **no wifi hardware**.

**Live probe** (`env.txt`):

* `device wlan` IS in `X86_64_GENERIC:258`; `nm /boot/kernel/kernel` shows **508
  `ieee80211_*` symbols** plus `wlan_clone_create` / `wlan_cloner` — the wlan
  stack (and the vulnerable static `setmlme_assoc_adhoc`) **is statically linked
  into the running kernel**. `kldload wlan` reports "already loaded or in kernel".
* Loaded modules: only `ehci`, `xhci`, `if_wg` — **no radio driver**.
  Radio driver `.ko`s exist on disk (`if_ath.ko`, …) but none can attach (no HW).
* Interfaces: `vtnet0 lo0` only — **no wlan interface**.
* `ifconfig wlan create wlandev <ath0|run0|iwm0|iwn0|wpi0|ral0|rum0|bwi0|ipw0>`
  → **`SIOCIFCREATE2: Device not configured`** for *every* radio name, i.e.
  `ieee80211_find_com()` returns NULL → `ENXIO`.

⇒ **No vap can ever exist on this guest ⇒ `SIOCS80211`/`SIOCG80211` are never
delivered to `ieee80211_ioctl` ⇒ the in-kernel overflows cannot be triggered
from userspace here.** No in-kernel PoC run was possible; a code-level model
(`overflow_proof.c`) is used instead, and it confirms the overflow concretely.

This is the textbook `inconclusive` outcome: a real, shipped, exploitable-looking
bug that is gated behind hardware the audit VM lacks.

## 3. Privilege / threat model (for when a radio IS present)

* SET path requires `SYSCAP_NONET_WIFI` (`:3472`) — root-equivalent. On a normal
  laptop/desktop DragonFly install a local unprivileged user (e.g. `maxx`, not
  in wheel) **cannot** reach it.
* The realistic exposure is on **wifi-equipped AP / embedded boxes** where the
  radio is configured and a context holding the wifi capability (a setuid helper,
  a management daemon, or a misconfigured service) can be coerced into issuing the
  crafted `SIOCS80211 MLME_ASSOC` with `im_ssid_len = 255`. Given such a foothold,
  the bug is a kernel heap-write + a stack-smash primitive → local privilege
  escalation to kernel/root is plausible.
* Remote unauth exposure: **none** — ioctl is local-only.

## 4. Fix

`fix.diff` clamps `ssid_len` at the root-cause sink (the missing bound in
`setmlme_assoc_adhoc`) and adds a defense-in-depth clamp on the GET-path stack
sink so a corrupted stored length can never overflow `tmpssid`. Both `git apply
--check` and `patch --dry-run` pass against the unmodified `sys/` tree. See
`fix.diff`. This **matches in spirit** the finding's recommendation (bound the
adhoc path the same way the sibling `IOC_SSID` handler does) and additionally
hardens the GET sink.
