Heap buffer overflow in setmlme_assoc_adhoc: unbounded im_ssid_len into 32-byte buffers
Summary
setmlme_assoc_adhoc(:1580) only checks ssid_len==0 NOT >IEEE80211_NWID_LEN(32). memcpy into iv_des_ssid[0].ssid[32](:1594) and sr->sr_ssid[0].ssid[32](:1600) with len up to 255 -> ~223 bytes heap overflow into ieee80211vap/scan_req. Sibling IOC_SSID handler(:2672) correctly checks. Amplifies: GET IOC_SSID(:809) copies corrupted len into stack tmpssid[32] -> stack overflow. WiFi-capability-gated.
PoC verification
Evidence pack
findings/poc/DF-0291 ยท 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| overflow_proof.c | trigger-source | userspace MODEL of the kernel path: reproduces the heap overflow (iv_des_ssid[0].ssid / sr->sr_ssid[0].ssid, :1595/:1600) and the GET-path stack overflow (tmpssid, :810); prints concrete 223-byte-past-32-byte counts | 8.0 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o overflow_proof overflow_proof.c | 269 B | view raw |
| run.sh | run-script | ./overflow_proof | 362 B | view raw |
| build.log | build-log | full untrimmed final build output (BUILD_EXIT=0) | 123 B | view raw |
| run.log | run-log | full untrimmed run output: the two heap overflows + the stack overflow counts | 1.2 KB | view raw |
| env.txt | environment | uname, cc, kldstat, wlan-symbol check (508 ieee80211 syms), ifconfig wlan create reachability probe (ENXIO for every radio), no wlan interface present | 2.6 KB | view raw |
| fix.diff | suggested-fix | git-apply-able fix: clamp ssid_len<=IEEE80211_NWID_LEN in setmlme_assoc_adhoc (:1580) + defense-in-depth clamp on GET-path tmpssid sink (:809); git apply --check OK | 910 B | view raw |
| VERDICT.md | verdict | full narrative: code-level proof line-by-line + reachability classification (real shipped bug, unreachable without a wifi vap) | 7.9 KB | โ raw |
| README.md | readme | build/run/expected + how to reach the in-kernel sink on wifi hardware | 3.3 KB | โ raw |
DF-0291 โ heap overflow in setmlme_assoc_adhoc (SIOCS80211 MLME ASSOC adhoc)
Claim (from the DB finding row, High / CWE-787 / confidence certain)
setmlme_assoc_adhoc (sys/netproto/802_11/wlan/ieee80211_ioctl.c:1568) only
checks ssid_len == 0 at :1580, not ssid_len > IEEE80211_NWID_LEN (32).
It then memcpys an attacker-controlled length (up to 255, since
struct ieee80211req_mlme.im_ssid_len is uint8_t) into the 32-byte
vap->iv_des_ssid[0].ssid (:1595) and sr->sr_ssid[0].ssid (:1600) โ
~223-byte heap overflow into struct ieee80211vap / the kmalloc'd
struct ieee80211_scan_req. The sibling IEEE80211_IOC_SSID SET handler
(:2671) does bound ireq->i_len at :2673, so the adhoc path is the only
unguarded sink. Amplification: the GET IEEE80211_IOC_SSID handler (:805)
reads the corrupted iv_des_ssid[0].len at :809 and memcpys it into a
32-byte stack buffer tmpssid at :810 โ stack overflow. WiFi-capability
gated (root / SYSCAP_NONET_WIFI).
Verdict
INCONCLUSIVE โ real shipped bug, unreachable on this audit guest.
The overflow is real and confirmed line-by-line in the audited master DEV
source (overflow_proof.c reproduces the exact vulnerable logic and prints the
byte counts). It is not reachable from userspace on the KVM guest because
the SIOCS80211/SIOCG80211 ioctls are wired only to wlan vap interfaces,
and a wlan vap can only be created through wlan_clone_create
(ieee80211_dragonfly.c:80), which requires a wifi radio parent and returns
ENXIO when none is registered (:92-94). The guest has vtnet0/lo0 only,
no radio hardware, and no attached radio driver, so no vap can ever exist and
the ioctl is never delivered. See VERDICT.md for the full trace and
env.txt for the live reachability probe.
Files in this evidence pack
| file | what it is |
|---|---|
overflow_proof.c |
userspace MODEL of the kernel path โ demonstrates the heap + stack overflow byte counts concretely |
build.sh / run.sh |
exact build & run |
build.log / run.log |
full untrimmed build & run output |
env.txt |
guest uname, cc, kldstat, wlan-symbol check, and the ifconfig wlan create reachability probe |
VERDICT.md |
full narrative: code-level proof + reachability classification |
fix.diff |
git apply-able fix for BOTH the set-path overflow and the GET-path stack overflow |
manifest.json |
machine-readable catalog |
Reproduce
./build.sh && ./run.sh # prints the heap (223B past 32B) + stack (223B past 32B) overflow counts
To reach the in-kernel sink you need a host with a wifi radio + driver that
registers an ieee80211com, create a vap (ifconfig wlan create wlandev <radio>
wlanmode adhoc), then issue SIOCS80211 with im_op=IEEE80211_MLME_ASSOC and
im_ssid_len=255. That hardware is absent on this guest, so no in-kernel
trigger was attempted.
Privilege
The SET path is gated by caps_priv_check_self(SYSCAP_NONET_WIFI) at
ieee80211_ioctl.c:3472 (root-equivalent). So even with a radio present this is
a local privileged โ kernel memory corruption primitive, not unprivileged.
On a wifi-equipped embedded/AP box where the radio is auto-configured and a
semi-trusted context holds the wifi capability, it becomes a real concern.
VERDICT โ DF-0291
Finding: heap buffer overflow in setmlme_assoc_adhoc
(sys/netproto/802_11/wlan/ieee80211_ioctl.c:1568), with a stack-overflow
amplification in the GET IEEE80211_IOC_SSID handler.
Verdict: INCONCLUSIVE โ the bug is REAL and shipped in the audited master DEV kernel, but it is NOT reachable from userspace on the KVM audit guest (no wifi radio โ no wlan vap โ the ioctl is never delivered).
1. The overflow is REAL โ confirmed line-by-line
Call chain (SET path)
ieee80211_ioctl() (ieee80211_ioctl.c:3377) is the if_ioctl of wlan vap
interfaces. For SIOCS80211 it runs, at :3471-3475:
case SIOCS80211:
error = caps_priv_check_self(SYSCAP_NONET_WIFI); /* root / wifi cap */
if (error == 0)
error = ieee80211_ioctl_set80211(vap, cmd, (struct ieee80211req *) data);
โ ieee80211_ioctl_setmlme() (:1611) copyins a struct ieee80211req_mlme
(:1618) and, for IBSS/AHDEMO opmode + IEEE80211_MLME_ASSOC, calls
(:1628-1629):
return setmlme_assoc_adhoc(vap, mlme.im_macaddr, mlme.im_ssid_len, mlme.im_ssid);
im_ssid_len is uint8_t (ieee80211_ioctl.h:309) โ attacker-controlled
0..255. im_ssid is uint8_t im_ssid[IEEE80211_NWID_LEN] (32 bytes) and is
the last field of the struct.
The missing guard
setmlme_assoc_adhoc() (:1568) at :1580:
if (ssid_len == 0)
return EINVAL;
It checks only ssid_len == 0, never ssid_len > IEEE80211_NWID_LEN.
Compare the sibling IEEE80211_IOC_SSID SET handler at :2671-2674:
case IEEE80211_IOC_SSID:
if (ireq->i_val != 0 ||
ireq->i_len > IEEE80211_NWID_LEN) /* <-- correct bound */
return EINVAL;
So the adhoc path is the only unguarded sink.
The overflows
With ssid_len = 255:
:1594vap->iv_des_ssid[0].len = ssid_len;โ stores 255.iv_des_ssidisstruct ieee80211_scan_ssid iv_des_ssid[1](ieee80211_var.h:400), soiv_des_ssid[0].ssidis exactly 32 bytes.:1595memcpy(vap->iv_des_ssid[0].ssid, ssid, ssid_len);โ HEAP OVERFLOW #1: 255 bytes into a 32-byte field inside thestruct ieee80211vapheap object โ 223 bytes past the end, corrupting the following vap fields (and, depending on slab layout, the adjacent slab object).:1600memcpy(sr->sr_ssid[0].ssid, ssid, ssid_len);sriskmalloc(sizeof(*sr), M_TEMP, โฆ)(:1584);sr_ssid[3]each with a 32-bytessid(ieee80211_ioctl.h:790). โ HEAP OVERFLOW #2: 255 bytes intosr_ssid[0].ssid, spilling acrosssr_ssid[1],sr_ssid[2]and past the end of the kmalloc'd object. (A later check at:2496if (sr->sr_ssid[i].len > IEEE80211_NWID_LEN)does not save you โ the overflow at:1600has already happened beforeieee80211_scanreq()is called at:1604.)
Source content: ssid is &mlme.im_ssid; the first 32 bytes are the
attacker-supplied SSID, bytes 33..255 are kernel-stack residue (the read
runs past the last field of the stack mlme). So the first 32 bytes of each
overflow are attacker-controlled; the rest is kernel memory. That is enough to
corrupt adjacent heap objects with attacker-chosen data in the leading 32 bytes.
GET-path stack-overflow amplification
ieee80211_ioctl_get80211() (:794), IEEE80211_IOC_SSID INIT/SCAN branch
(:807-811):
char tmpssid[IEEE80211_NWID_LEN]; /* :801 โ 32-byte stack buffer */
...
case IEEE80211_S_INIT:
case IEEE80211_S_SCAN:
ireq->i_len = vap->iv_des_ssid[0].len; /* :809 โ reads corrupted 255 */
memcpy(tmpssid, vap->iv_des_ssid[0].ssid, ireq->i_len); /* :810 โ STACK OVERFLOW */
After the SET path stores len = 255, the GET handler copies 255 bytes into the
32-byte tmpssid โ 223-byte stack overflow (smashes saved frame pointer /
return address / other locals). This is a stronger primitive than the heap
overflow alone: a controlled return-address overwrite is a direct
code-execution / LPE path. The amplification is real. (The default branch at
:813-814 is also reachable via ni_esslen, which is itself derived from the
corrupted iv_des_ssid[0].len at ieee80211_node.c:354-355, so both branches
of the GET handler can overflow tmpssid.)
Code-level proof
overflow_proof.c reproduces the exact vulnerable logic against buffers laid
out like the kernel structs and prints the concrete byte counts. run.log:
HEAP OVERFLOW #1 (iv_des_ssid[0].ssid, :1595): 223 bytes past the 32-byte field ... HEAP OVERFLOW #2 (sr->sr_ssid[0].ssid, :1600): 223 bytes past the 32-byte field ... GET IOC_SSID: memcpy 255 bytes into 32-byte stack tmpssid -> 223 bytes written past end
2. Why it is NOT reachable on this guest
ieee80211_ioctl is only the if_ioctl of wlan vap interfaces
(ieee80211.c:567 ifp->if_ioctl = ieee80211_ioctl). To reach it you must have
a wlanN interface, which is created via the wlan cloner
wlan_clone_create() (ieee80211_dragonfly.c:80). At :88-94:
error = copyin(params, &cp, sizeof(cp)); /* cp.icp_parent = radio name */
...
ic = ieee80211_find_com(cp.icp_parent); /* :92 */
if (ic == NULL)
return ENXIO; /* :94 โ no radio => fail */
A vap therefore requires a registered radio parent (struct ieee80211com),
which only exists when a wifi radio driver (if_ath, if_run, if_iwm, โฆ)
has attached to real hardware. The KVM guest has no wifi hardware.
Live probe (env.txt):
device wlanIS inX86_64_GENERIC:258;nm /boot/kernel/kernelshows 508ieee80211_*symbols pluswlan_clone_create/wlan_clonerโ the wlan stack (and the vulnerable staticsetmlme_assoc_adhoc) is statically linked into the running kernel.kldload wlanreports "already loaded or in kernel".- Loaded modules: only
ehci,xhci,if_wgโ no radio driver. Radio driver.kos exist on disk (if_ath.ko, โฆ) but none can attach (no HW). - Interfaces:
vtnet0 lo0only โ no wlan interface. ifconfig wlan create wlandev <ath0|run0|iwm0|iwn0|wpi0|ral0|rum0|bwi0|ipw0>โSIOCIFCREATE2: Device not configuredfor every radio name, i.e.ieee80211_find_com()returns NULL โENXIO.
โ No vap can ever exist on this guest โ SIOCS80211/SIOCG80211 are never
delivered to ieee80211_ioctl โ the in-kernel overflows cannot be triggered
from userspace here. No in-kernel PoC run was possible; a code-level model
(overflow_proof.c) is used instead, and it confirms the overflow concretely.
This is the textbook inconclusive outcome: a real, shipped, exploitable-looking
bug that is gated behind hardware the audit VM lacks.
3. Privilege / threat model (for when a radio IS present)
- SET path requires
SYSCAP_NONET_WIFI(:3472) โ root-equivalent. On a normal laptop/desktop DragonFly install a local unprivileged user (e.g.maxx, not in wheel) cannot reach it. - The realistic exposure is on wifi-equipped AP / embedded boxes where the
radio is configured and a context holding the wifi capability (a setuid helper,
a management daemon, or a misconfigured service) can be coerced into issuing the
crafted
SIOCS80211 MLME_ASSOCwithim_ssid_len = 255. Given such a foothold, the bug is a kernel heap-write + a stack-smash primitive โ local privilege escalation to kernel/root is plausible. - Remote unauth exposure: none โ ioctl is local-only.
4. Fix
fix.diff clamps ssid_len at the root-cause sink (the missing bound in
setmlme_assoc_adhoc) and adds a defense-in-depth clamp on the GET-path stack
sink so a corrupted stored length can never overflow tmpssid. Both git apply
--check and patch --dry-run pass against the unmodified sys/ tree. See
fix.diff. This matches in spirit the finding's recommendation (bound the
adhoc path the same way the sibling IOC_SSID handler does) and additionally
hardens the GET sink.
Confirmed kernel references
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:1568
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:1580
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:1595
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:1600
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:2673
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:809
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:810
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:3472
- sys/netproto/802_11/ieee80211_ioctl.h:309
- sys/netproto/802_11/ieee80211_var.h:400
- sys/netproto/802_11/wlan/ieee80211_dragonfly.c:92
- sys/netproto/802_11/wlan/ieee80211.c:567
Detail
Exploit chain
Not demonstrated on the guest (unreachable). On wifi-equipped hardware with a SYSCAP_NONET_WIFI foothold the chain would be: crafted SIOCS80211 MLME_ASSOC im_ssid_len=255 -> 223B heap overflow into struct ieee80211vap (corrupts adjacent vap fields / slab neighbor) + 223B overflow of the kmalloc'd scan_req; the stronger primitive is the GET-path stack smash (memcpy 255 into 32-byte tmpssid) -> saved-frame/return-address control -> ROP -> uid0. First 32 bytes of the overflow are attacker-controlled (the user SSID), the rest is kernel-stack residue since im_ssid is the last field of the copyin'd mlme. No in-kernel trigger was possible because no wlan vap exists on this guest.
Evidence (decisive lines)
overflow_proof.c/run.log prove the overflow concretely (HEAP OVERFLOW #1/#2: 223 bytes past 32-byte field; GET IOC_SSID: 255 bytes into 32-byte stack tmpssid -> 223 past end). env.txt proves unreachability: 508 ieee80211 kernel symbols + wlan_cloner present, but kldstat shows no radio driver, ifconfig -l = 'vtnet0 lo0', and 'ifconfig wlan create wlandev <ath0|run0|iwm0|iwn0|wpi0|ral0|rum0|bwi0|ipw0>' -> ENXIO for all. fix.diff (git apply --check OK) fixes both paths.
PoC changes
Authored the entire evidence pack from scratch (no prior folder): overflow_proof.c (userspace MODEL of the kernel path, since the in-kernel sink is unreachable), build.sh/run.sh, VERDICT.md, README.md, env.txt (reachability probe), fix.diff (fixes both set-path and GET-path), manifest.json. The model lays out buffers exactly like ieee80211vap.iv_des_ssid[1] and ieee80211_scan_req.sr_ssid[3] and runs the exact unbounded memcpy at attacker len=255 to print concrete overflow byte counts without crashing.
Verified recommended fix
fix.diff clamps ssid_len in setmlme_assoc_adhoc (:1580) to 'if (ssid_len == 0 || ssid_len > IEEE80211_NWID_LEN) return EINVAL;' (root cause, mirrors the sibling IOC_SSID handler's :2673 check) AND adds a defense-in-depth clamp on the GET-path stack sink (:809) so a corrupted stored length can never overflow tmpssid[32]. git apply --check and patch --dry-run both pass against the unmodified sys/ tree. Matches finding proposal's intent (bound the adhoc path the same way IOC_SSID is bounded) and additionally hardens the GET amplification.
Verdict
INCONCLUSIVE -- real shipped bug, unreachable on this guest. The overflow is CONFIRMED line-by-line: setmlme_assoc_adhoc (ieee80211_ioctl.c:1580) only checks ssid_len==0, never >IEEE80211_NWID_LEN(32); with im_ssid_len a uint8_t (max 255) it memcpy's 255 bytes into iv_des_ssid[0].ssid[32] (:1595) and sr->sr_ssid[0].ssid[32] (:1600) -> 223-byte heap overflow into ieee80211vap/kmalloc'd scan_req. The sibling IOC_SSID SET handler (:2673) correctly bounds ireq->i_len, so the adhoc path is the only unguarded sink. The GET amplification is ALSO real: GET IOC_SSID (:809) reads the corrupted iv_des_ssid[0].len=255 and memcpy's it into the 32-byte stack tmpssid (:810) -> 223-byte stack overflow. overflow_proof.c reproduces these byte counts. HOWEVER the sink is unreachable on the KVM guest: ieee80211_ioctl is only the if_ioctl of wlan VAPs (ieee80211.c:567), and wlan_clone_create (ieee80211_dragonfly.c:92-94) returns ENXIO unless a radio parent ieee80211com exists. The guest has vtnet0/lo0 only, no wifi hardware, no attached radio driver; 'ifconfig wlan create wlandev