โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0285

Remote heap overflow via unchecked MESHID IE length in ieee80211_parse_meshid

Summary

ieee80211_parse_meshid(:3456-3461) memcpy(ni->ni_meshid,ie+2,ie[1]) with NO check ie[1]<=IEEE80211_MESHID_LEN(32). ie[1] is attacker-controlled uint8 up to 255. ni_meshid is fixed 32-byte array. Up to 223 bytes overflow into ni_mlstate/ni_mllid/ni_mlpid/ni_mltimer(callout with function pointer). Remote: mesh peer sends beacon with MESHID IE length>32 matching mesh ID prefix. Controllable heap overflow into function-pointer-bearing object -> potential remote code execution via WiFi adjacency.

PoC verification

Evidence pack

findings/poc/DF-0285 ยท 11 files
FileTypeDescriptionSize
layout_proof.c trigger-source userspace offsetof() replica of the 11s-state block + struct callout; prints the overflow-reach table (ni_mltimer.toc clobbered at ie[1]>=48) 8.7 KB view raw
frame_craft.c exploit-payload deterministic builder for the attacker 802.11 mesh beacon (MESHID IE len=0xFF); emits a DLT_IEEE802_11 pcap for radio replay 6.2 KB view raw
df0285_beacon.pcap attack-artifact generated 341-byte crafted beacon (id=0x72 len=0xFF 'DF285'+0x41 pad) 341 B view raw
build.sh build-script cc -O2 -Wall both programs 482 B view raw
run.sh run-script 3-part code-level proof: layout table + payload build + live-kernel symbol/ifconfig check 2.1 KB view raw
build.log build-log full untrimmed compiler output (clean) 101 B view raw
run.log run-log full untrimmed run output (layout table, pcap bytes, live symbols, no-wifi) 4.2 KB view raw
env.txt environment uname, cc 8.3, kldstat, live nm symbols, ifconfig=vtnet0 lo0, GENERIC mesh option, /sys status 1.2 KB view raw
VERDICT.md verdict full narrative: trigger->sink trace, field-offset proof, function-pointer-claim correction, reachability, related secondary scan-cache overflow 10.1 KB โ†“ raw
README.md readme build/run/expected + how to fire on real wifi HW 4.3 KB โ†“ raw
fix.diff suggested-fix git-apply-able clamp of ie[1] to IEEE80211_MESHID_LEN in ieee80211_parse_meshid (validated: git apply --check RC 0); supersedes (no prior proposal existed) 821 B view raw
README.md readme build/run/expected + how to fire on real wifi HW
โ†“ download raw

DF-0285 โ€” PoC evidence pack

Finding: ieee80211_parse_meshid heap OOB write via unchecked MESHID IE length. File: sys/netproto/802_11/wlan/ieee80211_mesh.c:3456-3461 Class: CWE-787 heap overflow (802.11s mesh receive path). Severity: High (remote, wifi-adjacency; function-pointer-control surface).

TL;DR verdict

INCONCLUSIVE โ€” real bug, code-level-confirmed; NOT runtime-triggerable on this guest (no wifi hardware). The vulnerable function is statically linked into the live kernel and IEEE80211_SUPPORT_MESH is enabled in the GENERIC config, but ifconfig -l shows only vtnet0 lo0 and there is no virtual/software frame-injection path into ieee80211_parse_beacon. See VERDICT.md for the full trace and the precise function-pointer correction.

What this pack proves

  1. The overflow is real โ€” memcpy(ni->ni_meshid, ie+2, ie[1]) at ieee80211_mesh.c:3460 with ie[1] attacker-controlled and unchecked, into a fixed 32-byte array. (Source trace in VERDICT.md.)
  2. The overflow reaches the ni_mltimer.toc pointer (and a second one, ni_mlhtimer.toc) โ€” proven by offsetof() arithmetic in layout_proof (numeric, ABI-checked, deterministic). ni_mltimer.toc is clobbered for any ie[1] >= 48; worst case ie[1]=255 gives a 223-byte OOB write.
  3. The function-pointer claim is corrected: struct callout does not hold a function pointer directly; it holds toc, a pointer to a separately allocated struct _callout that holds the function pointers. So the primitive is a controlled-pointer-deref / type confusion (forge toc โ†’ forged _callout.qfunc), not a direct function-pointer field overwrite. Real RCE surface, but the finding's one-line wording is slightly imprecise.
  4. The vulnerable code ships in the production kernel (nm on /boot/kernel/kernel shows ieee80211_parse_meshid live); it is merely unreachable without a wifi driver+radio.

