* driver core: uevent: netns teardown frees uevent_sock while a tagged uevent is still being broadcast (UAF)
[not found] ` <2026102727-data-sequel-ce41@gregkh>
@ 2026-11-10 12:56 ` 徐晗翔
2026-11-10 13:00 ` 徐晗翔
2026-11-11 7:30 ` kernfs: kernfs_drain()'s fast-path decision reads the active count without acquire ordering -- on weakly-ordered architectures the forced ->release()/unmap can be skipped 徐晗翔
2 siblings, 0 replies; 5+ messages in thread
From: 徐晗翔 @ 2026-11-10 12:56 UTC (permalink / raw)
To: dakr, rafael; +Cc: driver-core, linux-kernel
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@hust.edu.cn
^ permalink raw reply [flat|nested] 5+ messages in thread* driver core: uevent: netns teardown frees uevent_sock while a tagged uevent is still being broadcast (UAF)
[not found] ` <2026102727-data-sequel-ce41@gregkh>
2026-11-10 12:56 ` driver core: uevent: netns teardown frees uevent_sock while a tagged uevent is still being broadcast (UAF) 徐晗翔
@ 2026-11-10 13:00 ` 徐晗翔
2026-11-10 18:48 ` Greg KH
2026-11-11 7:30 ` kernfs: kernfs_drain()'s fast-path decision reads the active count without acquire ordering -- on weakly-ordered architectures the forced ->release()/unmap can be skipped 徐晗翔
2 siblings, 1 reply; 5+ messages in thread
From: 徐晗翔 @ 2026-11-10 13:00 UTC (permalink / raw)
To: dakr, rafael; +Cc: driver-core, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 6706 bytes --]
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@hust.edu.cn
[-- Attachment #2: uevent_uaf_repro.c --]
[-- Type: text/plain, Size: 13479 bytes --]
// 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, ®) == -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;
}
^ permalink raw reply [flat|nested] 5+ messages in thread* Re: driver core: uevent: netns teardown frees uevent_sock while a tagged uevent is still being broadcast (UAF)
2026-11-10 13:00 ` 徐晗翔
@ 2026-11-10 18:48 ` Greg KH
0 siblings, 0 replies; 5+ messages in thread
From: Greg KH @ 2026-11-10 18:48 UTC (permalink / raw)
To: 徐晗翔; +Cc: dakr, rafael, driver-core, linux-kernel
On Tue, Nov 10, 2026 at 09:00:29PM +0800, 徐晗翔 wrote:
> 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.
No patch?
^ permalink raw reply [flat|nested] 5+ messages in thread
* kernfs: kernfs_drain()'s fast-path decision reads the active count without acquire ordering -- on weakly-ordered architectures the forced ->release()/unmap can be skipped
[not found] ` <2026102727-data-sequel-ce41@gregkh>
2026-11-10 12:56 ` driver core: uevent: netns teardown frees uevent_sock while a tagged uevent is still being broadcast (UAF) 徐晗翔
2026-11-10 13:00 ` 徐晗翔
@ 2026-11-11 7:30 ` 徐晗翔
2026-11-11 13:41 ` Greg KH
2 siblings, 1 reply; 5+ messages in thread
From: 徐晗翔 @ 2026-11-11 7:30 UTC (permalink / raw)
To: tj; +Cc: driver-core, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 6825 bytes --]
kernfs_drain() (fs/kernfs/dir.c:508-551) decides whether it must
drain open files with the fast-path test "atomic_read(&kn->active)
== KN_DEACTIVATED_BIAS && !kernfs_should_drain_open_files(kn)" --
both a plain atomic_read (relaxed) and an unlocked read of
kn->attr.open / on->nr_mmapped / on->nr_to_release. The writer side
(the last active holder) releases correctly -- its writes to
attr.open/counter happen under the hash lock, followed by the fully
ordered atomic_dec_return() that wakes the drain waiter
(dir.c:491-495) -- but on the READER side there is no acquire edge
on any fast path: the early-return branch (:522-524), the
wait_event's non-sleeping first check (:537-538), and the slow-loop
checks where the waker has not yet touched the queue lock. The
phase-1 BIAS add is itself a relaxed atomic_add (:1563), and the
kernfs_rwsem write lock the remover holds has no intersection with
the putter. On a weakly-ordered architecture (arm64, powerpc,
riscv; x86-TSO is immune because plain load-load does not reorder),
the remover can therefore observe "active drained" WITHOUT observing
the racing opener's attr.open/nr_to_release/nr_mmapped updates, skip
kernfs_drain_open_files() entirely, and leave the mandatory
->release() and unmap_mapping_range() to be deferred to the last fd
close. Consequences under current semantics: no memory corruption
(of->kn/inode stay pinned through the dentry chain; the close path
performs the release and decrements then, and the accounting WARN
never fires) -- the observable effects are a delayed
cgroup_file_release()/put_cgroup_ns() (a cgroup-namespace reference
pinned for the lifetime of a leaked-open fd) and a skipped
unmap_mapping_range() (mmap'ed vmas survive until their next fault
takes the kernfs fault path and SIGBUSes, safely). I verified the
RACE itself dynamically on a stock Linux 7.3 KCSAN kernel: a
cgroup2-slot hammer (attached) produced 17 captures of exactly the
target race pair -- kernfs_fop_release (the nr_to_release--
writer, inlined in the close path) vs kernfs_should_drain_open_files
(read), with the reader stack at kernfs_drain+0xba, which I mapped
back to dir.c:522-524 (the early-return decision) via objdump (the
cmp $0x80000001 / KN_DEACTIVATED_BIAS immediate). The consequence
itself needs a weakly-ordered machine plus a microsecond window, so
I did not and could not trigger it on x86 -- the report is therefore
a memory-ordering fix with (a) the race pair proven to occur as a
matter of course on x86, (b) an LKMM litmus showing the outcome is
Allowed on linux-kernel.cat and Never on x86-TSO (available; please
run it), and (c) a one-line fix that closes all three fast paths.
The detailed description:
* The ordering gap (Linux 7.3)
Writer (last active holder of a HAS_RELEASE file):
mutex(attr.open = on); on->nr_to_release = 1 <- plain writes,
hash-locked
... ops->open() ...
kernfs_put_active(): atomic_dec_return(active) <- fully ordered
RMW (release
for the writer)
+ wake_up_all
(dir.c:491-495)
Reader (the remover, holding kernfs_rwsem write since phase 1 --
itself a relaxed atomic_add(BIAS) at dir.c:1563):
kernfs_drain():
if (atomic_read(&kn->active) == KN_DEACTIVATED_BIAS && /* :522,
relaxed */
!kernfs_should_drain_open_files(kn)) /* :784-802 plain
loads */
return; /* :522-524 early
out */
wait_event(root->deactivate_waitq,
atomic_read(&kn->active) == BIAS) /* :537-538 first
check also
relaxed */
No lock edge (the fast paths never take the queue lock), no data
or address dependency between active and attr.open/on->, and
control dependencies do not order subsequent loads
(Documentation/memory-barriers.txt). Only the fully-sleeping path
is ordered today (putter's RMW -> wake_up_all's queue lock ->
waiter's recheck under that lock).
* Evidence (stock 7.3 KCSAN kernel; hammer attached)
kfshammer.c drives synchronized cgroup2 slot rmdir/open/close
traffic (the cgroup release path is the HAS_RELEASE consumer).
300 s: 10,287 rmdirs x 185,310 open/close -> the exact target race
pair captured 17 times:
BUG: KCSAN: data-race in kernfs_fop_release /
kernfs_should_drain_open_files
reader stack at kernfs_drain+0xba <- mapped to dir.c:522-524
(objdump: cmp $0x80000001 == KN_DEACTIVATED_BIAS; also +0x1ab =
dir.c:545)
writer stack: the close path's inlined nr_to_release--
plus 7 same-root reports (__kernfs_remove /
kernfs_unlink_open_file family)
Attribution controls: a sequential-cleanup control (kfs1ctl2) also
captures the pair (plain rmdir chains reach it); a TRUE
zero-rmdir control (kfs1ctl4, 692,469 open+close, zero rmdirs)
shows should_drain never executing and zero target-family
reports -- the pair is specific to rmdir-vs-close concurrency.
The stale-READ consequence (skipped drain) is architecturally
impossible on this x86 guest; the race itself is not.
* The fix (one line, both decision sites)
--- a/fs/kernfs/dir.c
+++ b/fs/kernfs/dir.c
@@ -519
- if (atomic_read(&kn->active) == KN_DEACTIVATED_BIAS &&
- !kernfs_should_drain_open_files(kn))
+ /* acquire pairs with the fully-ordered dec_return in
+ * kernfs_put_active() so that the skip decision observes the
+ * racing opener's attr.open/counter updates */
+ if (atomic_read_acquire(&kn->active) == KN_DEACTIVATED_BIAS &&
+ !kernfs_should_drain_open_files(kn))
return;
@@ -537
- wait_event(root->deactivate_waitq,
- atomic_read(&kn->active) == KN_DEACTIVATED_BIAS);
+ wait_event(root->deactivate_waitq,
+ atomic_read_acquire(&kn->active) == KN_DEACTIVATED_BIAS);
atomic_read_acquire() is available in this tree (include/linux/
atomic/atomic-instrumented.h:47) and covers all three fast paths;
the sleeping path was already ordered and is unaffected.
smp_load_acquire() is an equivalent alternative.
A herd7 litmus test for the pair (Expected: Allowed on
linux-kernel.cat, Never on x86-TSO) is available and will be
provided with the patch -- please run it, as no herd7 exists on my
side.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: kfshammer.c --]
[-- Type: text/plain, Size: 9682 bytes --]
// SPDX-License-Identifier: GPL-2.0
/* kfshammer.c - user-space hammer for fs__kernfs audit verification (v3).
*
* Target: kernfs open-file accounting vs drain decision (fs/kernfs/file.c,
* fs/kernfs/dir.c), exercised on cgroup2 attribute files.
*
* Synchronized slot design (structurally forces the races instead of
* relying on timing luck):
*
* slot states: 0 empty -> 1 prepared (subtree t_i/{x,y} created, 12 fds
* open) -> 2 removing (rmdir(2) in-kernel, recursive kernfs_remove over
* ~33 nodes) -> 0.
*
* On seeing state 2, the prep thread closes its fds one by one with
* microsecond gaps, so kernfs_unlink_open_file (plain stores under the
* hashed per-node mutex) executes inside / right after the removal,
* concurrent with kernfs_drain's kernfs_should_drain_open_files loads
* (plain loads, no acquire: audit Case 5). fd[0] closes last, often
* after the forced release, exercising kernfs_release_file exactly-once
* and the close-after-release WARN tripwire (file.c:586).
*
* Plans:
* kfs1 (KCSAN): expect data-race reports naming
* kernfs_should_drain_open_files / kernfs_unlink_open_file /
* kernfs_get_open_node / kernfs_drain_open_files
* kfs1ctl: ctrl=1, rmdir disabled -> drain never runs -> accounting is
* mutex-protected -> expect zero drain/accounting reports
* kfs2 (KASAN): heavier negative control; in-kernel tripwires
* (file.c:586, file.c:830, dir.c:602) must stay silent and KASAN
* must see no use-after-free (any hit refutes the Case 2/3 loop)
*
* usage: kfshammer <cg2-mount> <slots> <rmdir-threads> <seconds> [ctrl]
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <time.h>
#include <sys/stat.h>
#define MAXSLOTS 32
#define NF_TOP 10
#define NF_CHILD 4
static char mount_[256];
static int slots = 8, rmdirs = 3, secs = 120, ctrl = 0;
static volatile int running = 1;
/* per-slot state: 0 empty, 1 prepared (fds open), 2 removing */
static volatile int slot_state[MAXSLOTS];
static int slot_fd[MAXSLOTS][NF_TOP + 2 * NF_CHILD];
static int slot_nopen[MAXSLOTS];
static const char *files_top[NF_TOP] = {
"cgroup.procs", "cgroup.type", "cgroup.stat", "cgroup.events",
"cgroup.controllers", "cgroup.subtree_control", "cgroup.threads",
"cgroup.max.descendants", "cgroup.max.depth", "cgroup.freeze",
};
static const char *files_child[NF_CHILD] = {
"cgroup.procs", "cgroup.type", "cgroup.stat", "cgroup.events",
};
enum { E_MKDIR_OK, E_MKDIR_FAIL, E_OPEN_OK, E_OPEN_FAIL, E_CLOSE,
E_RMDIR_OK, E_RMDIR_FAIL, E_MAX };
static long st[E_MAX];
static int last_rmdir_errno, last_open_errno;
#define ATOMIC_INC(x) __atomic_add_fetch(&(x), 1, __ATOMIC_RELAXED)
/* nanosleep with us ranges from 50us to 500ms - all < 1s, tv_nsec safe */
static int usleep_(long us)
{
struct timespec ts = { .tv_sec = 0, .tv_nsec = us * 1000L };
if (ts.tv_nsec >= 1000000000L) { /* paranoia */
ts.tv_sec += ts.tv_nsec / 1000000000L;
ts.tv_nsec %= 1000000000L;
}
return nanosleep(&ts, NULL);
}
static void mkpath(char *buf, size_t len, int slot, const char *sub,
const char *f)
{
snprintf(buf, len, "%s/t%d%s%s%s%s", mount_, slot, sub ? "/" : "",
sub ?: "", f ? "/" : "", f ?: "");
}
/* prep thread: owns one slot */
static void *th_prep(void *arg)
{
long id = (long)(intptr_t)arg;
char path[512];
unsigned seed = id * 7919u + 7;
int n = 0;
while (running) {
mkpath(path, sizeof(path), (int)id, NULL, NULL);
if (mkdir(path, 0755) == 0) {
ATOMIC_INC(st[E_MKDIR_OK]);
} else if (!ctrl) {
ATOMIC_INC(st[E_MKDIR_FAIL]);
/* stale dir from a failed cycle: best-effort cleanup
* (bottom-up), then retry */
mkpath(path, sizeof(path), (int)id, "x", NULL);
rmdir(path);
mkpath(path, sizeof(path), (int)id, "y", NULL);
rmdir(path);
mkpath(path, sizeof(path), (int)id, NULL, NULL);
rmdir(path);
usleep_(2000);
continue;
} else {
/* ctrl: dir persists - churn on it, zero rmdir calls */
ATOMIC_INC(st[E_MKDIR_FAIL]);
}
mkpath(path, sizeof(path), (int)id, "x", NULL);
mkdir(path, 0755);
mkpath(path, sizeof(path), (int)id, "y", NULL);
mkdir(path, 0755);
/* fd[0] = top cgroup.procs; closes LAST (after forced release) */
n = 0;
mkpath(path, sizeof(path), (int)id, NULL, files_top[0]);
int fd = open(path, O_RDONLY);
if (fd >= 0) { slot_fd[id][n++] = fd; ATOMIC_INC(st[E_OPEN_OK]); }
else { last_open_errno = errno; ATOMIC_INC(st[E_OPEN_FAIL]); }
for (int i = 1; i < NF_TOP; i++) {
mkpath(path, sizeof(path), (int)id, NULL, files_top[i]);
fd = open(path, O_RDONLY);
if (fd < 0) { last_open_errno = errno; ATOMIC_INC(st[E_OPEN_FAIL]); continue; }
ATOMIC_INC(st[E_OPEN_OK]);
slot_fd[id][n++] = fd;
}
for (int c = 0; c < 2; c++) {
const char *sub = c ? "y" : "x";
for (int i = 0; i < NF_CHILD; i++) {
mkpath(path, sizeof(path), (int)id, sub, files_child[i]);
fd = open(path, O_RDONLY);
if (fd < 0) { last_open_errno = errno; ATOMIC_INC(st[E_OPEN_FAIL]); continue; }
ATOMIC_INC(st[E_OPEN_OK]);
slot_fd[id][n++] = fd;
}
}
slot_nopen[id] = n;
if (ctrl) {
/* control mode (churn-only): open/read/poll/close on
* persistent dirs, NO removal ever -> kernfs_drain /
* kernfs_should_drain_open_files never execute, so
* the target race site is unreachable by construction;
* accounting stays under the hashed mutex */
for (int i = 0; i < n; i++) {
close(slot_fd[id][i]);
ATOMIC_INC(st[E_CLOSE]);
}
slot_nopen[id] = 0;
usleep_(500);
continue;
}
if (n >= 2) {
slot_state[id] = 1;
/* short poll on fd0: exercises kernfs_generic_poll;
* (kernfs releases the active ref before poll sleeps,
* so this does NOT lengthen the drain window) */
if ((rand_r(&seed) & 3) == 0) {
struct pollfd pf = { .fd = slot_fd[id][0],
.events = POLLPRI };
poll(&pf, 1, 0);
}
/* wait for the remover to mark state 2 */
int spins = 0;
while (running && slot_state[id] == 1 && spins < 3000) {
usleep_(100);
spins++;
}
/* close fds INSIDE the rmdir(2) kernel window, with
* gaps so stores land across the whole removal */
for (int i = 1; i < slot_nopen[id]; i++) {
close(slot_fd[id][i]);
ATOMIC_INC(st[E_CLOSE]);
usleep_(100 + (long)(rand_r(&seed) % 300));
}
/* fd[0] closes last: often after the forced release */
close(slot_fd[id][0]);
ATOMIC_INC(st[E_CLOSE]);
} else {
for (int i = 0; i < n; i++)
close(slot_fd[id][i]);
}
slot_nopen[id] = 0;
slot_state[id] = 0;
usleep_(500);
}
return NULL;
}
/* remover thread: drive slots through state 2; bottom-up (children first,
* each child rmdir runs its own drain window, then the top removal runs
* with the top-level fds still open) */
static void *th_rmdir(void *arg)
{
long id = (long)(intptr_t)arg;
char path[512];
unsigned seed = id * 104729u + 1;
int i = (int)id;
while (running) {
if (slot_state[i] == 1) {
slot_state[i] = 2;
int ok = 1;
mkpath(path, sizeof(path), i, "x", NULL);
if (rmdir(path) != 0)
ok = 0;
mkpath(path, sizeof(path), i, "y", NULL);
if (rmdir(path) != 0)
ok = 0;
mkpath(path, sizeof(path), i, NULL, NULL);
if (rmdir(path) != 0)
ok = 0;
if (ok)
ATOMIC_INC(st[E_RMDIR_OK]);
else {
last_rmdir_errno = errno;
ATOMIC_INC(st[E_RMDIR_FAIL]);
}
}
i = (i + 1) % slots;
usleep_(rand_r(&seed) % 400);
}
return NULL;
}
static void *th_report(void *arg)
{
(void)arg;
/* exactly 3 progress lines per run to avoid serial flooding */
long wait[] = { 30, 90, 0 };
for (int k = 0; k < 3 && running; k++) {
struct timespec ts = { .tv_sec = wait[k] ? wait[k] : secs,
.tv_nsec = 0 };
if (nanosleep(&ts, NULL) && !running)
break;
fprintf(stderr,
"KFSHAMMER-PROGRESS[%d]: mkdir_ok=%ld open_ok=%ld "
"close=%ld rmdir_ok=%ld rmdir_fail=%ld "
"last_rmdir_errno=%d last_open_errno=%d\n",
k + 1, st[E_MKDIR_OK], st[E_OPEN_OK], st[E_CLOSE],
st[E_RMDIR_OK], st[E_RMDIR_FAIL],
last_rmdir_errno, last_open_errno);
}
return NULL;
}
int main(int argc, char **argv)
{
if (argc < 5) {
fprintf(stderr,
"usage: %s <mount> <slots> <rmdir-threads> <secs> [ctrl]\n",
argv[0]);
return 1;
}
snprintf(mount_, sizeof(mount_), "%s", argv[1]);
slots = atoi(argv[2]);
rmdirs = atoi(argv[3]);
secs = atoi(argv[4]);
if (argc > 5)
ctrl = atoi(argv[5]);
if (slots > MAXSLOTS)
slots = MAXSLOTS;
int nthreads = slots + (ctrl ? 0 : rmdirs) + 1;
pthread_t *t = calloc(nthreads, sizeof(*t));
for (long i = 0; i < slots; i++)
pthread_create(&t[i], NULL, th_prep, (void *)i);
for (long i = 0; i < (long)(ctrl ? 0u : (unsigned)rmdirs); i++)
pthread_create(&t[slots + i], NULL, th_rmdir, (void *)(long)i);
pthread_create(&t[nthreads - 1], NULL, th_report, NULL);
struct timespec ts = { .tv_sec = secs, .tv_nsec = 0 };
nanosleep(&ts, &ts);
running = 0;
for (int i = 0; i < nthreads; i++)
pthread_join(t[i], NULL);
printf("KFSHAMMER-FINAL: mkdir_ok=%ld mkdir_fail=%ld open_ok=%ld "
"open_fail=%ld close=%ld rmdir_ok=%ld rmdir_fail=%ld "
"last_rmdir_errno=%d last_open_errno=%d\n",
st[E_MKDIR_OK], st[E_MKDIR_FAIL], st[E_OPEN_OK],
st[E_OPEN_FAIL], st[E_CLOSE], st[E_RMDIR_OK],
st[E_RMDIR_FAIL], last_rmdir_errno, last_open_errno);
fflush(stdout);
printf("KFSHAMMER-FINAL2: rmdir_ok=%ld rmdir_fail=%ld close=%ld "
"open_ok=%ld mkdir_ok=%ld\n",
st[E_RMDIR_OK], st[E_RMDIR_FAIL], st[E_CLOSE], st[E_OPEN_OK],
st[E_MKDIR_OK]);
fflush(stdout);
return 0;
}
^ permalink raw reply [flat|nested] 5+ messages in thread* Re: kernfs: kernfs_drain()'s fast-path decision reads the active count without acquire ordering -- on weakly-ordered architectures the forced ->release()/unmap can be skipped
2026-11-11 7:30 ` kernfs: kernfs_drain()'s fast-path decision reads the active count without acquire ordering -- on weakly-ordered architectures the forced ->release()/unmap can be skipped 徐晗翔
@ 2026-11-11 13:41 ` Greg KH
0 siblings, 0 replies; 5+ messages in thread
From: Greg KH @ 2026-11-11 13:41 UTC (permalink / raw)
To: 徐晗翔; +Cc: tj, driver-core, linux-kernel
On Wed, Nov 11, 2026 at 03:30:11PM +0800, 徐晗翔 wrote:
> kernfs_drain() (fs/kernfs/dir.c:508-551) decides whether it must
> drain open files with the fast-path test "atomic_read(&kn->active)
> == KN_DEACTIVATED_BIAS && !kernfs_should_drain_open_files(kn)" --
> both a plain atomic_read (relaxed) and an unlocked read of
> kn->attr.open / on->nr_mmapped / on->nr_to_release. The writer side
> (the last active holder) releases correctly -- its writes to
> attr.open/counter happen under the hash lock, followed by the fully
> ordered atomic_dec_return() that wakes the drain waiter
> (dir.c:491-495) -- but on the READER side there is no acquire edge
> on any fast path: the early-return branch (:522-524), the
> wait_event's non-sleeping first check (:537-538), and the slow-loop
> checks where the waker has not yet touched the queue lock. The
> phase-1 BIAS add is itself a relaxed atomic_add (:1563), and the
> kernfs_rwsem write lock the remover holds has no intersection with
> the putter. On a weakly-ordered architecture (arm64, powerpc,
> riscv; x86-TSO is immune because plain load-load does not reorder),
> the remover can therefore observe "active drained" WITHOUT observing
> the racing opener's attr.open/nr_to_release/nr_mmapped updates, skip
> kernfs_drain_open_files() entirely, and leave the mandatory
> ->release() and unmap_mapping_range() to be deferred to the last fd
> close. Consequences under current semantics: no memory corruption
> (of->kn/inode stay pinned through the dentry chain; the close path
> performs the release and decrements then, and the accounting WARN
> never fires) -- the observable effects are a delayed
> cgroup_file_release()/put_cgroup_ns() (a cgroup-namespace reference
> pinned for the lifetime of a leaked-open fd) and a skipped
> unmap_mapping_range() (mmap'ed vmas survive until their next fault
> takes the kernfs fault path and SIGBUSes, safely). I verified the
> RACE itself dynamically on a stock Linux 7.3 KCSAN kernel: a
> cgroup2-slot hammer (attached) produced 17 captures of exactly the
> target race pair -- kernfs_fop_release (the nr_to_release--
> writer, inlined in the close path) vs kernfs_should_drain_open_files
> (read), with the reader stack at kernfs_drain+0xba, which I mapped
> back to dir.c:522-524 (the early-return decision) via objdump (the
> cmp $0x80000001 / KN_DEACTIVATED_BIAS immediate). The consequence
> itself needs a weakly-ordered machine plus a microsecond window, so
> I did not and could not trigger it on x86 -- the report is therefore
> a memory-ordering fix with (a) the race pair proven to occur as a
> matter of course on x86, (b) an LKMM litmus showing the outcome is
> Allowed on linux-kernel.cat and Never on x86-TSO (available; please
> run it), and (c) a one-line fix that closes all three fast paths.
<snip>
This is a wall of LLM text that isn't useful to anyone, sorry.
Please just submit normal patches, as our documentation asks for.
thanks,
greg k-h
^ permalink raw reply [flat|nested] 5+ messages in thread