# DF-0285 — `ieee80211_parse_meshid` heap OOB write (802.11s MESHID IE)

## Verdict

**INCONCLUSIVE (real bug, code-level-confirmed; NOT runtime-triggerable on this audit guest).**

The claimed heap overflow is **real and exactly as described at the source level**,
but the audit guest is a KVM VM with **no wifi hardware**, so the receive path
that feeds it (`ieee80211_parse_beacon` → `ieee80211_mesh_init_neighbor` →
`ieee80211_parse_meshid`) cannot be driven at runtime here. This is the expected
outcome for a wifi-receive-path bug; we provide a rigorous code-level proof
instead, plus the exact attacker payload that *would* fire it on real radio HW.

The vulnerable code **is statically linked into the live kernel**
(`nm /boot/kernel/kernel` shows `T ieee80211_parse_meshid @ 0xffffffff8077aa50`,
`mesh_peer_timeout_cb @ 0xffffffff80775560`), and `IEEE80211_SUPPORT_MESH` is
enabled in the audited `X86_64_GENERIC` config — so the bug ships in the
production kernel image; it is merely unreachable without a wifi driver+radio.

## The bug — trigger → primitive → effect (every hop cited)

**Trigger.** A mesh peer (or an attacker within wifi range of an MBSS vap) transmits
a beacon/probe-response whose MESHID Information Element has a length byte `ie[1]`
greater than `IEEE80211_MESHID_LEN` (32). The 802.11 IE parser
`ieee80211_input.c:620-623` stores the **raw, unchecked** IE pointer:

```c
#ifdef IEEE80211_SUPPORT_MESH
case IEEE80211_ELEMID_MESHID:
    scan->meshid = frm;     /* frm[0]=id, frm[1]=len(!!), frm[2..]=data */
    break;
```

Contrast line 602 (`case IEEE80211_ELEMID_ERP`), which *does* validate
`if (frm[1] != 1)` — proving the kernel's own pattern is to bounds-check IE
lengths; the MESHID case simply omits it. The pointer then flows through
`ieee80211_mesh_init_neighbor` (`ieee80211_mesh.c:3467-3472`) into the sink:

**Sink.** `sys/netproto/802_11/wlan/ieee80211_mesh.c:3456-3461`

```c
void
ieee80211_parse_meshid(struct ieee80211_node *ni, const uint8_t *ie)
{
    ni->ni_meshidlen = ie[1];
    memcpy(ni->ni_meshid, ie + 2, ie[1]);   /* ie[1] UNCHECKED, up to 255 */
}
```

`ni->ni_meshid` is a **fixed 32-byte array** (`ieee80211_node.h:200`,
`uint8_t ni_meshid[IEEE80211_MESHID_LEN]`, `IEEE80211_MESHID_LEN == 32` per
`ieee80211.h:200`). `ie[1]` is an attacker-controlled `uint8_t` in `[0..255]`.
For any `ie[1] > 32`, `memcpy` writes `ie[1]-32` bytes past the end of the
array — a classic heap OOB write of up to **223 bytes**.

A second call site (`ieee80211_node.c:843-844`, `ieee80211_parse_meshid(ni,
ni->ni_ies.meshid_ie)`) feeds the same sink from cached node IEs; the IE
pointer there is likewise stored without clamping (`ieee80211_node.c:1017`,
`ies->meshid_ie = ie`).

## What gets overwritten (the layout proof — `layout_proof.c`)

`struct ieee80211_node`'s "11s state" block (`ieee80211_node.h:199-208`) is
laid out (offsets relative to `ni_meshid[0]`, verified by `offsetof()` in
`layout_proof`):

