β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0393

Remote heap buffer overflow via oversized Mesh ID IE in sta_add: memcpy 2+meshid[1] into se_meshid[34] with no bounds check

Field Value
ID DF-0393
Status new
Severity Critical
CVSS 3.1 CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE CWE-122 Heap-based Buffer Overflow
File sys/netproto/802_11/wlan/ieee80211_scan_sta.c
Lines 310-312
Area netproto (802.11 WiFi)
Confidence certain
Discovered 2026-07-01
Reported pending
Known CVE CVE-2022-23088 (FreeBSD-SA-22:07.wifi_meshid)
CVE match equivalent

Summary

When a WiFi interface in scanning mode receives a beacon or probe-response frame containing a Mesh ID information element with a length byte greater than 32, the scan subsystem copies the IE into a fixed-size 34-byte struct field (se_meshid[2+IEEE80211_MESHID_LEN]) without any bounds check. The copy size 2+meshid[1] can reach 257, overflowing the field by up to ~223 bytes with attacker-controlled data, corrupting adjacent pointer fields in the heap-allocated scan entry. This is reachable by any unauthenticated attacker within radio range and can be triggered with a single crafted frame during the automatic background scan interval.

Root cause

sta_add() in sys/netproto/802_11/wlan/ieee80211_scan_sta.c:310-312:

#ifdef IEEE80211_SUPPORT_MESH
    if (sp->meshid != NULL && sp->meshid[1] != 0)
        memcpy(ise->se_meshid, sp->meshid, 2+sp->meshid[1]);
#endif

ise->se_meshid is declared as uint8_t se_meshid[2+IEEE80211_MESHID_LEN] in sys/netproto/802_11/ieee80211_scan.h:282, where IEEE80211_MESHID_LEN == 32 (sys/netproto/802_11/ieee80211.h:200), giving a destination of exactly 34 bytes.

sp->meshid[1] is the IE length byte from the attacker-controlled beacon/probe-response frame. As a uint8_t, it can be up to 255, making the copy size 2+255 = 257 β€” overflowing se_meshid by 223 bytes.

The Mesh ID IE is not validated upstream. The generic beacon parser ieee80211_parse_beacon() in sys/netproto/802_11/wlan/ieee80211_input.c:621-622 simply assigns:

case IEEE80211_ELEMID_MESHID:
    scan->meshid = frm;

with no IEEE80211_VERIFY_ELEMENT() call and no BPARSE_* status bit for an oversized Mesh ID. This contrasts with the mesh-specific receive path (ieee80211_mesh.c:2075) which correctly validates with IEEE80211_VERIFY_ELEMENT(meshid, IEEE80211_MESHID_LEN, ...).

IEEE80211_SUPPORT_MESH is enabled by default in the generic kernel (sys/config/X86_64_GENERIC:256).

Threat model & preconditions

  • Attacker position: unauthenticated, within WiFi radio range of the target interface.
  • Privileges gained or impact: remote kernel heap corruption with full attacker-controlled data. Immediate kernel panic (DoS). With heap grooming of the M_80211_SCAN slab, controlled read/write of kernel memory and potential remote code execution.
  • Required config or capabilities: none. The target interface must be scanning (which occurs automatically every few seconds when associated via background scan, or during any manual/active scan in sta, hostap, ibss, or mbss modes).
  • Reachability: send a single 802.11 beacon or probe-response frame with a Mesh ID IE whose length byte exceeds 32. The frame must have a valid fixed header and valid/absent SSID and rates IEs so that ieee80211_parse_beacon() returns successfully.

Proof of concept

PoC source: findings/poc/DF-0393/poc.py

Build & run

# Requires a WiFi adapter in monitor/inject mode (e.g. ath9k)
python3 poc.py --iface wlan0mon --target <victim-bssid>

Expected output

The victim kernel panics with a heap corruption fault:

Fatal trap 12: page fault while in kernel mode
virtual address = 0x<corrupted pointer from se_ies>
cpuid = 0
KDB: stack backtrace:
#0 ...
#1 sta_add at ieee80211_scan_sta.c:312
#2 sta_rx_mgmt at ieee80211_scan_sta.c:...
#3 ieee80211_deliver_l2 ...

