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

Signed sr_ssid[i].len in ieee80211_scanreq passes the >NWID_LEN check; -1 reaches memcpy as (size_t)-1 β†’ unbounded kernel-heap copy through the vap (fatal write fault)

Summary

ieee80211_scanreq() validates each scan SSID length with a signed-only upper bound (:2495-2497); sr_ssid[].len is a signed int (ieee80211_ioctl.h:788), so len=-1 passes and, in the S_INIT stash branch, is memcpy'd as (size_t)-1 from the fully copyin-controlled 128-byte ieee80211_scan_req into vap->iv_scanreq_ssid[0].ssid (32-byte array inside struct ieee80211vap) - an unbounded linear kernel-heap overwrite walking the vap tail (including iv_newstate/iv_reset/iv_key_* function pointers) and adjacent heap until a fatal page fault. The non-INIT branch also hands the signed len to ieee80211_start_scan/ieee80211_check_scan. Distinct from DF-0642 (signed sr_nssid, scan.c copy_ssid sink). VERIFIED on stock INVARIANTS guest (fw.ko harness + DISASSOC->S_INIT + SCAN_REQ len=-1): 'Fatal trap 12 ... supervisor write, Stopped at memcpy+0xfa: repe movsq', guest down. Fix validated (reject len<0 and sr_nssid<0): identical PoC returns EINVAL, valid scans accepted. Any holder of SYSCAP_NONET_WIFI (root or delegated wifi-cap operator): deterministic panic, or with heap grooming a controlled-pointer overwrite (first ~128 bytes fully attacker-chosen) -> kernel code execution from a wifi-admin capability; no unpriv entry path.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3295 Β· 16 files
FileTypeDescriptionSize
README.md β€” 4.4 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
scanreq.c β€” 3.9 KB view raw
fw.c β€” 9.7 KB view raw
Makefile β€” 177 B ↓ download
build.sh β€” 87 B view raw
run.sh β€” 111 B view raw
build.log β€” 5.9 KB view raw
run.log β€” 2.9 KB view raw
run.log.baseline β€” 695 B ↓ download
panic.txt β€” 2.2 KB view raw
env.txt β€” 698 B view raw
fix.diff β€” 898 B view raw
fix_build.log β€” 5.9 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 5.5 KB view raw

DF-3295 β€” ieee80211_scanreq: signed sr_ssid[i].len passes the > NWID_LEN check; negative value reaches memcpy as (size_t)-1 β†’ unbounded kernel-heap copy

File: sys/netproto/802_11/wlan/ieee80211_ioctl.c (pass 2 audit, GLM 5.3)

What

ieee80211_scanreq() validates each scan-request SSID length with a signed comparison only (ieee80211_ioctl.c:2495-2497):

    for (i = 0; i < sr->sr_nssid; i++)
        if (sr->sr_ssid[i].len > IEEE80211_NWID_LEN)   /* int len */
            return EINVAL;

struct ieee80211_scan_req's embedded SSID is struct { int len; uint8_t ssid[IEEE80211_NWID_LEN]; } (ieee80211_ioctl.h:787-789), so len = -1 (0xffffffff as int) passes (-1 > 32 is false). When the vap is in IEEE80211_S_INIT and the parent is running, the stash branch copies the SSIDs into the vap (ieee80211_ioctl.c:2536-2540):

    for (i = 0; i < sr->sr_nssid; i++) {
        vap->iv_scanreq_ssid[i].len = sr->sr_ssid[i].len;   /* -1 */
        memcpy(vap->iv_scanreq_ssid[i].ssid,   /* 32-byte array inside struct ieee80211vap */
            sr->sr_ssid[i].ssid,               /* 32-byte array inside kmalloc'd sr (copyin'd) */
            sr->sr_ssid[i].len);               /* (size_t)(-1) */
    }

The negative int sign-extends to SIZE_MAX in memcpy's size_t parameter: an unbounded linear kernel-heap copy whose destination starts inside the ieee80211vap (walking forward over the remainder of the vap β€” including the iv_newstate/iv_reset/iv_key_* function pointers β€” then adjacent heap objects) and whose source starts at the attacker-supplied 128-byte ieee80211_scan_req (fully user-controlled via the preceding copyin) continuing into adjacent heap.

