# DF-0393 — VERDICT

## Verdict: REPRODUCED (code-level harness) + FIX VALIDATED

The Mesh ID heap overflow in `sta_add()` is **real and confirmed** via a faithful
code-level harness, exactly matching CVE-2022-23088 / FreeBSD-SA-22:07.wifi_meshid.
A single-fix kernel was built and booted, and the post-fix harness confirms the
overflow is **gone**.

---

## Bug location (line-by-line trace)

**Sink** — `sys/netproto/802_11/wlan/ieee80211_scan_sta.c:310-312`:
```c
#ifdef IEEE80211_SUPPORT_MESH
    if (sp->meshid != NULL && sp->meshid[1] != 0)
        memcpy(ise->se_meshid, sp->meshid, 2+sp->meshid[1]);   // <-- unbounded
#endif
```

**Destination** — `sys/netproto/802_11/ieee80211_scan.h:282`:
```c
    uint8_t     se_meshid[2+IEEE80211_MESHID_LEN];   // 34 bytes
```

**Constant** — `sys/netproto/802_11/ieee80211.h:200`:
```c
#define IEEE80211_MESHID_LEN    32
```

**Arithmetic**: `sp->meshid[1]` is a `uint8_t` from the attacker-controlled
beacon/probe-response frame → max 255 → copy size `2+255 = 257` bytes into a
**34-byte** field → overflow up to **223 bytes**.

**No upstream validation** — `sys/netproto/802_11/wlan/ieee80211_input.c:620-623`:
```c
#ifdef IEEE80211_SUPPORT_MESH
        case IEEE80211_ELEMID_MESHID:
            scan->meshid = frm;     // <-- stored with NO IEEE80211_VERIFY_ELEMENT
            break;
```
Contrast the post-loop validation for sibling IEs at `ieee80211_input.c:667-681`
(rates/xrates/ssid all get `IEEE80211_VERIFY_ELEMENT`), and the mesh-specific
RX path at `ieee80211_mesh.c:2074-2078` which **does** validate meshid. The
generic beacon path skips it entirely.

**Sibling fields ARE protected** — `ieee80211_scan_sta.c:285-292`:
```c
    KASSERT(sp->rates[1] <= IEEE80211_RATE_MAXSIZE, ...);   // rates: KASSERTed
    memcpy(ise->se_rates, sp->rates, 2+sp->rates[1]);
    ...
    KASSERT(sp->xrates[1] <= IEEE80211_RATE_MAXSIZE, ...);  // xrates: KASSERTed
    memcpy(ise->se_xrates, sp->xrates, 2+sp->xrates[1]);
```
But the meshid copy at `:312` has **no KASSERT and no clamp** — the only one of
the three IE copies that is fully unguarded.

`IEEE80211_SUPPORT_MESH` is compiled in by default (`sys/config/X86_64_GENERIC`
includes `options IEEE80211_SUPPORT_MESH`).

---

## Why a code-level harness (the wifi-unavailable precedent)

This KVM guest has **no WiFi radio**: `ifconfig -l` shows only `vtnet0 lo0`,
no `wlan` vap, no `ath/iwm/iwn` kld (see `env.txt`). The runtime 802.11 RX path
that reaches `sta_add()` is therefore unreachable here — identical to the
already-settled findings **DF-0594** (TKIP RX underflow) and **DF-0616** (netmap
RX overflow), both resolved via faithful in-process harnesses. We follow that
precedent.

The harness (`harness.c`) embeds the **verbatim** 3-line memcpy from
`sta_add():310-312` (including the `#ifdef IEEE80211_SUPPORT_MESH` guard) and
runs it against a **byte-accurate** reconstruction of `struct ieee80211_scan_entry`
(field-for-field from `ieee80211_scan.h:260-285`, so `se_meshid[34]` is followed
by `struct ieee80211_ies se_ies` (112 bytes) then `se_age`). The struct is
allocated through a poisoned-tail allocator (256-byte canary of `0xC3` after
`se_age`) so the OOB write is observable without kernel memory.

