driver core: uevent: netns teardown frees uevent_sock while a tagged uevent is still being broadcast (UAF)

From: 徐晗翔

Date: Tue Nov 10 2026 - 09:20:18 EST


net->uevent_sock is published as a bare pointer by uevent_net_init()
and freed by uevent_net_exit(), but the tagged broadcast path
dereferences net->uevent_sock->sk without holding uevent_sock_mutex or
taking a socket reference. uevent_net_exit() drops the mutex before
releasing the socket and never clears net->uevent_sock, so a kobject
whose namespace callback returns an unpinned net pointer can emit a
tagged uevent after the per-net exit has freed the socket; KASAN
reports a slab-use-after-free read in kobject_uevent_net_broadcast()
(inlined into kobject_uevent_env in the report below).

An IB compat device supplies a concrete path. Its rdma_net is a raw
possible_net_t, and its asynchronous unregister can still be inside
device_del() after the net namespace has run uevent_net_exit(). This
was reported by syzbot (KASAN: slab-use-after-free Read in
disable_device, extid 4393dfdddf166f2de2b0,
https://syzkaller.appspot.com/bug?extid=4393dfdddf166f2de2b0) and I
reproduced it locally on 7.3 with a KASAN kernel; the same reproducer
is clean with a no-rxe control and with the v4 locking change described
below.

The detailed description:

* Test environment / affected version

Linux 7.3.0 (vanilla mainline tarball), x86_64, QEMU/KVM guest,
8 vCPU / 4 GB RAM, KASAN generic. The tested
lib/kobject_uevent.c is byte-identical to the audited 7.3 tree
(sha256 bbcf120ea86f42c74dec10e255a8cd0b3034c9f34cd5bb911fd94015729dd076).
Kernel config included CONFIG_NET_NS=y, CONFIG_INFINIBAND=y,
CONFIG_INFINIBAND_USER_ACCESS=y, CONFIG_RDMA_RXE=y, CONFIG_DUMMY=y
and CONFIG_KASAN=y. The kernel source was not modified to inject
the race; only the IB/RXE configuration was enabled and a userspace
reproducer was added.

* Mechanism

Three sides do not share a lifetime protocol:

- Publish side: uevent_net_init() allocates ue_sk and stores it
with net->uevent_sock = ue_sk (lib/kobject_uevent.c:773-803).
The field is a plain pointer in struct net, with no refcount,
RCU head, or socket reference (include/net/net_namespace.h:113).

- Free side: uevent_net_exit() reads the pointer, removes ue_sk
from the global uevent_sock_list under uevent_sock_mutex only
when it belongs to the initial user namespace, then unlocks and
calls netlink_kernel_release(ue_sk->sk) and kfree(ue_sk)
outside the mutex (lib/kobject_uevent.c:805-817). It never
writes net->uevent_sock = NULL.

- Read side: kobject_uevent_net_broadcast() obtains the target
net from the kobject namespace callback and passes
net->uevent_sock->sk directly to
uevent_net_broadcast_tagged() (lib/kobject_uevent.c:382-421;
the tagged dereference is at line 415). This path takes no
uevent_sock_mutex, no socket reference, and performs no NULL
check.

The IB trigger makes the window concrete:

- ib_class.namespace is net_namespace(), which only does
read_pnet(&coredev->rdma_net); rdma_init_coredev() stored the
net with write_pnet() without get_net()
(drivers/infiniband/core/device.c:536-550, 553-580).

- remove_one_compat_dev() erases the compat device from the
xarray under compat_devs_mutex, releases the mutex, and then
calls ib_free_port_attrs() and device_del()
(drivers/infiniband/core/device.c:1027-1039). device_del()
emits KOBJ_REMOVE at drivers/base/core.c:3983.

- device_del() has several sleeping or blocking points between
the xa_erase() and KOBJ_REMOVE (device_lock(), bus_notify(),
sysfs removal, class callbacks, devres_release_all()), so the
window is not merely theoretical.

On netns cleanup, rdma_dev_net_ops is registered with
register_pernet_device() (drivers/infiniband/core/device.c:3274),
while uevent_net_ops is registered with register_pernet_subsys()
(lib/kobject_uevent.c:826). Because the pernet list is walked in
reverse on exit, rdma_dev_exit_net() runs before
uevent_net_exit(). If the async IB path wins the xa_erase() in
remove_one_compat_dev(), rdma_dev_exit_net() sees an empty xarray
and proceeds to uevent_net_exit(), which frees ue_sk. The IB
worker can then continue through device_del() and read the freed
ue_sk through net->uevent_sock.

cleanup_net() calls ops_undo_list() (where uevent_net_exit() frees
ue_sk) and only later rcu_barrier()/net_complete_free()
(net/core/net_namespace.c:220-258, 666-729). struct net itself is
deferred, so KASAN reports the freed kmalloc-32 ue_sk, not net.


* Reproducer (attached: uevent_uaf_repro.c) and results on 7.3

The attached file is based on the syzbot C reproducer and adds a
no-rxe control mode plus a duration argument. It uses only
userspace netlink/ioctl calls; no kernel module or source change is
required.

# with dummy and rdma_rxe available, as root
gcc -O2 -pthread uevent_uaf_repro.c -o uevent_uaf_repro
./uevent_uaf_repro 60
./uevent_uaf_repro no-rxe 30 # negative control

Results (one QEMU/KVM guest, 8 vCPU):

- vulnerable 7.3, dummy + rxe, 60 s: 125 iterations, 4 KASAN
slab-use-after-free reports;
- vulnerable 7.3, dummy + netns churn without rxe, 30 s:
191 iterations, 0 KASAN reports;
- v4-patched kernel, same dummy + rxe trigger, 60 s: 0 reports.

The first positive report is:

BUG: KASAN: slab-use-after-free in kobject_uevent_env+0x64b/0x710
Read of size 8 ...
Workqueue: ib-unreg-wq ib_unregister_work

kobject_uevent_env
device_del
disable_device
__ib_unregister_device
ib_unregister_work

Allocated by: uevent_net_init
Freed by: ops_undo_list -> cleanup_net
cache: kmalloc-32

The race is probabilistic: an earlier 60 s run of the same reproducer
produced no report, and the successful run had kprobes installed on the path
(the probe overhead can widen the window). The v4 run used the same
kprobe configuration and stayed clean; the no-rxe control also
stayed clean. The syzbot report is independent of this local run.


* Suggested direction

Serialize the tagged broadcast with uevent_net_exit() using the
existing uevent_sock_mutex, and clear net->uevent_sock before
dropping the lock:

- in kobject_uevent_net_broadcast(), take uevent_sock_mutex,
read ue_sk = net->uevent_sock, and check ue_sk && ue_sk->sk
before calling uevent_net_broadcast_tagged();
- in uevent_net_exit(), take the same mutex, set
net->uevent_sock = NULL while removing ue_sk from the list, then
release the socket and free ue_sk outside the lock.


Please keep me in Cc; I am not subscribed to the lists.



Regards,
Hanxiang Xu
xuhx@xxxxxxxxxxx
// https://syzkaller.appspot.com/bug?id=62db7f0b628381f621ab352d501352f0cb359ba1
// Copyright 2026 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

// IMPORTANT: Do not copy the macros or definitions below directly into your reproducer.
// Instead, add the following line to your reproducer:
// #include "race_toolkit.h"

// --- Race Condition Toolkit ---
// Macros and snippets for CPU pinning, memory barriers, and userfaultfd.

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/futex.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <time.h>
#include <unistd.h>

// Unbuffered I/O: Ensure logs are written immediately.
#define SETUP_UNBUFFERED_IO() setvbuf(stdout, NULL, _IONBF, 0)

// CPU Pinning: Pin the current thread to a specific CPU core.
#define PIN_TO_CPU(cpu) \
do { \
cpu_set_t mask; \
CPU_ZERO(&mask); \
CPU_SET(cpu, &mask); \
if (sched_setaffinity(0, sizeof(mask), &mask) == -1) { \
perror("sched_setaffinity"); \
} \
} while (0)

// Memory Barrier: Ensure memory ordering.
#define MB() __atomic_thread_fence(__ATOMIC_SEQ_CST)

// Spin-wait Barrier: Wait until a memory location has a specific value.
// Best for tight race windows (low latency, no context switches).
#define WAIT_ON(addr, val) \
do { \
while (__atomic_load_n(addr, __ATOMIC_ACQUIRE) != (val)) \
; \
} while (0)

// Signal: Set a memory location to a specific value to release a WAIT_ON.
#define SIGNAL(addr, val) __atomic_store_n(addr, val, __ATOMIC_RELEASE)

// --- Timing Primitives ---
// Robust timing loops in VM environments (using CLOCK_MONOTONIC to avoid time(NULL) jumps).

static inline double timer_elapsed_sec(struct timespec* start)
{
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) {
perror("clock_gettime(CLOCK_MONOTONIC) elapsed");
exit(1);
}
return (double)(now.tv_sec - start->tv_sec) + (double)(now.tv_nsec - start->tv_nsec) / 1e9;
}

