# DF-0326 — ieee80211_node SSID heap overflow (code-level verification)

## Verdict: **INCONCLUSIVE (real shipped bug, unreachable without 802.11 radio)**

The vulnerability is **real and present in the live GENERIC kernel**, confirmed
by line-by-line source tracing and a faithful userspace replica of the
`struct ieee80211_node` overflow. It is **not live-reproducible on this KVM
guest only because the guest has no 802.11 interface** (`ifconfig -l` →
`vtnet0 lo0`); the vulnerable code is 802.11 frame-receive-path only and
cannot be driven from `vtnet0`/`lo0` or via netgraph/hostap/loopback. On any
DragonFlyBSD host with a wlan device (STA/IBSS/Mesh/TDMA), a single crafted
Beacon triggers it remotely, unauthenticated.

## The bug — confirmed line by line

Two sinks copy the attacker-controlled SSID length byte `ssid[1]` (range 0–255,
taken verbatim from a Beacon/Probe-Response Information Element) into the
fixed 32-byte `ni_essid[]` via `memcpy` with **no bounds check**:

- `sys/netproto/802_11/wlan/ieee80211_node.c:815-816` (`ieee80211_sta_join`, STA join):
  ```c
  ni->ni_esslen = se->se_ssid[1];
  memcpy(ni->ni_essid, se->se_ssid+2, ni->ni_esslen);   /* ni_esslen unbounded */
  ```
- `sys/netproto/802_11/wlan/ieee80211_node.c:1521-1522` (`ieee80211_init_neighbor`, IBSS/Mesh/TDMA):
  ```c
  ni->ni_esslen = sp->ssid[1];
  memcpy(ni->ni_essid, sp->ssid + 2, sp->ssid[1]);      /* sp->ssid[1] unbounded */
  ```

The sink buffer is declared at `ieee80211_node.h:188` as
`uint8_t ni_essid[IEEE80211_NWID_LEN];` where `IEEE80211_NWID_LEN == 32`
(`ieee80211.h:199`). `ssid[1]` may legitimately be 0–255, so any value > 32
overflows the buffer.

A third identical-class sink exists upstream and feeds `sta_join`:
- `sys/netproto/802_11/wlan/ieee80211_scan_sta.c:282-284` (`ieee80211_add_scan`):
  ```c
  if (sp->ssid[1] != 0 && (ISPROBE(subtype) || ise->se_ssid[1] == 0))
      memcpy(ise->se_ssid, sp->ssid, 2+sp->ssid[1]);   /* se_ssid[34] overflow */
  ```
  (`se_ssid` is `uint8_t[2+IEEE80211_NWID_LEN]` = 34 bytes, `ieee80211_scan.h:264`.)
  The unbounded `se_ssid[1]` then flows into `sta_join` (`se->se_ssid[1]` at :815).

## Why the "length check" does not help — the dead flag

`ieee80211_input.c:680-681` appears to validate the SSID:
```c
IEEE80211_VERIFY_ELEMENT(scan->ssid, IEEE80211_NWID_LEN,
    scan->status |= IEEE80211_BPARSE_SSID_INVALID);
```
But `IEEE80211_VERIFY_ELEMENT` (`ieee80211_input.h:31-43`) only **logs a discard
message, increments a stat counter, and sets the `IEEE80211_BPARSE_SSID_INVALID`
status bit**. It does **not** clamp `ssid[1]`, null the pointer, or return. The
oversized SSID therefore survives into the scan table and into both node sinks.

Decisive proof — a grep of the entire `sys/` tree shows the flag is a **dead bit**:
```
sys/netproto/802_11/wlan/ieee80211_input.c:680  IEEE80211_VERIFY_ELEMENT(...SSID_INVALID)   <- SET here
sys/netproto/802_11/ieee80211_scan.h:209         IEEE80211_BPARSE_SSID_INVALID = 0x08        <- DEFINED here
```
**It is never read by any consumer.** By contrast, the sibling flags
`IEEE80211_BPARSE_OFFCHAN` and `IEEE80211_BPARSE_RATES_INVALID` **are** gated on
(`scan_sta.c:296,319`; `hostap.c:1707,1768`). The rate set is additionally
enforced by a hard `KASSERT(sp->rates[1] <= IEEE80211_RATE_MAXSIZE, ...)`
(`scan_sta.c:285`); the SSID has **no** equivalent KASSERT and **no** live
consumer of its invalid flag. The asymmetry is the root cause.

## What gets clobbered — ni_chan pointer CONFIRMED

`node_overflow.c` replicates the tail of `struct ieee80211_node` verbatim
(`ieee80211_node.h:176-196`) and runs the exact `memcpy` from :1521-1522 /
:815-816 on the live LP64 ABI. Measured offsets (from the start of the replica):

| field                | offset | size | notes                          |
|----------------------|-------:|-----:|--------------------------------|
| `ni_esslen`          |     28 |    1 | written first (== ssid[1])     |
| `ni_essid[32]`       |     29 |   32 | **SINK**                       |
| `ni_rates`           |     61 |   16 | clobbered (offset 32 from essid)|
| **`ni_chan` (POINTER)** |  80 |    8 | **fully attacker-controlled**  |
| `ni_fhdwell`         |     88 |    2 | clobbered                      |
| `ni_fhindex`         |     90 |    1 | clobbered                      |
| `ni_erp`             |     92 |    2 | clobbered                      |
| `ni_timoff`          |     94 |    2 | clobbered                      |
| `ni_dtim_period`     |     96 |    1 | clobbered                      |
| `ni_dtim_count`      |     97 |    1 | clobbered                      |
| `ni_meshidlen`       |     98 |    1 | clobbered                      |
| `ni_meshid[32]`      |     99 |   32 | clobbered                      |

