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

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
FileTypeDescriptionSize
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
README.md readme build/run/expected + how to reach the in-kernel sink on wifi hardware
โ†“ download 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.md verdict full narrative: code-level proof line-by-line + reachability classification (real shipped bug, unreachable without a wifi vap)
โ†“ download raw

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:

  • :1594 vap->iv_des_ssid[0].len = ssid_len; โ†’ stores 255. iv_des_ssid is struct ieee80211_scan_ssid iv_des_ssid[1] (ieee80211_var.h:400), so iv_des_ssid[0].ssid is exactly 32 bytes.
  • :1595 memcpy(vap->iv_des_ssid[0].ssid, ssid, ssid_len); โ†’ HEAP OVERFLOW #1: 255 bytes into a 32-byte field inside the struct ieee80211vap heap object โ†’ 223 bytes past the end, corrupting the following vap fields (and, depending on slab layout, the adjacent slab object).
  • :1600 memcpy(sr->sr_ssid[0].ssid, ssid, ssid_len); sr is kmalloc(sizeof(*sr), M_TEMP, โ€ฆ) (:1584); sr_ssid[3] each with a 32-byte ssid (ieee80211_ioctl.h:790). โ†’ HEAP OVERFLOW #2: 255 bytes into sr_ssid[0].ssid, spilling across sr_ssid[1], sr_ssid[2] and past the end of the kmalloc'd object. (A later check at :2496 if (sr->sr_ssid[i].len > IEEE80211_NWID_LEN) does not save you โ€” the overflow at :1600 has already happened before ieee80211_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 wlan IS in X86_64_GENERIC:258; nm /boot/kernel/kernel shows 508 ieee80211_* symbols plus wlan_clone_create / wlan_cloner โ€” the wlan stack (and the vulnerable static setmlme_assoc_adhoc) is statically linked into the running kernel. kldload wlan reports "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 lo0 only โ€” no wlan interface.
  • ifconfig wlan create wlandev <ath0|run0|iwm0|iwn0|wpi0|ral0|rum0|bwi0|ipw0> โ†’ SIOCIFCREATE2: Device not configured for 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_ASSOC with im_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

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 ' -> 'SIOCIFCREATE2: Device not configured' for every radio name -> no vap can ever exist -> SIOCS80211/SIOCG80211 are never delivered. The wlan stack IS statically linked (508 ieee80211 syms, device wlan in X86_64_GENERIC:258) so the vulnerable code ships; it just cannot be triggered from userspace here. Privilege note: the SET path is gated by caps_priv_check_self(SYSCAP_NONET_WIFI) at :3472 (root-equivalent), so even with a radio this is a local-privileged -> kernel-corruption primitive.