// Initialize a monotonic timer variable.
#define TIMER_START(t) \
struct timespec t; \
if (clock_gettime(CLOCK_MONOTONIC, &t) == -1) { \
perror("clock_gettime(CLOCK_MONOTONIC) start"); \
exit(1); \
}

// Check if the elapsed time since 't' is less than 'sec' seconds.
#define TIMER_NOT_EXPIRED(t, sec) (timer_elapsed_sec(&(t)) < (double)(sec))

// Futex-based Event: Shared with syzkaller executor.
// Best for general synchronization or longer waits to save CPU.
typedef struct {
int state;
} event_t;

static void event_init(event_t* ev)
{
ev->state = 0;
}
static void event_reset(event_t* ev)
{
ev->state = 0;
}

static void event_set(event_t* ev)
{
if (__atomic_load_n(&ev->state, __ATOMIC_ACQUIRE)) {
fprintf(stderr, "event already set\n");
exit(1);
}
__atomic_store_n(&ev->state, 1, __ATOMIC_RELEASE);
syscall(SYS_futex, &ev->state, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1000000);
}

static void event_wait(event_t* ev)
{
while (!__atomic_load_n(&ev->state, __ATOMIC_ACQUIRE))
syscall(SYS_futex, &ev->state, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 0, 0);
}