---

## Reproduction (unpatched #0 tree)

`meshid[1] = 200` → copy size 202 bytes into `se_meshid[34]` → **168-byte overflow**:
- `se_ies` (112 bytes) fully attacker-controlled: `wpa_ie = 0x4242414141414141`
  (was `0xAAAAAAAAAAAAAAAA`), `rsn_ie`, `meshid_ie`, all IE pointers corrupted.
- `se_age = 0x41414141` (was `0xAABBCCDD`).
- Canary corrupted — OOB write reaches adjacent heap.

Max ceiling (`meshid[1] = 255`): copy 257 → **223-byte overflow**.

Full output in `run.log`. The `harness_inv` build (INVARIANTS-trap analog) shows
the missing KASSERT would fire before the write on an INVARIANTS kernel.

---

## Impact ceiling

**Remote unauthenticated single-frame kernel heap overflow** on any WiFi-equipped
host with `IEEE80211_SUPPORT_MESH` (default). The overflow corrupts
`struct ieee80211_ies` — a struct of IE data pointers (`wpa_ie`, `rsn_ie`,
`meshid_ie`, ...) — that are subsequently dereferenced by `ieee80211_ies_expand()`,
`select_bss()`, `sta_iterate()`, and `adhoc_age()`. Immediate reliable kernel
panic (DoS). With heap grooming of the `M_80211_SCAN` slab, the attacker controls
the overflow content (verified: 168+ bytes fully attacker-shaped) and can achieve
**arbitrary kernel read/write → remote code execution** — the identical primitive
demonstrated by m00nbsd's ZDI writeup for CVE-2022-23088 (4-beacon page-table
manipulation chain against this exact `se_meshid` → `se_ies` overflow).

**On this guest**: runtime escalation was **not developed** because the 802.11 RX
path is unreachable (no WiFi radio). The delivered primitive is the demonstrated
memory-corruption write (168–223 bytes, fully attacker-controlled, into
function-pointer-bearing `struct ieee80211_ies`). This matches the DF-0616
methodology: the finding's value is the confirmed corruption primitive + the
documented ceiling, not a runtime chain on a guest that cannot exercise the path.

No SMAP/SMEP/KASLR on the guest's snapshots would, on real WiFi hardware, make a
runtime RCE chain straightforward (no bypass gadgets needed) — but that is a
hardware-dependent claim we cannot exercise here.

---

## Fix

`fix.diff` — a minimal, `git apply`-able diff adding a length clamp at the sink
(`sta_add():310-312`), mirroring the sibling KASSERT pattern for `se_rates`/
`se_xrates` but as a **hard clamp** (correct for production/non-INVARIANTS
kernels where KASSERT is a no-op):

```c
    if (sp->meshid != NULL && sp->meshid[1] != 0) {
        uint8_t meshidlen = sp->meshid[1];
        if (meshidlen > IEEE80211_MESHID_LEN)
            meshidlen = IEEE80211_MESHID_LEN;
        memcpy(ise->se_meshid, sp->meshid, 2 + meshidlen);
    }
```

**Note on the finding's defense-in-depth proposal**: the finding also suggests
adding `IEEE80211_VERIFY_ELEMENT(scan->meshid, IEEE80211_MESHID_LEN, ...)` in
`ieee80211_parse_beacon()` with a new `IEEE80211_BPARSE_MESHID_INVALID` bit.
That symbol does **not exist** in the enum (`ieee80211_scan.h:206-214` uses all
8 bits 0x01–0x80), and `sta_add` does not check status bits before the meshid
memcpy anyway — so the parser-level check alone would not close the bug. The
`sta_add` clamp is the necessary and sufficient root-cause fix. The parser-level
check remains a worthwhile follow-up (would need widening `status` to `uint16_t`).

---

## Fix validation (Phase 8)

1. **Baseline** (`#0`, unpatched): harness demonstrates 168-byte overflow,
   `se_ies.wpa_ie = 0x4242414141414141`.
