An adjacent attacker can overflow iwd's stack with a spoofed 802.11k request
once 17 BSS entries are cached. Hardened builds abort; a representative
unhardened ARM32 layout permits saved-LR control. No network credentials
are required on WPA2 without PMF.
One maintainer reviewed and confirmed the findings and patches, verified
them against the spec, and applied them locally - but they remain unmerged.
Affected: all four coexist in 1.30 through 3.12 (latest release).
They arrived separately, so older versions have a subset.
Repository: https://git.kernel.org/pub/scm/network/wireless/iwd.git
Write-up:
https://abhinavagarwal07.github.io/posts/iwd-rrm-stack-overflow/?src=oss
PoC: https://github.com/abhinavagarwal07/iwd-security-poc
1. RRM beacon report stack buffer overflow
CWE-121. CVSS 3.1: 8.8 High
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
src/rrm.c:334 and :370, rrm_report_beacon_results()
Introduced 1.1, commit 1f01819c701e
rrm_report_beacon_results() builds a Radio Measurement Report action
frame in uint8_t frame[512] on the stack. The loop appends one 31-byte
record per matching BSS - a 5-byte Measurement Report IE header plus a
26-byte packed rrm_beacon_report - and advances the write pointer with
"ptr += report_len + 5" and no bounds check. After the 3-byte action
header, 16 records fit. Record 17 writes frame[499..529]. The BSS list
is the station's own scan cache, so the count is a property of the RF
environment - but an attacker can deterministically reach the threshold
by transmitting additional fake beacons.
rrm_frame_watch_cb() requires STATION_STATE_CONNECTED (rrm.c:727), then
does a bare memcmp of the frame's address_2 against the connected AP's
MAC (rrm.c:733). There is no PMF requirement, so on WPA2 without
802.11w that source address is whatever the attacker puts in it. One
request clears all three per-BSS filters: a wildcard BSSID
(ff:ff:ff:ff:ff:ff) skips the BSSID compare, omitting the SSID
sub-element skips the SSID compare, and mode=TABLE permits channel=0,
which makes bss_in_request_range() return true unconditionally. Every
cached BSS is then reported.
Request fields and attacker-generated beacon metadata make parts of each
overflowing record attacker-controlled.
Outcomes are build-dependent:
* Hardened distro builds abort in __stack_chk_fail.
* In the tested no-canary x86-64 build, the zeroed scan_start_time
field overwrote saved RIP, producing a crash without control.
* In a representative unhardened ARM32 layout, the saved lr is
reachable and holds attacker bytes. I demonstrated pc hijack on
armhf in a harness reproducing that frame layout (exit code 42 from
win()). That shows the primitive reaches the return address on the
ABI; it is not an exploit against a shipped binary, since the record
and field that land on lr depend on the build's register allocation.
FORTIFY_SOURCE does not help. Each individual memcpy is in bounds; the
overflow comes from the loop accumulating them.
Evidence - AddressSanitizer on a real iwd 3.12 daemon, driven over
mac80211_hwsim with 21 cached BSSes and the action frame sent via
NL80211_CMD_FRAME:
==67193==ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 26 at 0x7ffe8136a2a0 thread T0
#3 in rrm_report_beacon_results src/rrm.c:367
[384, 896) 'frame' (line 334) <== Memory access at offset 896
overflows this variable
2. HE Capabilities validator reads the wrong byte
CWE-125. CVSS 3.1: 4.3 Medium
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
src/ie.c:2714 vs src/band.c:631
Introduced 1.30, commit 53988a728533
The Channel Width Set is bits B1-B7 of PHY Capabilities byte 0, element
body offset 6 (802.11ax-2021 s9.4.2.248.3). The validator
ie_validate_he_capabilities() reads offset 7 while
band_estimate_he_rx_rate() and wiphy.c read offset 6 from the same
pointer, so its length guards (len < 26 for the 160 MHz MCS map, len <
30 for 80+80) are keyed on the wrong byte. A
22-byte body with hec[6]=0x1C and hec[7]=0x00 passes validation, then
drives the estimator into both branches, reading hec[23..24] and
hec[27..28]. The local adapter must also advertise the width, since the
two sets are ANDed.
Passive scanning reaches this path before association. In a real beacon
the OOB reads consume attacker-controlled bytes from the following
element, influencing the rate used for BSS ranking. Only the HE
Capabilities element must be malformed.
The fix is one character: (ptr + 7) -> (ptr + 6).
3. mde_equal() compares its first argument to itself
CWE-697. CVSS 3.1: 3.1 Low
CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N
src/ft.c:385
Introduced 1.14, commit 2c0234e1
return memcmp(mde1, mde1, mde1[1] + 2) == 0;
mde2 is never read, so the check always passes. Its only caller is
ft_parse_ies() (ft.c:667), where it should confirm the target AP echoed
the Mobility Domain element the station sent on the 802.11r FT
Authentication Response and FT-over-DS paths. Not an authentication
bypass - __ft_rx_associate() compares correctly at ft.c:496 and the FTE
MIC there is keyed on PMK-R1 - but a response carrying an arbitrary
mobility domain ID and FT capability flags passes the authentication
stage into FTE parsing and key derivation. Finding 4 sits on that path,
one call later.
4. FTE sub-element length underflows a uint8_t
CWE-191 -> CWE-125. CVSS 3.1: 3.1 Low
CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L
src/ie.c:1919, ie_parse_fast_bss_transition()
Introduced 0.1, commit e4c168cc
The sub-element loop never checks the declared length against the bytes
remaining. The case handlers copy on the declared length alone, so a
6-byte R1KH-ID copy runs with zero bytes left, and "len -= subelem_len +
2" on a uint8_t wraps rather than going negative (2 - 8 == 250), so the
loop keeps walking past the element. The declared-length checks bound
every write into the fixed-size ie_ft_info fields, so there is no
out-of-bounds write; the reads are unbounded. The scored impact is the
read running off the end, not disclosure: ft_parse_fte() then requires
the parsed r0khid to equal the station's own (ft.c:361), so corrupted
values are discarded rather than echoed back, and I have no extraction
channel.
ft_parse_fte() parses before it validates (ft.c:353 parse, ft.c:357
checks), on the FT Authentication Response path where the FTE carries no
MIC at all - the code requires mic_element_count == 0. Nothing
cryptographic sits between a rogue AP's response and the parser. The
reassociation path has the same ordering (parse ft.c:513, MIC ft.c:524).
Reaching it needs the station to attempt an FT roam to a BSS the attacker
controls. Finding 3 removes the MDID check on that same path.
Who this affects:
Hardened desktop packages are expected to abort through the stack canary.
Embedded builds without stack protection may expose control-flow
corruption; on Yocto and Buildroot that is a per-image configuration
choice.
SteamOS uses iwd by default - steamos-customizations-git ships
/usr/lib/NetworkManager/conf.d/10-steamos-defaults.conf with
"wifi.backend=iwd" - and currently ships iwd 3.9-1.2, which contains all
four defects. Not tested but appears to be affected. PSIRT was notified.
Mitigation:
The PoC repository contains four independently applicable patches. See
patches/README.md for backport notes.
Coordination timeline:
2026-05-12 Reported to Intel via Intigriti
2026-05-19 Reported to the iwd maintainers
2026-05-21 Patches sent to iwd maintainers
2026-05-22 Intigriti closed the submission as out of scope
2026-05-28 Maintainers confirmed all four findings
2026-06-29 Valve PSIRT notified (SteamOS ships iwd as default)
2026-07-21 Notified the maintainers of a 27 July publication date
2026-08-01 This advisory, write-up and PoC code
CVE assignment:
CVE requested with MITRE:
CAN-2026-2051869 finding 1, RRM beacon report stack buffer overflow
CAN-2026-2051870 finding 2, HE Capabilities validator byte offset
CAN-2026-2051871 finding 3, mde_equal() self-comparison
CAN-2026-2051872 finding 4, FTE sub-element uint8_t underflow
Credits: Abhinav Agarwal