Build

./build.sh

Builds two pure-userspace programs (layout_proof, frame_craft) with the guest's base cc. No kernel contact.

Run

./run.sh

Runs the three-part code-level proof: 1. layout_proof โ€” overflow-reach table. 2. frame_craft โ€” builds the attacker beacon pcap (df0285_beacon.pcap). 3. live-kernel symbol + config + ifconfig check.

Expected output

  • layout_proof prints the ABI sanity checks (all OK), the field-offset table, and the reach matrix showing ni_mltimer.toc clobbered for ie[1]>=48 and both callout pointers for ie[1]>=80.
  • frame_craft writes a 341-byte pcap whose MESHID IE is 72 ff 44 46 32 38 35 41 41 โ€ฆ (id=0x72, len=0xFF, "DF285", 'A'*250).
  • the symbol check prints T ieee80211_parse_meshid, T ieee80211_mesh_init_neighbor, t mesh_peer_timeout_cb, and ifconfig -l โ‡’ vtnet0 lo0.

Because there is no wifi HW, no kernel panic or memory corruption is observable at runtime on this guest. That is the expected, honest result for a wifi-receive-path bug; the source/layout proof is the deliverable.

To actually fire it (requires real wifi HW โ€” NOT in this audit)

On a host with a monitor+inject-capable 802.11 radio (e.g. ath9k) and a DragonFly vap created in MBSS mesh mode:

# victim: create a mesh vap on a real radio
ifconfig wlan0 create wlandev ath0 wlanmode mesh
ifconfig wlan0 meshid mymesh up

# attacker: replay the crafted beacon at the victim
tcpreplay -i wlan0mon df0285_beacon.pcap    # Linux inject adapter
# or feed df0285_beacon.pcap to the injecting driver directly

On receipt, ieee80211_parse_beacon stores the IE (ieee80211_input.c:622), ieee80211_mesh_init_neighbor (ieee80211_mesh.c:3471) calls ieee80211_parse_meshid, and the 223-byte OOB write corrupts ni_mltimer.toc et al.

Files

file purpose
layout_proof.c userspace offsetof() overflow-reach proof (decisive evidence)
frame_craft.c deterministic attacker-beacon pcap builder
df0285_beacon.pcap the generated 341-byte payload
build.sh / run.sh exact build/run
build.log / run.log full untrimmed outputs
env.txt guest environment (uname, cc, kldstat, nm, ifconfig)
VERDICT.md full narrative trace + reachability + function-pointer correction
fix.diff standalone git apply-able clamp fix (validated)
manifest.json machine-readable catalog
VERDICT.md verdict full narrative: trigger->sink trace, field-offset proof, function-pointer-claim correction, reachability, related secondary scan-cache overflow
โ†“ download raw

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:

#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

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)
  .toc +47 pointer to struct _callout
ni_mlrcnt +71 uint8
ni_mltval +72 uint8
ni_mlhtimer +79 struct callout (24B)
  .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:

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

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.

ieee80211_scan_sta.c:312:

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.

Confirmed kernel references

Detail

Exploit chain