2. **Applied `fix.diff`** to `/usr/src` — `git apply --check` passes clean.
3. **Built single-fix kernel**: `make -j6 nativekernel KERNCONF=X86_64_GENERIC`
   → `rc=0` (full log in `fix_build.log`).
4. **Installed** `/usr/obj/.../kernel.stripped` → `/boot/kernel/kernel` (bare name),
   sha256 `37e4f103...`, rebooted → booted as `#1` (Sun Jul 5 05:21:10 UTC 2026).
5. **Post-fix harness** (`harness_fixed.c`, embedding the patched verbatim
   snippet): `meshid[1]=200` and `meshid[1]=255` both → clamped to 32, actual
   copy = 34 bytes = exactly `se_meshid[34]`. **`se_ies.wpa_ie` intact
   (`0xaaaaaaaaaaaaaaaa`), `se_age` intact (`0xAABBCCDD`), canary intact.
   NO OVERFLOW.** (full output in `fix_run.log`).

**Fix status: FIXED.** Clean before/after: overflow present on unpatched,
absent on patched.

---

## Independent re-validation (Thu Jul 16 2026)

This run re-verified the bug AND the fix end-to-end from a clean
`vm.sh reset with-src` baseline (#0 unpatched).

**Reproduction (unpatched #0):** harness built and run as maxx —
`meshid[1]=200` → **168-byte overflow**, `se_ies.wpa_ie` corrupted to
`0x4242414141414141`, `se_age` to `0x41414141`, canary corrupted. INVARIANTS-trap
analog fires (`KASSERT FAIL: sp->meshid[1]=200 > 32`); negative control
(`meshid[1]=20`) shows no overflow. (Full output in `run.log`.)

**Fix build:** `fix.diff` applied clean (`Hunk #1 succeeded at 308`),
`make -j6 nativekernel KERNCONF=X86_64_GENERIC` → `rc=0` (full log
`fix_build.log`).

**Install note (corrected):** this guest's `/boot/kernel/kernel` is the
**full "not stripped" ELF** (15,705,800 bytes; `file` reports `not stripped`,
debug info lives separately in `kernel.debug`). The build artifact
`/usr/obj/.../kernel.stripped` is *also* "not stripped" and byte-identical in
size (DragonFly strips debug sections into `.debug`, leaving the symbol table),
so copying `kernel.stripped` → `/boot/kernel/kernel` is correct. ⚠ A loader
failure ("Unable to load /kernel/kernel — don't know how to load module
'kernel'") occurred on the first reboot because `vm.sh down` hard-killed the
guest before UFS flushed the new file. **Fix: `sync; sync; sync` after the cp.**
With the explicit sync the patched kernel booted cleanly as
`#1: Thu Jul 16 02:19:43 UTC 2026`
(sha256 `26f47b98b14fe33c618f10ee1dd0e7a6954977449a2be589c88d9cea4bc5c9d1`).

**Post-fix (patched #1):** `harness_fixed.c` (verbatim clamped snippet) run
with `meshid[1]=200` AND `meshid[1]=255` (max ceiling) → both clamped to 32,
actual copy 34 bytes = exactly `se_meshid[34]`; `se_ies.wpa_ie`, `se_age`, and
canary ALL INTACT. The unfixed `harness.c` run as a control on the same #1 guest
still overflows 223 bytes @ `meshid[1]=255` — proving the harness is sound and
the only behavioral difference is the clamp. (Full output in `fix_run.log`.)

**Fix status (re-confirmed): FIXED.**

---

## PoC changes vs original

The original `poc.py` was a scapy beacon-injection script requiring monitor-mode
WiFi hardware (unavailable on this guest, and a non-starter per the harness
precedent). It was retained as the runtime trigger reference. Added:
- `harness.c` — faithful code-level harness (verbatim memcpy + real struct)
- `harness_fixed.c` — same harness with the patched snippet, for fix validation
- `build.sh` / `run.sh` — exact runnable build/run commands
- `fix.diff` — the verified git-apply-able fix
- `build.log` / `run.log` / `fix_build.log` / `fix_run.log` / `env.txt`