// userfaultfd setup: Register a memory range for page fault handling.
static int setup_uffd(void* addr, size_t len)
{
int uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
return -1;
struct uffdio_api api = {.api = UFFD_API, .features = 0};
if (ioctl(uffd, UFFDIO_API, &api) == -1) {
close(uffd);
return -1;
}
struct uffdio_register reg = {
.range = {.start = (uintptr_t)addr, .len = len},
.mode = UFFDIO_REGISTER_MODE_MISSING};
if (ioctl(uffd, UFFDIO_REGISTER, &reg) == -1) {
close(uffd);
return -1;
}
return uffd;
}

// --- Guidance on Usage ---
// 1. Use WAIT_ON/SIGNAL for tight race conditions to avoid scheduling overhead.
// 2. Use event_t (futexes) for general coordination or when waiting for longer periods.
// 3. Always use PIN_TO_CPU to increase race probability on multi-core systems.
// 4. Use setup_uffd to register a memory range for page fault handling. This allows you to
// pause a thread accessing that memory until you handle the fault, creating a reliable
// and controllable race window.
// 5. Call SETUP_UNBUFFERED_IO() at the start of main() to ensure that logs are printed
// immediately. This is essential for understanding the exact interleaving of events
// when debugging race conditions.
// 6. For timing-based loops (e.g., running a race for 10 seconds), do NOT use time(NULL)
// or loops relying on real-time clocks, as VM clocks are highly unreliable and can fail or drift.
// Instead, use the robust monotonic timing primitives TIMER_START and TIMER_NOT_EXPIRED:
// TIMER_START(start);
// while (TIMER_NOT_EXPIRED(start, 10.0)) {
// // Your race logic here
// }

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <sched.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <signal.h>
#include <errno.h>
#include <time.h>