On a host with a wifi adapter in MBSS mesh mode: attacker within wifi range transmits a crafted beacon whose MESHID IE has len=0xFF (built deterministically by frame_craft.c -> df0285_beacon.pcap). Receipt stores the IE at ieee80211_input.c:622, flows through ieee80211_mesh_init_neighbor (ieee80211_mesh.c:3471) to ieee80211_parse_meshid, writing 223 attacker-controlled bytes past ni_meshid[32]. This corrupts ni_mlstate (peering FSM), ni_mllid/ni_mlpid (link ids), and crucially ni_mltimer.toc + ni_mlhtimer.toc (callout internal pointers). Since ni_mltimer is armed with mesh_peer_timeout_cb via callout_reset, the callout subsystem will later deref cc->toc and invoke toc->qfunc(toc->qarg) -- a forged toc -> forged _callout -> arbitrary kernel function call with controlled arg => kernel PC control -> RCE/LPE. Full chain NOT developed to uid0 here because there is no runtime trigger on this guest (out of scope for wifi-receive-path bug on headless KVM); realistic blockers on a real target: KASLR + heap randomization + SMAP/SMEP, plus a race between the overflow write and the callout firing.

Evidence (decisive lines)

VERDICT.md (full trace), layout_proof.c/layout_proof (offsetof table: ni_mltimer.toc rel +47 clobbered for ie[1]>=48, ni_mlhtimer.toc rel +79 for ie[1]>=80, 223-byte OOB for ie[1]=255), frame_craft.c/df0285_beacon.pcap (341-byte beacon with MESHID IE id=0x72 len=0xFF 'DF285'+0x41 pad), run.log (full 3-part proof output), env.txt (nm shows T ieee80211_parse_meshid live; ifconfig=vtnet0 lo0), fix.diff (git apply --check RC=0).

PoC changes

Authored the entire evidence pack from scratch (no prior markdown or poc folder existed): layout_proof.c (userspace offsetof replica of the 11s-state block + struct callout/_callout proving overflow reach), frame_craft.c (deterministic attacker-beacon pcap builder emitting a DLT_IEEE802_11 frame with weaponized MESHID IE id=0x72 len=0xFF), build.sh/run.sh, README.md, VERDICT.md, manifest.json, env.txt, fix.diff (clamp ie[1] to IEEE80211_MESHID_LEN, validated git apply --check RC=0).

Verified recommended fix

Clamp ie[1] to IEEE80211_MESHID_LEN in ieee80211_parse_meshid before the memcpy and store the clamped length (full git-apply-able diff in findings/poc/DF-0285/fix.diff, validated git apply --check RC=0). No prior finding-proposal diff existed (finding had no markdown), so this is the authoritative fix. Recommend also clamping the related secondary overflow at ieee80211_scan_sta.c:312 (memcpy into 34-byte se_meshid with 2+sp->meshid[1]).

Verdict

The claimed heap overflow is REAL and exactly as described at the source level: ieee80211_mesh.c:3460 does memcpy(ni->ni_meshid, ie+2, ie[1]) with ie[1] attacker-controlled and unchecked into a fixed 32-byte array (IEEE80211_MESHID_LEN=32, ieee80211.h:200), fed by an unchecked IE pointer stored at ieee80211_input.c:622 (contrast line 602 where ERP DOES validate ie[1]). The vulnerable function 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 X86_64_GENERIC:256. CORRECTION to the function-pointer claim: the overflow does NOT directly overwrite a function pointer field -- struct callout (callout.h:77) holds only a toc POINTER to a separately-allocated struct _callout (callout.h:54) which holds the qfunc/rfunc pointers; so the primitive is a controlled-pointer-deref/type-confusion (forge ni_mltimer.toc -> forged _callout.qfunc, armed via callout_reset at ieee80211_mesh.c:3055,3069), still a genuine RCE surface but via indirect pointer control. layout_proof.c confirms via offsetof() that ni_mltimer.toc is clobbered for ie[1]>=48 and ni_mlhtimer.toc for ie[1]>=80 (worst case ie[1]=255 -> 223-byte OOB). The bug CANNOT be triggered at runtime on this guest: ifconfig -l => vtnet0 lo0 (no wifi HW), there is no virtual/software frame-injection path into ieee80211_parse_beacon, and no ioctl/sysctl reaches the sink -- so honest classification is INCONCLUSIVE (real shipped bug, unreachable without a radio).