| field               | rel | type            |
|---------------------|----:|-----------------|
| `ni_meshid[0..31]`  |  +0 | `uint8[32]` (legal sink) |
| `ni_mlstate`        | +35 | `enum` (4B, peering FSM) |
| `ni_mllid`          | +39 | `uint16` |
| `ni_mlpid`          | +41 | `uint16` |
| **`ni_mltimer`**    | +47 | `struct callout` (24B) |
| &nbsp;&nbsp;`.toc`  | +47 | **pointer** to `struct _callout` |
| `ni_mlrcnt`         | +71 | `uint8` |
| `ni_mltval`         | +72 | `uint8` |
| **`ni_mlhtimer`**   | +79 | `struct callout` (24B) |
| &nbsp;&nbsp;`.toc`  | +79 | **pointer** to `struct _callout` |
| `ni_mlhcnt`         |+103 | `uint8` |
| *(11n HT state ...)*|+104+| `ni_htcap`, … |

Reach table from `layout_proof`:

| `ie[1]` | OOB bytes | reaches `ni_mltimer.toc`? | reaches `ni_mlhtimer.toc`? |
|--------:|----------:|:-------------------------:|:--------------------------:|
| 33      |   1       | no                        | no                         |
| 48      |  16       | **YES**                   | no                         |
| 64      |  32       | **YES**                   | no                         |
| 128     |  96       | **YES**                   | **YES**                    |
| 255     | 223       | **YES**                   | **YES** (151B past `ni_mlhcnt` into HT state) |

## CONFIRM or REFUTE the "function-pointer overwrite" claim

**PARTIALLY CONFIRMED — with a precise correction to the finding's wording.**

The finding says `ni_mltimer` is "a callout containing a function pointer."
That is imprecise. On DragonFly, `struct callout` (`sys/sys/callout.h:77-82`)
does **not** itself contain a function pointer:

```c
struct callout {
    struct _callout *toc;   /* opaque internal pointer   <-- FIRST FIELD */
    struct lock     *lk;
    uint32_t         flags;
    uint32_t         unused01;
};
```

The actual function pointers (`rfunc`/`qfunc`) live in `struct _callout`
(`callout.h:54-75`), which is a **separately allocated** object that `toc`
points at. Therefore the overflow does **not** directly overwrite a function
pointer field; it overwrites the **`toc` pointer** inside `ni_mltimer` (and a
second one inside `ni_mlhtimer`).

This is nonetheless a serious primitive — arguably *more* powerful than a
direct function-pointer overwrite, because it is a **controlled pointer
dereference / type confusion**:

* `ni_mltimer` is armed with a real callback: `callout_reset(&ni->ni_mltimer,
  ni->ni_mltval, mesh_peer_timeout_cb, ni)` (`ieee80211_mesh.c:3055,3069`),
  and drained on peer-down/cleanup (`ieee80211_mesh.c:640,3449`). When the
  callout subsystem fires, it dereferences `cc->toc` and invokes
  `toc->qfunc(toc->qarg)`.
* If the attacker controls `toc` (which the overflow gives them, at `ie[1]>=48`),
  they can redirect it at a forged `struct _callout` placed in attacker-influenced
  kernel memory (e.g. via a separate heap spray, or at a chosen existing address).
  The forged `_callout.qfunc` then becomes an arbitrary kernel function call with
  a controlled argument → **kernel PC control → RCE / local privilege escalation
  on a host that also has a wifi adapter in MBSS mode**.

So: the *function-pointer-control RCE severity is justified*, but the mechanism
is "corrupt the `toc` pointer → indirect call through a forged `_callout`", not
"a function pointer field is directly overwritten". The finding's headline
mechanism is right; its one-sentence structural description is slightly off.

**Exploitability ceiling on this primitive (honest):** full RCE/LPE requires the
attacker to (a) reach the receive path (wifi adjacency to a mesh vap), (b) win
any race between the overflow and the callout firing, and (c) place a forged
`_callout` at a predictable/writable kernel address (defended by KASLR + heap
randomization + SMAP/SMEP). We did not develop the chain to `uid0` here because
there is no runtime path on this guest to even trigger the overflow — that is
explicitly out of scope for a wifi-receive-path bug on a headless KVM guest.
The realistic impact is **remote code execution against any DragonFly host
operating a wifi interface in MBSS mesh mode**, severity **High** (matches the
finding). The *content* of the overflow is fully attacker-controlled (the IE
body), and up to 223 bytes of it land in struct fields — a clean, deterministic
heap write primitive, not a probabilistic one.

