rtl8723bs — WiFi Heap Overflow in the Linux Kernel
A heap overflow in the Linux staging driver for the rtl8723bs WiFi chip, exploitable by any rogue AP within radio range. No authentication required. The bug has been sitting in the kernel since 2017.
1. Background
The rtl8723bs driver handles the Realtek RTL8723BS SDIO WiFi/Bluetooth combo chip, which ships in a wide range of embedded Linux devices — tablets, single-board computers, industrial controllers. It was added to the kernel's staging tree in 2017 (commit 554c0a3abf21) and has been there ever since.
The staging tree (drivers/staging/) is where the kernel accepts drivers that do not yet meet mainline quality standards — they may be incomplete, undertested, or carry known technical debt. The expectation is that they will be cleaned up over time toward mainline acceptance. Because staging code is less thoroughly reviewed than upstream drivers, years of accumulated technical debt can quietly hide security-relevant bugs that a more rigorous review would have caught early.
The bugs in this post all follow a single root cause. WiFi management frames — Beacon, Authentication, Association, Probe Response, and others — carry Information Elements (IEs): TLV-encoded fields that advertise the network's capabilities (supported data rates, HT/VHT extensions, WPS, WMM parameters, and so on). Each IE has a 1-byte element ID, a 1-byte length, and a variable-length payload. When a driver iterates over IEs in a received frame it must verify two things at every step: first, that the two-byte IE header (ID + length) fits within the remaining buffer; second, that the declared payload length does not extend past the buffer end. Skipping either check is an out-of-bounds read waiting to happen on any over-the-air frame an attacker controls.
One constant appears throughout the affected code: WLAN_HDR_A3_LEN = 24. This is the size of the standard 802.11 three-address management frame header: 2 bytes Frame Control + 2 bytes Duration + 6 bytes Addr1 (destination/DA) + 6 bytes Addr2 (source/SA) + 6 bytes Addr3 (BSSID) + 2 bytes Sequence Control = 24 bytes total. The IE payload in any management frame starts immediately after this header (plus any fixed fields specific to that frame type, such as the authentication algorithm and sequence number in an Auth frame).
I found the primary bug — a heap overflow in OnAuthClient() — while reading through the driver's 802.11 management frame handling code. From there, a reviewer pointed out that the same function was also missing basic frame length guards, which led to a second patch. The same IE parsing pattern repeated across the driver, so I kept going. Three initial patch series and two additional standalone patches. The first two series cover the OnAuthClient() overflow, the client-side and AP-side frame length checks, and the IE handler OOBs in the association path. The third adds fixes for five more functions: update_beacon_info(), bwmode_update_check(), issue_assocreq(), join_cmd_hdl(), rtw_get_wps_ie(), and a separate one-byte heap overflow in rtw_cfg80211_set_wpa_ie() reachable via nl80211. The additional patches extend coverage to is_ap_in_tkip() and three functions in rtw_ieee80211.c — rtw_get_sec_ie(), rtw_get_wapi_ie(), and rtw_get_wps_attr().
2. OnAuthClient heap overflow
The function OnAuthClient() in core/rtw_mlme_ext.c processes 802.11 Authentication frames received by the station. When the AP sends an Auth sequence 2 frame using Shared Key authentication, the driver looks for a Challenge Text IE (element ID 0x10) and copies its payload into a fixed 128-byte buffer:
/* rtw_mlme_ext.c ~line 891 */
p = rtw_get_ie(pframe + WLAN_HDR_A3_LEN + _AUTH_IE_OFFSET_,
WLAN_EID_CHALLENGE, (int *)&len,
pkt_len - WLAN_HDR_A3_LEN - _AUTH_IE_OFFSET_);
if (!p)
goto authclnt_fail;
memcpy(pmlmeinfo->chg_txt, p + 2, len); /* len = raw IE length byte, 0-255 */
rtw_get_ie() validates that the IE fits within the received frame buffer — so the copy source is legitimate. What it does not check is whether len fits in the 128-byte destination chg_txt[]. The raw IE length field is a single byte and can be anywhere from 0 to 255. If an AP sends a Challenge Text IE with length 200, the driver writes 72 bytes past the end of chg_txt.
IEEE 802.11-2020 §12.3.3 is unambiguous: the Challenge Text element carries exactly 128 bytes of challenge data. The fix is a one-line addition — reject the frame if the IE length is anything other than sizeof(pmlmeinfo->chg_txt):
-if (!p)
+if (!p || len != sizeof(pmlmeinfo->chg_txt))
The overflow target is struct mlme_ext_info, with chg_txt at offset 40. Immediately following it are aid, bcn_interval, capability, and a handful of protocol state flags. With a crafted length of 200, 72 bytes past the end of chg_txt are overwritten.
3. Auth algorithm toggle — any device is affected
The overflow is in the Shared Key path, which at first looks like it only affects devices explicitly configured for WEP Shared Key authentication. It does not. There's a toggle in the same function:
if (status == WLAN_STATUS_NOT_SUPPORTED_AUTH_ALG) {
pmlmeinfo->auth_algo ^= 1; /* toggle Open ↔ Shared */
pmlmeinfo->reauth_count = 0;
set_link_timer(pmlmeext, 1);
return _SUCCESS;
}
The attack sequence is two frames. First, send an Auth Response (seq=2, status=13 — WLAN_STATUS_NOT_SUPPORTED_AUTH_ALG). The driver toggles from Open System to Shared Key and sends a new Auth Request. Then immediately send a second Auth Response (seq=2, status=0) with a crafted Challenge Text IE. The driver is now in the Shared Key path and executes the overflow unconditionally.
This works against any device running this driver that is attempting to connect to a network, regardless of how the device is configured. The attacker just needs to be within radio range.
4. Missing frame length checks — OnAuthClient
After I submitted the first patch, Dan Carpenter pointed out that OnAuthClient() was also missing basic frame length validation. Two problems:
First, get_da(pframe) inspects the ToDs/FrDs bits in Frame Control (bytes 0–1) and returns either Addr1 (bytes 4–9) or Addr3 (bytes 16–21). GetPrivacy(pframe) also reads the Frame Control field. Both are called before any check that pkt_len is at least WLAN_HDR_A3_LEN (24 bytes), so a short frame causes an out-of-bounds read.
Second, the seq and status fields are read at pframe + WLAN_HDR_A3_LEN + offset + 2 and +4 with no check that those offsets are within the frame. The fix adds two guards: one before the first pframe access, and a second after computing offset (0 for open-system, 4 for WEP) to cover the seq/status reads:
+if (pkt_len < WLAN_HDR_A3_LEN)
+ goto authclnt_fail;
/* check A1 matches or not */
if (memcmp(myid(&(padapter->eeprompriv)), get_da(pframe), ETH_ALEN))
return _SUCCESS;
...
offset = (GetPrivacy(pframe)) ? 4 : 0;
+if (pkt_len < WLAN_HDR_A3_LEN + offset + 6)
+ goto authclnt_fail;
5. Missing frame length checks — OnAuth (AP mode)
The same class of missing frame length guards turned up on the AP side. OnAuth() handles 802.11 Authentication frames received by the driver when it is operating as a soft-AP. Three OOB reads:
The first operation after the AP-state guard is GetAddr2Ptr(pframe), which reads 6 bytes at offset 10–15 (the SA field). This happens with no prior check that the frame is at least WLAN_HDR_A3_LEN (24) bytes long. A station could send a truncated auth frame and cause an OOB read before the driver has done anything else with the packet.
When the Privacy bit is set in Frame Control, the code sets iv = pframe + WLAN_HDR_A3_LEN and immediately reads iv[3] to extract the key index. There is no check that the frame is long enough to contain four bytes after the header. A frame that is between 24 and 27 bytes long passes the first guard but triggers an OOB read here.
After computing offset (0 for open-system, 4 for WEP), the code reads algorithm at pframe + WLAN_HDR_A3_LEN + offset and seq at +offset + 2. No check confirms that those reads are within the frame.
The fixes use return _FAIL for the first two guards rather than goto auth_fail, because the auth_fail block calls memcpy(pstat->hwaddr, sa, ETH_ALEN) to build a rejection frame — but sa has not been set at those points. Jumping there would copy stack garbage into the destination address. The third guard, after sa is valid, sets a defined status code and uses goto auth_fail:
+if (len < WLAN_HDR_A3_LEN)
+ return _FAIL;
sa = GetAddr2Ptr(pframe);
...
+if (len < WLAN_HDR_A3_LEN + 4) /* inside GetPrivacy() branch */
+ return _FAIL;
iv = pframe + prxattrib->hdrlen;
prxattrib->key_index = ((iv[3] >> 6) & 0x3);
...
+if (len < WLAN_HDR_A3_LEN + offset + 4) {
+ status = WLAN_STATUS_UNSPECIFIED_FAILURE;
+ goto auth_fail;
+}
algorithm = le16_to_cpu(*(__le16 *)((SIZE_PTR)pframe + WLAN_HDR_A3_LEN + offset));
seq = le16_to_cpu(*(__le16 *)((SIZE_PTR)pframe + WLAN_HDR_A3_LEN + offset + 2));
6. HT_caps_handler OOB write
While in the same file, I found a similar issue in HT_caps_handler() in core/rtw_wlan_util.c. This function processes the HT Capabilities IE from an Association Response. It iterates pIE->length bytes and writes them into HT_caps.u.HT_cap[], which is a fixed 26-byte array:
for (i = 0; i < (pIE->length); i++) {
HT_caps.u.HT_cap[i] = pIE->data[i];
...
}
Since pIE->length is a raw byte from the received frame (0–255), a malicious AP can cause up to 229 bytes of out-of-bounds writes into adjacent fields of struct mlme_ext_info. The fix truncates the iteration count with umin() instead of rejecting the IE outright — APs that send an oversized HT Caps element are handled gracefully rather than causing a connection failure:
-for (i = 0; i < (pIE->length); i++) {
+for (i = 0; i < umin(pIE->length,
+ sizeof(pmlmeinfo->HT_caps.u.HT_cap)); i++) {
7. OnAssocRsp IE loop OOB read
The IE parsing loop in OnAssocRsp() iterates through all information elements in the Association Response frame. The loop condition is i < pkt_len, and at the top of each iteration it casts pframe + i to struct ndis_80211_var_ie * — which has a two-byte header (element ID + length). If the last IE in the frame starts at pkt_len - 1, reading pIE->length touches pframe[pkt_len], one byte past the allocation. Even when the header bytes are in bounds, the IE's declared pIE->length can extend past pkt_len, passing a truncated IE to the handler functions.
The fix adds two guards at the top of the loop body:
for (i = (6 + WLAN_HDR_A3_LEN); i < pkt_len;) {
+ if (i + sizeof(*pIE) > pkt_len)
+ break;
pIE = (struct ndis_80211_var_ie *)(pframe + i);
+ if (i + sizeof(*pIE) + pIE->length > pkt_len)
+ break;
switch (pIE->element_id) {
8. More IE loop OOB reads — beacon, join, and WPS paths
The same missing two-guard pattern turned up in several more places across the driver. The subsequent review rounds also surfaced additional bugs in the individual IE handlers.
update_beacon_info()
This function in core/rtw_wlan_util.c processes incoming Beacon frames. Three problems. First, the len computation — pkt_len - (_BEACON_IE_OFFSET_ + WLAN_HDR_A3_LEN) — uses an unsigned result with no prior check that pkt_len is large enough. A Beacon shorter than 36 bytes wraps len to a very large value and sends the loop far past the receive buffer. Second, the IE loop had the same missing two-guard problem as the others. Third, the WMM OUI comparison called memcmp(pIE->data, WMM_PARA_OUI, 6) before checking pIE->length == WLAN_WMM_LEN — an IE with fewer than 6 bytes of payload caused the memcmp to read into adjacent frame data. The conditions are now swapped so the length check comes first.
bwmode_update_check()
Called from the same beacon processing loop, this function rejects IEs that are longer than sizeof(struct HT_info_element) but accepts any shorter length, including zero. After the check it casts pIE->data to struct HT_info_element * and reads infos[0] at offset 1. A zero-length IE passes the guard and causes an OOB read. The guard was changed from > to != to require an exact match on the expected IE length.
issue_assocreq() and join_cmd_hdl()
Both functions in core/rtw_mlme_ext.c walk stored network IE data filled earlier from the AP's Beacon and Probe Response frames. The loop bounds problems were the same as the others. The vendor-specific OUI comparisons in issue_assocreq() called memcmp on a 4-byte OUI without checking that pIE->length is at least 4. In the WPS truncation path, when wifi_spec is 0, the code sets vs_ie_length = 14 and passes pIE->data to rtw_set_ie() regardless of the actual IE length — an IE between 4 and 13 bytes triggers an OOB read of up to 10 bytes; the fix skips the IE when pIE->length < 14. The HT Capability IE handler copied sizeof(struct HT_caps_element) bytes from the IE payload without first checking the IE was that long, then passed the raw pIE->length to rtw_set_ie() — if the IE was longer than the struct, that caused an OOB read from pmlmeinfo->HT_caps. In join_cmd_hdl(), the WMM check used pIE->length >= 4 before calling WMM_param_handler(), but the handler reads pIE->data + 6 and copies sizeof(struct WMM_para_element) (18 bytes) for a total of 24 bytes minimum read — the guard was strengthened to pIE->length >= WLAN_WMM_LEN. The HT Operation IE handler cast pIE->data to struct HT_info_element * and read infos[0] at offset 1 with no minimum length check.
rtw_get_wps_ie()
This function in core/rtw_ieee80211.c searches IE data for a WPS IE. The loop only checked cnt < in_len before reading in_ie[cnt + 1] — if the buffer ends on an element_id byte with no length byte following, this reads one byte past the end. There was also no check that the declared payload fits within in_len, and the 4-byte WPS OUI comparison ran without confirming the IE payload was at least 4 bytes long.
9. is_ap_in_tkip(), rtw_get_sec_ie(), rtw_get_wapi_ie(), rtw_get_wps_attr()
is_ap_in_tkip()
This function in core/rtw_wlan_util.c checks whether the associated AP is using TKIP as its pairwise cipher by walking the stored network IE buffer. The loop lacked both the header and payload bounds checks entirely. The vendor-specific path additionally reads pIE->data + 12 — four bytes of cipher suite data — without verifying pIE->length >= 16, and the RSN path reads pIE->data + 8 without verifying pIE->length >= 12. Both memcmp calls extend into adjacent IE data when the payload is shorter than expected. The fix adds the standard two-guard header check and explicit minimum-length guards before each OUI comparison:
+ if (i + sizeof(*pIE) > pmlmeinfo->network.ie_length)
+ break;
+ if (i + sizeof(*pIE) + pIE->length > pmlmeinfo->network.ie_length)
+ break;
switch (pIE->element_id) {
case WLAN_EID_VENDOR_SPECIFIC:
- if ((!memcmp(pIE->data, RTW_WPA_OUI, 4)) && (!memcmp((pIE->data + 12), WPA_TKIP_CIPHER, 4)))
+ if (pIE->length >= 16 &&
+ !memcmp(pIE->data, RTW_WPA_OUI, 4) &&
+ !memcmp((pIE->data + 12), WPA_TKIP_CIPHER, 4))
return true;
case WLAN_EID_RSN:
- if (!memcmp((pIE->data + 8), RSN_TKIP_CIPHER, 4))
+ if (pIE->length >= 12 &&
+ !memcmp((pIE->data + 8), RSN_TKIP_CIPHER, 4))
return true;
rtw_get_sec_ie() and rtw_get_wapi_ie()
Both functions in core/rtw_ieee80211.c search a raw IE buffer for the WPA and WAPI IEs respectively. Both iterate with while (cnt < in_len) and immediately read in_ie[cnt + 1] (the length byte) without first checking that cnt + 2 <= in_len. A buffer ending on an element_id byte with no length byte following causes a one-byte OOB read. Additionally, rtw_get_sec_ie() compares a 4-byte WPA OUI at cnt+2 without checking in_ie[cnt+1] >= 4, and rtw_get_wapi_ie() compares a 4-byte WAPI OUI at cnt+6 without checking in_ie[cnt+1] >= 8. The fix adds a two-part bounds check at the top of each loop body and minimum-length guards before each OUI comparison.
rtw_get_wps_attr()
This function in the same file searches a WPS IE for a specific attribute. At entry it reads wps_ie[0] (element ID) and the 4-byte OUI at wps_ie+2 before verifying that wps_ielen is large enough to hold the 6-byte WPS IE header. An input shorter than 6 bytes causes an OOB read before any meaningful work happens. Inside the attribute loop, get_unaligned_be16() is called on attr_ptr and attr_ptr+2 with only attr_ptr - wps_ie < wps_ielen as the guard — 1, 2, or 3 bytes remaining still pass the check but the 16-bit reads go out of bounds. The fix adds an early return when wps_ielen < 6 and a 4-byte remaining check inside the loop:
+ if (wps_ielen < 6)
+ return attr_ptr;
if ((wps_ie[0] != WLAN_EID_VENDOR_SPECIFIC) ||
...
while (attr_ptr - wps_ie < wps_ielen) {
+ if (attr_ptr + 4 > wps_ie + wps_ielen)
+ break;
u16 attr_id = get_unaligned_be16(attr_ptr);
10. rtw_cfg80211_set_wpa_ie() — WPA IE one-byte overflow (local)
This one is different from the others: it is a local attack, not over-the-air. rtw_cfg80211_set_wpa_ie() in os_dep/ioctl_cfg80211.c handles WPA and WPA2 IE data supplied via NL80211_CMD_CONNECT. It copies the IE into supplicant_ie, a 256-byte array in struct security_priv:
memcpy(padapter->securitypriv.supplicant_ie, &pwpa[0], wpa_ielen + 2);
wpa_ielen is the raw IE length field — a u8, so it ranges from 0 to 255. When a local user issues a connect with a crafted WPA IE of length 255, wpa_ielen + 2 = 257 and the memcpy writes one byte past the end of the 256-byte buffer into the adjacent last_mic_err_time field.
The existing length check in rtw_parse_wpa_ie() does not prevent this. It compares *(wpa_ie+1) against (u8)(wpa_ie_len - 2). With wpa_ie_len = 257, the cast produces (u8)(255) = 255, which matches — so the check passes silently.
The fix adds an explicit bounds check before both the WPA and WPA2 copy paths:
+if (wpa_ielen + 2 > sizeof(padapter->securitypriv.supplicant_ie)) {
+ ret = -EINVAL;
+ goto exit;
+}
11. Patches and stable backports
Three series went to the staging mailing list. Series 1 (v8) covered the OnAuthClient() heap overflow, the client-side frame length checks, and the AP-side frame length guards in OnAuth() — by v8, the earlier patches had already landed in Greg's tree and only the OnAuth() fix remained. Series 2 (v5) fixed the HT_caps_handler() OOB write, the OnAssocRsp() IE loop OOB read, a truncated frame guard before the fixed-field reads, and the WMM length guard. Series 3 (v5) fixed the remaining IE parsing issues in update_beacon_info(), bwmode_update_check(), issue_assocreq(), join_cmd_hdl(), and rtw_get_wps_ie(), plus the one-byte WPA IE overflow in rtw_cfg80211_set_wpa_ie(), with Reviewed-by from Luka Gejak. Two additional patches closed the remaining gaps: is_ap_in_tkip() missed the two-guard pattern and carried unguarded multi-byte OUI comparisons, and three functions in core/rtw_ieee80211.c — rtw_get_sec_ie(), rtw_get_wapi_ie(), and rtw_get_wps_attr() — had the same missing header bounds checks plus an OOB read before the WPS IE entry validation.
All eight patches landed in Greg's staging-linus branch on 2026-07-07. Greg Kroah-Hartman backported the series to six stable trees simultaneously on 2026-07-13. All eight patches applied cleanly to 7.1, 6.18, 6.12, 6.6, 6.1, and 5.15. Cherry-picks to 5.10 produced conflicts on most of the series during the initial pass; three patches that backported cleanly — the OnAuth() WEP length underflow fix (a1fc19d61f66), the rtw_cfg80211_set_wpa_ie() heap overflow fix (5a752a616e75), and the IE loop guards in issue_assocreq() and join_cmd_hdl() (ef61d628dfad) — were subsequently queued for the 5.10 stable tree, confirmed in email from Sasha Levin on 2026-07-14.
All changes are in drivers/staging/rtl8723bs/:
core/rtw_mlme_ext.c—OnAuthClient(): Challenge Text length check, frame length guardscore/rtw_mlme_ext.c—OnAuth(): three frame length guards (header, IV, algorithm/seq)core/rtw_wlan_util.c—HT_caps_handler(): OOB write fix, LDPC/STBC macro OOB read fixcore/rtw_mlme_ext.c—OnAssocRsp(): IE loop bounds checks, truncated frame guard, WMM length guardcore/rtw_wlan_util.c—update_beacon_info(): pkt_len underflow guard, IE loop bounds checks, WMM condition orderingcore/rtw_wlan_util.c—bwmode_update_check(): minimum IE length guardcore/rtw_mlme_ext.c—issue_assocreq(): OUI length guard, WPS truncation guard, HT caps length checkcore/rtw_mlme_ext.c—join_cmd_hdl(): WMM length guard, HT info length checkcore/rtw_ieee80211.c—rtw_get_wps_ie(): header bounds, payload bounds, OUI length guardcore/rtw_wlan_util.c—is_ap_in_tkip(): IE loop bounds checks, vendor-specific and RSN minimum-length guardscore/rtw_ieee80211.c—rtw_get_sec_ie(),rtw_get_wapi_ie(): header bounds check, OUI minimum-length guardscore/rtw_ieee80211.c—rtw_get_wps_attr(): entry length guard, inner-loop 4-byte remaining checkos_dep/ioctl_cfg80211.c—rtw_cfg80211_set_wpa_ie(): WPA/WPA2 IE size check
Upstream commits (staging-linus, 2026-07-07) and assigned CVEs:
f8001e1a516b—HT_caps_handler()OOB write — CVE-2026-644401463ca3ec660—rtw_get_sec_ie(),rtw_get_wapi_ie(),rtw_get_wps_attr()— CVE-2026-64441ef61d628dfad—issue_assocreq()/join_cmd_hdl()IE loops — CVE-2026-64442ed51de4a86e1—update_beacon_info()IE loop — CVE-2026-64443f9654207e922—OnAssocRsp()IE loop — CVE-2026-64444a1fc19d61f66—OnAuth()WEP length underflow — CVE-2026-644455a752a616e75—rtw_cfg80211_set_wpa_ie()heap overflow — CVE-2026-644463bf39f711ff2—is_ap_in_tkip()IE loop — CVE-2026-64536