driver core: uevent: netns teardown frees uevent_sock while a tagged uevent is still being broadcast (UAF)
From: 徐晗翔
Date: Tue Nov 10 2026 - 08:07:51 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