## Reachability on THIS audit guest

| question | answer | evidence |
|---|---|---|
| is mesh code compiled in? | **yes** | `sys/config/X86_64_GENERIC:256` (`options IEEE80211_SUPPORT_MESH`); `sys/conf/files:1644` (`ieee80211_mesh.c optional wlan ieee80211_support_mesh`) |
| is it in the LIVE kernel? | **yes (static)** | `kldload wlan` ⇒ "module already loaded or in kernel"; `nm /boot/kernel/kernel` ⇒ `T ieee80211_parse_meshid`, `T ieee80211_mesh_init_neighbor`, `t mesh_peer_timeout_cb` |
| is there a wifi interface? | **no** | `ifconfig -l` ⇒ `vtnet0 lo0` only |
| is there a virtual/software frame-injection path? | **no** | the only callers that deliver frames to `ieee80211_parse_beacon` are real radio drivers (`if_ath`, `if_wi`, `if_iwi`, `if_iwn`, `if_iwm`, `if_wpi`, `bwn`, `ral`); there is **no** netgraph 802.11 node and no hostap/atheros software emulation in tree |
| can it be triggered via ioctl/sysctl? | **no** | the only two callers of `ieee80211_parse_meshid` are `ieee80211_mesh.c:3471` (from a received-frame scan entry) and `ieee80211_node.c:844` (from cached node IEs populated by a received frame). No ioctl path. |
| conclusion | **INCONCLUSIVE** — real, shipped bug; not runtime-reachable on a guest with no wifi HW | — |

## PoC artifacts in this folder

* `layout_proof.c` / `layout_proof` — userspace `offsetof()` proof of the
  overflow reach; prints the field table + reach matrix above. **This is the
  decisive numeric evidence.**
* `frame_craft.c` / `frame_craft` — deterministic builder for the exact
  attacker payload: a valid 802.11 beacon (DLT_IEEE802_11 pcap) whose MESHID IE
  has `id=0x72, len=0xFF` and a 253-byte marker body. Writes `df0285_beacon.pcap`.
  On a host with a wifi radio in monitor+inject mode, `tcpreplay`-ing this pcap
  at a DragonFly MBSS vap fires the overflow. **Not delivered on this guest**
  (no radio).
* `df0285_beacon.pcap` — the generated payload (341 bytes).
* `build.sh` / `run.sh` — exact build/run (both pure-userspace, no kernel contact).
* `build.log` / `run.log` — full untrimmed outputs.
* `env.txt` — guest uname, cc, kldstat, live kernel symbols, ifconfig.
* `fix.diff` — standalone `git apply`-able clamp fix (validated:
  `git apply --check` ⇒ RC 0).

## Recommended fix

Clamp `ie[1]` to `IEEE80211_MESHID_LEN` in `ieee80211_parse_meshid` before the
`memcpy`, and store the clamped length. One-line logical change; full diff in
`fix.diff`. **No prior finding-proposal diff existed** (this finding had no
markdown writeup), so this is the authoritative fix.

### Related secondary bug (not the primary claim, flagged for completeness)

`ieee80211_scan_sta.c:312`:
```c
memcpy(ise->se_meshid, sp->meshid, 2+sp->meshid[1]);
```
where `se_meshid` is `uint8_t[2+IEEE80211_MESHID_LEN]` = 34 bytes
(`ieee80211_scan.h:282`), but `2+sp->meshid[1]` can be up to 257. Same beacon
overflows the scan-cache entry too. Worth a separate finding / same-fix sweep.
