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)
PoC verification
Evidence pack
findings/poc/DF-3295 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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].ssidforward across the whole tail of theieee80211vap(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:
SIOCS80211requiresSYSCAP_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 throughiv_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).
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].lenisint, 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)withlen = -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
sh findings/poc/DF-3295/build.shβ builds DF-3282's software radio KLD (fw.ko: registersfw0, STA/hostap/ibss capable, nop scan callbacks) +scanreqtrigger on the guest (stock INVARIANTS kernel#0, DragonFly 6.5-DEVELOPMENT).ifconfig wlan create wlandev fw0 wlanmode sta upβ vap UP; state machine reaches S_SCAN (status: no carrier)../scanreq wlan0: *ioctl(SIOCS80211, MLME DISASSOC)β 0 β STA-mode DISASSOC doesieee80211_new_state(vap, IEEE80211_S_INIT, reason)(ieee80211_ioctl.c:1396-1400) while the interface stays UP, soic_nrunningstays 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.- 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-allocatedieee80211vap; the copy walks the entire vap tail (iv_scanreq_ssid[1..2], and everything after: including function pointersiv_newstate,iv_reset,iv_key_alloc,iv_key_set, β¦) and then adjacent kernel heap. - Source: the 128-byte
kmalloc'dieee80211_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 wlan0fine). - 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
fixedfix.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'"]
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).
No comments yet.