Impact

  • Remote unauthenticated kernel heap overflow β€” the most severe class of WiFi vulnerability. Any device within radio range can exploit it.
  • The overflow corrupts struct ieee80211_scan_entry fields immediately following se_meshid: se_ies (a struct ieee80211_ies containing numerous IE data pointers), se_age, and the TAILQ/LIST link pointers (se_list, se_hash). These corrupted pointers are later dereferenced by ieee80211_ies_expand(), select_bss(), sta_iterate(), and adhoc_age().
  • Reliable kernel panic from corrupted pointer dereference.
  • Potential remote code execution with heap grooming: the attacker can flood beacons to fill the M_80211_SCAN slab with controlled data, then trigger the overflow to overwrite a function pointer or vtable entry.
  • This is distinct from DF-0285 (which targeted the mesh-specific receive path in ieee80211_mesh.c). DF-0393 targets the generic beacon parsing path used by ALL operating modes (sta, hostap, ibss, mbss).

Bounds-check the Mesh ID length before copying, matching the SSID/rates protection pattern:

--- a/sys/netproto/802_11/wlan/ieee80211_scan_sta.c
+++ b/sys/netproto/802_11/wlan/ieee80211_scan_sta.c
@@ -308,8 +308,12 @@ sta_add(const struct ieee80211_scanparams *sp,
    ise->se_capinfo = sp->capinfo;
 #ifdef IEEE80211_SUPPORT_MESH
-   if (sp->meshid != NULL && sp->meshid[1] != 0)
-       memcpy(ise->se_meshid, sp->meshid, 2+sp->meshid[1]);
+   if (sp->meshid != NULL && sp->meshid[1] != 0) {
+       uint8_t mlen = sp->meshid[1];
+       if (mlen > IEEE80211_MESHID_LEN)
+           mlen = IEEE80211_MESHID_LEN;
+       memcpy(ise->se_meshid, sp->meshid, 2 + mlen);
+   }
 #endif

Additionally, for defense-in-depth, add validation in ieee80211_parse_beacon() (ieee80211_input.c:621-622):

    case IEEE80211_ELEMID_MESHID:
        scan->meshid = frm;
+       IEEE80211_VERIFY_ELEMENT(scan->meshid,
+           IEEE80211_MESHID_LEN, status |= IEEE80211_BPARSE_MESHID_INVALID);
        break;

References

  • CVE-2022-23088 / FreeBSD-SA-22:07.wifi_meshid β€” the identical upstream bug in FreeBSD's net80211, patched April 2022. m00nbsd's ZDI writeup (https://www.thezdi.com/blog/2022/6/15/cve-2022-23088-exploiting-a-heap-overflow-in-the-freebsd-wi-fi-stack) demonstrates full remote kernel RCE via a 4-beacon page-table manipulation chain against the same se_meshid -> se_ies overflow. The struct layout, root cause, and write-what-where primitive are byte-for-byte identical here. DragonFlyBSD forked from FreeBSD's net80211 and never backported SA-22:07, so DF-0393 is CVE-2022-23088 still live in DFly.
  • DF-0285: Same class of overflow in the mesh-specific receive path (ieee80211_mesh.c:2063-2064) β€” that path has upstream validation but a separate overflow site.
  • IEEE 802.11-2020 Β§9.4.2.113 Mesh ID element: max length 32 octets.
  • IEEE80211_VERIFY_ELEMENT macro is defined in sys/netproto/802_11/wlan/ieee80211_input.h.

Timeline

  • 2026-07-01 Discovered during automated audit.
  • 2026-07-01 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0393 Β· 15 files
FileTypeDescriptionSize
harness.c trigger-source faithful code-level harness: verbatim sta_add:310-312 memcpy + byte-accurate struct ieee80211_scan_entry + poisoned canary 11.3 KB view raw
harness_fixed.c exploit-chain same harness with PATCHED verbatim snippet, for fix validation (clamp -> no overflow) 12.2 KB view raw
poc.py trigger-source original runtime scapy beacon-injection trigger (requires WiFi HW, retained for reference) 3.8 KB view raw
build.sh build-script builds harness + harness_inv (INVARIANTS-trap analog) 553 B view raw
run.sh run-script decisive run: silent OOB + INVARIANTS-trap + negative control 1020 B view raw
fix.diff suggested-fix git-apply-able clamp on meshid[1] at sta_add:310-312 (root-cause fix) 1.0 KB view raw
build.log build-log full harness build output (clean, no warnings) 254 B view raw
run.log run-log decisive unpatched #0 run (Jul 16 re-validation): 168-byte overflow, se_ies pointers corrupted, INVARIANTS-trap, negative control 3.9 KB view raw
fix_build.log build-log single-fix kernel build (rc=0), tail 4.6 KB view raw
fix_run.log run-log post-fix #1 kernel: clamp -> 34-byte copy for meshid[1]=200 & 255, se_ies/se_age/canary intact; + control showing unfixed snippet still overflows 4.2 KB view raw
env.txt environment uname (#1 patched), kern.version, /boot/kernel/kernel sha256, cc 8.3, ifconfig -l (no wlan), kldstat, mesh kernconf option 880 B view raw
VERDICT.md verdict full narrative: bug trace, harness methodology, impact ceiling, fix validation + Jul 16 independent re-validation 9.9 KB ↓ raw
README.md readme human reproduce doc 2.0 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human reproduce doc
↓ download raw

DF-0393 PoC β€” Remote Mesh ID heap overflow in ieee80211_scan_sta.c

Summary

Sends a crafted 802.11 beacon with an oversized Mesh ID information element (length byte = 200, exceeding the 32-byte maximum). The scan subsystem copies 2 + 200 = 202 bytes into the 34-byte se_meshid field, overflowing by ~168 bytes into adjacent heap-allocated struct fields including IE pointers and list links.

Build

pip install scapy

Run

# Put WiFi adapter in monitor mode
sudo airmon-ng start wlan0
# Run the PoC on the monitor interface
sudo python3 poc.py --iface wlan0mon --count 100

Expected output

The victim kernel panics with a page fault when dereferencing a corrupted pointer from the overflowed se_ies struct or se_list/se_hash links:

Fatal trap 12: page fault while in kernel mode
virtual address = 0xdeadbeef41414141
cpuid = 0
KDB: stack backtrace:
#0  mi_switch+0x...
#1  sta_add+0x... at ieee80211_scan_sta.c:312
#2  sta_rx_mgmt+0x... at ieee80211_scan_sta.c:...
#3  ieee80211_deliver_l2+0x...
#4  ieee80211_input+0x...

How it works

  1. ieee80211_parse_beacon() (ieee80211_input.c:621-622) stores the Mesh ID IE pointer without length validation.
  2. sta_add() (ieee80211_scan_sta.c:312) executes memcpy(ise->se_meshid, sp->meshid, 2 + sp->meshid[1]).
  3. ise->se_meshid is 34 bytes; the copy writes 202 bytes.
  4. The 168 overflow bytes corrupt: - se_ies (struct of IE data pointers) - se_age (int) - se_list / se_hash (TAILQ/LIST link pointers)
  5. When the scan table is subsequently walked (select_bss, sta_iterate, adhoc_age, ieee80211_ies_expand), corrupted pointers are dereferenced β†’ kernel panic.

Notes

  • The victim interface must be in scanning mode (background scan runs automatically every few seconds when associated).
  • IEEE80211_SUPPORT_MESH is compiled in by default in X86_64_GENERIC.
  • This is the same class of bug as DF-0285 but in a different code path (generic beacon parser vs. mesh-specific receive path).
VERDICT.md verdict full narrative: bug trace, harness methodology, impact ceiling, fix validation + Jul 16 independent re-validation
↓ download raw

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:

#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:

    uint8_t     se_meshid[2+IEEE80211_MESHID_LEN];   // 34 bytes

Constant β€” sys/netproto/802_11/ieee80211.h:200:

#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:

#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:

    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):

    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

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline 223B overflow; patched clamp to 32, 0B overflow. Compile+boot+harness.

BEFORE: 223B overflow, se_ies corrupted. AFTER: 34B copy, all intact.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 16 02:19:43 UTC 2026

Confirmed kernel references

Detail

Exploit chain

BLOCKED by valid hard blocker: no WiFi HW on guest. Primitive: 168-223B fully attacker-controlled heap OOB write into struct ieee80211_ies (function-pointer-bearing). Remote RCE ceiling per public m00nbsd writeup.

Evidence (decisive lines)

BEFORE: meshid[1]=200 -> 168B overflow, se_ies.wpa_ie=0x4242414141414141, canary corrupted. meshid[1]=255 -> 223B overflow. AFTER: clamped to 32, 34B copy, all intact.

PoC changes

harness.c (verbatim memcpy + byte-accurate struct), harness_fixed.c (clamped), poc.py (scapy trigger for WiFi HW), fix.diff (clamp meshidlen=min(meshid[1],IEEE80211_MESHID_LEN) at :310), VERDICT.md, manifest.json.

Verified recommended fix

Clamp meshidlen=min(sp->meshid[1],IEEE80211_MESHID_LEN) at scan_sta.c:310 before memcpy at :312. Matches finding proposal. CVE-2022-23088 fix. Full diff in findings/poc/DF-0393/fix.diff.

Verdict

REPRODUCED (harness). sta_add scan_sta.c:312 memcpy(ise->se_meshid, sp->meshid, 2+sp->meshid[1]) with NO bounds check. se_meshid[34] but meshid[1] up to 255 -> 257B copy -> 223B overflow into se_ies (function ptrs), se_age, canary. CVE-2022-23088/FreeBSD-SA-22:07.wifi_meshid twin. No WiFi HW on guest.