- `ssid_len = 48` → 16-byte overflow, clobbers `ni_rates` only.
- `ssid_len = 56` → 24-byte overflow, **clobbers 5/8 bytes of `ni_chan`**.
- **`ssid_len = 59` is the minimum to fully overwrite `ni_chan`** (offset 51 from `ni_essid`, +8).
- **`ssid_len = 192`** (the finding's cited payload) → **160-byte overflow**, fully
  clobbering `ni_rates`, the entire `ni_chan` kernel pointer (8/8 bytes), and 8
  further fields through `ni_meshid[32]`.
- `ssid_len = 255` (max) → 223-byte overflow.

**The finding's `ni_chan`-pointer and `ni_rates` overflow claims are CONFIRMED.**
`ni_chan` is a `struct ieee80211_channel *` (`ieee80211_node.h:190`), dereferenced
throughout the wlan stack (e.g. `ni_chan->ic_freq`, `ic_ieee`, rate/channel
lookups). A crafted Beacon that writes a controlled 8-byte value into `ni_chan`
yields an arbitrary kernel-pointer dereference on the next channel/rate query for
that node — a clean memory-corruption primitive.

## Reachability on the live kernel

All three functions are compiled **into the base kernel** (`device wlan` is in
`sys/config/X86_64_GENERIC:258`), confirmed by `nm /boot/kernel/kernel`:
```
ffffffff8078c4c0 T ieee80211_add_scan
ffffffff8077dc50 T ieee80211_init_neighbor
ffffffff8077d1f0 T ieee80211_sta_join
ffffffff8076ce50 T ieee80211_parse_beacon
```
The code is live. The **only** thing preventing exploitation on this guest is the
absence of an 802.11 radio: `ifconfig -l` shows `vtnet0 lo0` only. The vulnerable
helpers are wlan receive-path only (`ieee80211_parse_beacon` → `ieee80211_add_scan`
→ `ieee80211_sta_join`; and `ieee80211_recv_mgmt` → `ieee80211_init_neighbor` via
`ieee80211_adhoc.c:741`). There is no netgraph/hostap/loopback path that reaches
them without a wlan vap attached to a real driver. Reproduction requires either a
wlan NIC passed through to the guest, or an injection harness on real radio
hardware emitting `beacon.bin` (this folder) in monitor mode while the victim
STA/IBSS is scanning.

## Exploitability (chain sketch, not developed live)

Primitive class: **remote, unauthenticated, attacker-controlled heap overflow
into a kernel object** (`struct ieee80211_node` is `kmalloc`-allocated via
`ieee80211_alloc_node`). Overwrite of `ni_chan` (channel pointer) is the high-
value target:

1. Beacon with `ssid_len` ≥ 59 writes a fully-controlled 8-byte kernel pointer
   into `ni_chan`.
2. On the next operation dereferencing `ni_chan` (channel/frequency lookup,
   rate-control setup `ieee80211_node_setuptxparms` → `ni_chan->ic_*`, or
   `ieee80211_ratectl_node_init`), the attacker controls the pointer base,
   yielding an arbitrary kernel read, or — if the forged "channel" object is
   itself groomed — a type-confused dereference toward code execution.
3. Larger overflows (len 192/255) additionally clobber rate state, mesh ID,
   and DTIM fields, broadening corruption.

`ni_chan` is the strongest lever; a full RCE chain would require heap-grooming a
fake `struct ieee80211_channel` and steering a function-pointer-bearing field
into the dereference path (not pursued here — no radio in the guest). Even
without a chain, the corruption reliably yields a **panic** (null/bogus pointer
deref) — i.e. unauthenticated remote DoS at minimum.

## Fix

`fix.diff` clamps the SSID length to `IEEE80211_NWID_LEN` at all three sinks
using `MIN()` (from `<sys/param.h>`, already included):

- `ieee80211_node.c:815` — `ni->ni_esslen = MIN(se->se_ssid[1], IEEE80211_NWID_LEN);`
- `ieee80211_node.c:1521` — `ni->ni_esslen = MIN(sp->ssid[1], IEEE80211_NWID_LEN);` (+ use `ni->ni_esslen` in the memcpy)
- `ieee80211_scan_sta.c:284` — `2 + MIN(sp->ssid[1], IEEE80211_NWID_LEN)` (defense-in-depth on the upstream sink)

Verified `git apply --check` and `patch --dry-run -p1` both succeed; `sys/` is
untouched. A defense-in-depth follow-up would be to make
`IEEE80211_VERIFY_ELEMENT` for the SSID actually reject the frame (e.g. null
`scan->ssid` when `ssid[1] > IEEE80211_NWID_LEN`), but the clamp at the three
memcpy sinks is the minimal, root-cause fix.

## Reproduce

```
cd findings/poc/DF-0326
./build.sh && ./run.sh     # code-level proof + crafted beacon + live-kernel probe
```
On real 802.11 hardware: `./beacon_gen 192 > beacon.bin`, then inject `beacon.bin`
via a monitor+injection-capable wlan vap while the victim is scanning/peering.