#define NETLINK_RDMA 20
#define RDMA_NL_NLDEV 5
#define RDMA_NLDEV_CMD_NEWLINK 3
#define RDMA_NLDEV_ATTR_DEV_NAME 2
#define RDMA_NLDEV_ATTR_NDEV_NAME 51
#define RDMA_NLDEV_ATTR_LINK_TYPE 65

static int create_dummy_interface(const char *name) {
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0) return -1;

struct {
struct nlmsghdr nlh;
struct ifinfomsg ifm;
char buf[1024];
} req = {0};

req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK;
req.nlh.nlmsg_type = RTM_NEWLINK;
req.nlh.nlmsg_seq = 1;

req.ifm.ifi_family = AF_UNSPEC;

struct rtattr *rta;

rta = (struct rtattr *)(((char *)&req) + NLMSG_ALIGN(req.nlh.nlmsg_len));
rta->rta_type = IFLA_IFNAME;
rta->rta_len = RTA_LENGTH(strlen(name) + 1);
strcpy(RTA_DATA(rta), name);
req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len) + RTA_ALIGN(rta->rta_len);

struct rtattr *linkinfo = (struct rtattr *)(((char *)&req) + NLMSG_ALIGN(req.nlh.nlmsg_len));
linkinfo->rta_type = IFLA_LINKINFO;
linkinfo->rta_len = RTA_LENGTH(0);

rta = (struct rtattr *)(((char *)linkinfo) + RTA_ALIGN(linkinfo->rta_len));
rta->rta_type = IFLA_INFO_KIND;
rta->rta_len = RTA_LENGTH(strlen("dummy") + 1);
strcpy(RTA_DATA(rta), "dummy");

linkinfo->rta_len += RTA_ALIGN(rta->rta_len);
req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len) + RTA_ALIGN(linkinfo->rta_len);

struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
sendto(fd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa));

struct {
struct nlmsghdr nlh;
struct nlmsgerr err;
} resp;
recv(fd, &resp, sizeof(resp), 0);
close(fd);
return resp.err.error;
}

static int delete_interface(const char *name) {
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0) return -1;

struct {
struct nlmsghdr nlh;
struct ifinfomsg ifm;
char buf[1024];
} req = {0};

req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
req.nlh.nlmsg_type = RTM_DELLINK;
req.nlh.nlmsg_seq = 1;

req.ifm.ifi_family = AF_UNSPEC;

struct rtattr *rta;

rta = (struct rtattr *)(((char *)&req) + NLMSG_ALIGN(req.nlh.nlmsg_len));
rta->rta_type = IFLA_IFNAME;
rta->rta_len = RTA_LENGTH(strlen(name) + 1);
strcpy(RTA_DATA(rta), name);
req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len) + RTA_ALIGN(rta->rta_len);

struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
sendto(fd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa));

struct {
struct nlmsghdr nlh;
struct nlmsgerr err;
} resp;
recv(fd, &resp, sizeof(resp), 0);
close(fd);
return resp.err.error;
}

static void bring_up_interface(const char *name) {
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) return;
struct ifreq ifr;
strcpy(ifr.ifr_name, name);
if (ioctl(fd, SIOCGIFFLAGS, &ifr) == 0) {
ifr.ifr_flags |= IFF_UP;
ioctl(fd, SIOCSIFFLAGS, &ifr);
}
close(fd);
}

static int send_rdma_newlink(const char *ibdev_name, const char *link_type, const char *ndev_name) {
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_RDMA);
if (fd < 0) return -1;

struct {
struct nlmsghdr nlh;
char buf[1024];
} req = {0};

req.nlh.nlmsg_len = NLMSG_LENGTH(0);
req.nlh.nlmsg_type = (RDMA_NL_NLDEV << 10) | RDMA_NLDEV_CMD_NEWLINK;
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
req.nlh.nlmsg_seq = 1;

struct rtattr *rta;

