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| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
- The overflow is real โ
memcpy(ni->ni_meshid, ie+2, ie[1])atieee80211_mesh.c:3460withie[1]attacker-controlled and unchecked, into a fixed 32-byte array. (Source trace inVERDICT.md.) - The overflow reaches the
ni_mltimer.tocpointer (and a second one,ni_mlhtimer.toc) โ proven byoffsetof()arithmetic inlayout_proof(numeric, ABI-checked, deterministic).ni_mltimer.tocis clobbered for anyie[1] >= 48; worst caseie[1]=255gives a 223-byte OOB write. - The function-pointer claim is corrected:
struct calloutdoes not hold a function pointer directly; it holdstoc, a pointer to a separately allocatedstruct _calloutthat holds the function pointers. So the primitive is a controlled-pointer-deref / type confusion (forgetocโ forged_callout.qfunc), not a direct function-pointer field overwrite. Real RCE surface, but the finding's one-line wording is slightly imprecise. - The vulnerable code ships in the production kernel (
nmon/boot/kernel/kernelshowsieee80211_parse_meshidlive); 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_proofprints the ABI sanity checks (allOK), the field-offset table, and the reach matrix showingni_mltimer.tocclobbered forie[1]>=48and both callout pointers forie[1]>=80.frame_craftwrites a 341-byte pcap whose MESHID IE is72 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, andifconfig -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 |
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_mltimeris 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 dereferencescc->tocand invokestoc->qfunc(toc->qarg).- If the attacker controls
toc(which the overflow gives them, atie[1]>=48), they can redirect it at a forgedstruct _calloutplaced in attacker-influenced kernel memory (e.g. via a separate heap spray, or at a chosen existing address). The forged_callout.qfuncthen 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โ userspaceoffsetof()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 hasid=0x72, len=0xFFand a 253-byte marker body. Writesdf0285_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โ standalonegit 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:
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
- sys/netproto/802_11/wlan/ieee80211_mesh.c:3460
- sys/netproto/802_11/wlan/ieee80211_mesh.c:3459
- sys/netproto/802_11/wlan/ieee80211_mesh.c:3471
- sys/netproto/802_11/wlan/ieee80211_mesh.c:3055
- sys/netproto/802_11/wlan/ieee80211_mesh.c:3069
- sys/netproto/802_11/wlan/ieee80211_input.c:622
- sys/netproto/802_11/wlan/ieee80211_input.c:602
- sys/netproto/802_11/wlan/ieee80211_node.c:844
- sys/netproto/802_11/wlan/ieee80211_node.c:1017
- sys/netproto/802_11/wlan/ieee80211_scan_sta.c:312
- sys/netproto/802_11/ieee80211_node.h:200
- sys/netproto/802_11/ieee80211_node.h:204
- sys/netproto/802_11/ieee80211.h:200
- sys/sys/callout.h:77
- sys/sys/callout.h:66
- sys/config/X86_64_GENERIC:256
- sys/conf/files:1644
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).