In the non-INIT branch (ieee80211_ioctl.c:2544-2559) the same signed len is handed to ieee80211_start_scan()/ieee80211_check_scan(), which stash it via ieee80211_scan_copy_ssid (struct memcpy β€” no immediate overflow there, but the negative len is now resident in scan state for the probe-request builders).

Distinct from DF-0642 (signed sr_nssid β†’ ieee80211_scan.c copy_ssid memcpy, non-INIT branch): this finding is the signed sr_ssid[].len field reaching the ioctl.c memcpy in the INIT branch β€” different field, different sink, complementary preconditions.

Reproduced (stock INVARIANTS kernel #0)

mlme DISASSOC -> 0            (vap S_SCAN -> S_INIT while UP; ic_nrunning stays 1)
firing SCAN_REQ: sr_nssid=1 sr_ssid[0].len=-1
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff80118830000     (kernel heap, write side)
fault code = supervisor write data, page not present
current process = 1229 (scanreq)
Stopped at memcpy+0xfa: repe movsq (%rsi),%es:(%rdi)

Guest down at the first INIT-branch attempt. (The first loop iteration hit the non-INIT branch and returned EINPROGRESS=36, storing the negative len in scan state β€” the second iteration, after the MLME pushed the vap to S_INIT, hit the memcpy.)

Primitive characterization

  • write: linear overwrite from &vap->iv_scanreq_ssid[0].ssid forward across the whole tail of the ieee80211vap (function pointers!) and adjacent kernel heap, until the first unmapped page.
  • data: first 128 bytes fully attacker-controlled (the copyin'd ieee80211_scan_req), beyond that adjacent-heap contents.
  • gate: SIOCS80211 requires SYSCAP_NONET_WIFI (ieee80211_ioctl.c:3472). No unprivileged path exists, so an unprivileged userβ†’root chain is not reachable; the ceiling is a delegated-capability user converting wifi-admin into full kernel compromise (heap-groom a victim object behind the vap, overwrite with controlled bytes, pivot through iv_newstate-style calls), or trivially a guaranteed panic.

Build / run (repo root)

sh findings/poc/DF-3295/build.sh    # fw.ko (software radio) + scanreq in guest
sh findings/poc/DF-3295/run.sh      # baseline: fatal trap 12 at the ioctl

Harness (fw.c) is DF-3282's software net80211 radio, unchanged: registers fw0 (STA/hostap/ibss capable), nop scan callbacks. Trigger creates a STA vap, brings it UP (β†’ S_SCAN), uses MLME DISASSOC to return it to S_INIT while UP, then fires the SCAN_REQ with sr_ssid[0].len = -1, retrying.

Fix validation

fix.diff rejects sr_ssid[i].len < 0 (and sr_nssid < 0) with EINVAL. Patched kernel (#1, built in-guest): every attempt returns EINVAL, no panic, guest stays up, wlan0 functional, and a valid scan request (len=4) is still accepted (rc=0 via the INIT stash path).

VERDICT.md
↓ download raw

DF-3295 β€” VERDICT

Status: reproduced (impact: panic β€” memory-corruption primitive characterized; escalation ceiling is delegated-cap β†’ kernel compromise, there is no unprivileged entry path).

Finding

ieee80211_scanreq() (sys/netproto/802_11/wlan/ieee80211_ioctl.c) validates scan SSID lengths with a signed-only upper bound:

  • ieee80211_ioctl.c:2495-2497 β€” if (sr->sr_ssid[i].len > IEEE80211_NWID_LEN) return EINVAL; (sr_ssid[i].len is int, ieee80211_ioctl.h:788)

Sink, INIT branch (vap in IEEE80211_S_INIT, ic_nrunning != 0, ieee80211_ioctl.c:2526-2543):

  • ieee80211_ioctl.c:2538-2540 β€” memcpy(vap->iv_scanreq_ssid[i].ssid, sr->sr_ssid[i].ssid, sr->sr_ssid[i].len) with len = -1 β†’ SIZE_MAX.

Secondary (non-INIT branch): ieee80211_ioctl.c:2544-2559 passes the signed len into the scan machinery (ieee80211_start_scan / ieee80211_check_scan), where ieee80211_scan_copy_ssid (ieee80211_scan.c:305) struct-copies it into scan state (no immediate overflow; negative len resident for later probe construction). The signed sr_nssid sibling on that scan.c sink is DF-0642 β€” this finding is the sr_ssid[].len field and the ioctl.c memcpy, i.e. not a re-report.

How it was reproduced

  1. sh findings/poc/DF-3295/build.sh β€” builds DF-3282's software radio KLD (fw.ko: registers fw0, STA/hostap/ibss capable, nop scan callbacks) + scanreq trigger on the guest (stock INVARIANTS kernel #0, DragonFly 6.5-DEVELOPMENT).
  2. ifconfig wlan create wlandev fw0 wlanmode sta up β€” vap UP; state machine reaches S_SCAN (status: no carrier).
  3. ./scanreq wlan0: * ioctl(SIOCS80211, MLME DISASSOC) β†’ 0 β€” STA-mode DISASSOC does ieee80211_new_state(vap, IEEE80211_S_INIT, reason) (ieee80211_ioctl.c:1396-1400) while the interface stays UP, so ic_nrunning stays 1. * ioctl(SIOCS80211, SCAN_REQ, {nssid=1, ssid[0].len=-1}): first iteration hit the SCAN-state branch (returned EINPROGRESS=36 β€” the negative len entered scan state via copy_ssid); after the next MLME the vap was in S_INIT and the stash-branch memcpy ran.
  4. Result (run.log / panic.txt): Fatal trap 12: page fault while in kernel mode, fault virtual address = 0xfffff80118830000, supervisor write data, current process = 1229 (scanreq), Stopped at memcpy+0xfa: repe movsq β€” the destination walk off the vap hit an unmapped kernel page. Guest down; vm.sh status β‡’ down.

Primitive

  • Destination: &vap->iv_scanreq_ssid[0].ssid β€” 32-byte array inside the heap-allocated ieee80211vap; the copy walks the entire vap tail (iv_scanreq_ssid[1..2], and everything after: including function pointers iv_newstate, iv_reset, iv_key_alloc, iv_key_set, …) and then adjacent kernel heap.
  • Source: the 128-byte kmalloc'd ieee80211_scan_req, fully attacker-controlled (copyin at ieee80211_ioctl.c:2583), then adjacent heap.
  • Length: SIZE_MAX β€” copy runs until the fault; on this guest ~the distance to the next unmapped kernel page.

uid0? Not reachable by an unprivileged user: the only entry is SIOCS80211, gated by caps_priv_check_self(SYSCAP_NONET_WIFI) (ieee80211_ioctl.c:3472). Escalation ceiling = a user delegated SYSCAP_NONET_WIFI (wifi administrator) can groom the heap so a chosen object follows the vap and convert this into controlled-pointer overwrite β†’ kernel code exec β†’ full root; or trivially panic. Filed Medium (privileged heap corruption; deterministic local DoS is the demonstrated floor).

Fix validation (fix.diff)

vm.sh reset with-src β†’ applied fix.diff (reject sr_ssid[i].len < 0 and sr_nssid < 0 with EINVAL) in the guest /usr/src β†’ make nativekernel KERNCONF=X86_64_GENERIC β†’ make installkernel β†’ reboot (kernel #1, Thu Sep 10 07:24:45 UTC 2026) β†’ re-ran the exact PoC:

  • All 11 attempts: scanreq -> -1 (errno 22) (EINVAL) β€” the fix's rejection path. No trap, no panic; vm.sh status β‡’ up; wlan0 still responds (ifconfig wlan0 fine).
  • Regression: valid SCAN_REQ (len=4) accepted (rc=0 via the INIT stash path; normal behavior), guest healthy.

Baseline vs patched: fatal trap 12 + guest down β†’ clean EINVAL + guest up. fix_status: fixed.

Notes / honesty

  • errno 36 (EINPROGRESS) on the non-INIT first iteration is normal scan behavior, not an error of the PoC.
  • The guest was reset (vm.sh reset with-src) after validation; no dirty state left behind.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src, kernel rebuilt (make nativekernel KERNCONF=X86_64_GENERIC) and booted: the exact PoC that panicked the stock kernel now returns EINVAL (errno 22) on every attempt, no trap/panic, guest stays up, wlan0 functional, and a valid scan request (len=4) is still accepted (rc=0). Baseline bad behavior gone.

['findings/poc/DF-3295/fix.diff', 'findings/poc/DF-3295/fix_build.log (kernel build/install + KLD rebuild on patched kernel)', "VERDICT.md fix-validation transcript: 'scanreq -> -1 (errno 22)' x11 + 'valid scanreq -> 0' + 'guest still up'"]
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 10 07:24:45 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

wifi-cap holder (SYSCAP_NONET_WIFI, e.g. delegated wpa_supplicant operator): (1) create/bring up STA vap, (2) MLME DISASSOC to park vap in S_INIT while UP, (3) SCAN_REQ with sr_ssid[0].len=-1 -> (size_t)-1 memcpy from attacker-controlled kmalloc'd scan_req into vap->iv_scanreq_ssid[0].ssid, (4) linear overwrite of the ieee80211vap tail (function pointers) and adjacent heap with ~128 attacker-controlled bytes followed by heap contents, (5) heap-groom a victim object behind the vap, overwrite a pointer, pivot on a vap callback (iv_newstate/iv_reset) -> kernel code exec -> full root. Demonstrated stop: deterministic kernel panic (write fault) at step 3-4 on the stock guest. No unprivileged entry path exists (SIOCS80211 privilege gate), so no uid0-by-unprivileged chain is possible for this bug.

Evidence (decisive lines)

["findings/poc/DF-3295/run.log.baseline: 'firing SCAN_REQ: sr_nssid=1 sr_ssid[0].len=-1' followed by guest death mid-loop", "findings/poc/DF-3295/panic.txt: 'Fatal trap 12: page fault while in kernel mode', 'fault virtual address = 0xfffff80118830000', 'supervisor write data', 'current process = 1229', 'Stopped at memcpy+0xfa: repe movsq'", "findings/poc/DF-3295/VERDICT.md: full narrative + fix-validation transcript (patched kernel: 'scanreq -> -1 (errno 22)' x11, guest up, valid request rc=0)"]

PoC changes

fw.c/Makefile reused verbatim from the DF-3282 software-radio harness. scanreq.c written new: MLME DISASSOC ioctl (park STA vap in S_INIT while UP) then SCAN_REQ with sr_ssid[0].len=-1 in a 10x retry loop (first iteration typically hits the non-INIT branch and returns EINPROGRESS=36). One compile fix vs first draft: added .

Verified recommended fix

In ieee80211_scanreq() reject negative values before use: 'if (sr->sr_nssid < 0) return EINVAL;' and 'if (sr->sr_ssid[i].len < 0 || sr->sr_ssid[i].len > IEEE80211_NWID_LEN) return EINVAL;'

Verdict

Reproduced on the stock INVARIANTS guest: SIOCS80211 IEEE80211_IOC_SCAN_REQ with sr_nssid=1 and sr_ssid[0].len=-1 (signed int passes the 'len > IEEE80211_NWID_LEN' check at ieee80211_ioctl.c:2496) reaches memcpy(vap->iv_scanreq_ssid[0].ssid, sr->sr_ssid[0].ssid, (size_t)-1) in the S_INIT stash branch at ieee80211_ioctl.c:2538-2540 -> Fatal trap 12, supervisor write, Stopped at memcpy+0xfa repe movsq, guest down. Preconditions arranged by the PoC: STA vap UP (ic_nrunning!=0) and pushed back to S_INIT via MLME DISASSOC (ieee80211_ioctl.c:1396-1400). Primitive: unbounded linear kernel-heap copy; destination starts inside the ieee80211vap (overwrites the vap tail incl. function pointers iv_newstate/iv_reset/iv_key_*), source is the 128-byte fully user-controlled copyin'd scan_req then adjacent heap. Entry gated by SYSCAP_NONET_WIFI (ieee80211_ioctl.c:3472) so no unprivileged path exists; ceiling is delegated wifi-cap -> heap-groomed controlled-pointer corruption -> kernel compromise, demonstrated floor is a deterministic privileged panic. Distinct from DF-0642 (signed sr_nssid -> scan.c copy_ssid sink, non-INIT branch); this is the signed sr_ssid[].len -> ioctl.c memcpy sink, INIT branch. fix.diff (reject len<0 and nssid<0 with EINVAL) validated on an in-guest rebuilt kernel: PoC returns EINVAL on every attempt, guest stays up, valid scan request still accepted (rc=0).