rta = (struct rtattr *)(((char *)&req) + NLMSG_ALIGN(req.nlh.nlmsg_len));
rta->rta_type = RDMA_NLDEV_ATTR_DEV_NAME;
rta->rta_len = RTA_LENGTH(strlen(ibdev_name) + 1);
strcpy(RTA_DATA(rta), ibdev_name);
req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len) + RTA_ALIGN(rta->rta_len);

rta = (struct rtattr *)(((char *)&req) + NLMSG_ALIGN(req.nlh.nlmsg_len));
rta->rta_type = RDMA_NLDEV_ATTR_LINK_TYPE;
rta->rta_len = RTA_LENGTH(strlen(link_type) + 1);
strcpy(RTA_DATA(rta), link_type);
req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len) + RTA_ALIGN(rta->rta_len);

rta = (struct rtattr *)(((char *)&req) + NLMSG_ALIGN(req.nlh.nlmsg_len));
rta->rta_type = RDMA_NLDEV_ATTR_NDEV_NAME;
rta->rta_len = RTA_LENGTH(strlen(ndev_name) + 1);
strcpy(RTA_DATA(rta), ndev_name);
req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len) + RTA_ALIGN(rta->rta_len);

struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
sendto(fd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa));

struct {
struct nlmsghdr nlh;
struct nlmsgerr err;
} resp;
recv(fd, &resp, sizeof(resp), 0);
close(fd);
return resp.err.error;
}

int main(int argc, char **argv) {
/* no-rxe: negative control; keep the same dummy/netns churn without IB.
* Optional numeric arg: run duration in seconds (default 10).
*/
int use_rxe = 1;
double secs = 10.0;
for (int ai = 1; ai < argc; ai++) {
if (strcmp(argv[ai], "no-rxe") == 0) {
use_rxe = 0;
} else {
double v = atof(argv[ai]);
if (v > 0)
secs = v;
}
}
SETUP_UNBUFFERED_IO();
TIMER_START(start);
srand(time(NULL) ^ getpid());
printf("[*] uevent_uaf_repro: use_rxe=%d secs=%.0f pid=%d\n", use_rxe, secs, getpid());

int iter = 0;
while (TIMER_NOT_EXPIRED(start, secs)) {
char dummy_name[32];
char rxe_name[32];
sprintf(dummy_name, "d%d_%d", getpid(), iter);
sprintf(rxe_name, "r%d_%d", getpid(), iter);

int res = create_dummy_interface(dummy_name);
if (res < 0 && res != -EEXIST) {
printf("[-] Failed to create dummy interface: %d\n", res);
exit(1);
}

bring_up_interface(dummy_name);

if (use_rxe) {
res = send_rdma_newlink(rxe_name, "rxe", dummy_name);
if (res < 0) {
printf("[-] Failed to create RXE device: %d\n", res);
}
}

int p[2];
if (pipe(p) < 0) {
exit(1);
}

pid_t pid = fork();
if (pid == 0) {
close(p[0]);
if (unshare(CLONE_NEWNET) < 0) {
printf("[-] unshare(CLONE_NEWNET) failed: %d\n", errno);
exit(1);
}
// Signal parent that we are in new netns
if (write(p[1], "1", 1) != 1) {}

// Wait for parent to signal us to exit
char c;
if (read(p[1], &c, 1) != 1) {}

// Exit, triggering cleanup_net
exit(0);
}

close(p[1]);
char c;
// Wait for child to unshare
if (read(p[0], &c, 1) != 1) {}
close(p[0]);

// Signal child to exit
kill(pid, SIGKILL);

// Random delay between 0 and 50ms to hit the race window
// cleanup_net blocks on synchronize_rcu() which takes some time
usleep(rand() % 50000);

// Immediately delete the dummy interface to trigger ib_unregister_work
delete_interface(dummy_name);

waitpid(pid, NULL, 0);

// Wait a bit for workqueues to finish
usleep(50000);

iter++;
}

printf("[+] Reproducer finished after %d iterations.\n", iter);
return 0;
}