โฌข DragonFlyBSD Kernel Audit
DF-0901 / race_harness.c
โ† back to finding โ†“ download raw
/*
 * race_harness.c โ€” DragonFlyBSD kernel module that deterministically
 * reproduces the unlocked-traversal-vs-free race pattern in smbfs_node_alloc
 * (DF-0901).
 *
 * smbfs_node_alloc (sys/vfs/smbfs/smbfs_node.c) does:
 *   smbfs_hash_unlock(smp, td);           // line 203: DROP lock
 *   if (vget(vp, LK_EXCLUSIVE) != 0)      // line 204
 *       goto retry;
 *   LIST_FOREACH(np2, nhpp, n_hash) { ... } // line 209: UNLOCKED relookup
 *
 * while smbfs_reclaim concurrently does:
 *   smbfs_hash_lock(smp, td);             // line 304
 *   LIST_REMOVE(np, n_hash);              // line 310
 *   smbfs_hash_unlock(smp, td);           // line 316
 *   kfree(np, M_SMBNODE);                 // line 319: FREES the node
 *
 * This harness replicates the EXACT pattern: a LIST of M_SMBNODE-allocated
 * entries (same slab bucket as real smbnodes), one thread doing an unlocked
 * LIST_FOREACH (simulating line 209), another doing LIST_REMOVE + kfree
 * (simulating lines 310+319). With INVARIANTS, kfree poisons the freed
 * memory with WEIRD_ADDR (0xdeadc0de), so the unlocked traversal
 * dereferences poisoned le_next โ†’ fatal trap / page fault.
 *
 * On the FIXED kernel/module (lock held around the traversal), this harness
 * does NOT panic because Thread B blocks on the lock while Thread A
 * traverses.
 *
 * Build: see Makefile below. Load: kldload ./race_harness.ko
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/queue.h>
#include <sys/proc.h>
#include <sys/kthread.h>
#include <sys/lock.h>
#include <sys/module.h>
#include <sys/sysctl.h>

/* M_SMBNODE is defined in the smbfs module. Define our own compatible type. */
MALLOC_DEFINE(M_RACE_SMBNODE, "race_smbnode", "DF-0901 race harness smbnode sim");

/*
 * Simulated smbnode โ€” must be the same size as real struct smbnode so it
 * lands in the same slab bucket. struct smbnode is ~200+ bytes.
 * We use a 256-byte structure to be safe.
 */
struct sim_smbnode {
    LIST_ENTRY(sim_smbnode) n_hash;    /* must be first, matching real smbnode */
    char padding[256 - sizeof(LIST_ENTRY(sim_smbnode))];
};

LIST_HEAD(sim_hashhead, sim_smbnode);

static struct sim_hashhead sim_hash;
static struct lock sim_lock;        /* simulates sm_hashlock */
static int sim_running = 0;
static int sim_hold_lock = 1;       /* set to 1 to hold lock during traversal (the FIX) */
static int num_entries = 32;
SYSCTL_INT(_debug, OID_AUTO, race_entries, CTLFLAG_RW, &num_entries, 0, "");
SYSCTL_INT(_debug, OID_AUTO, race_holdlock, CTLFLAG_RW, &sim_hold_lock, 0,
    "1 = hold lock during traversal (simulates fix), 0 = unlocked (bug)");

static struct thread *trav_td = NULL;
static struct thread *free_td = NULL;

/*
 * Thread A: simulates smbfs_node_alloc line 209 โ€” unlocked LIST_FOREACH.
 * Repeatedly traverses the hash list reading each entry's fields.
 */
static void
traverse_thread(void *arg)
{
    struct sim_smbnode *np;
    int iter = 0;

    while (sim_running) {
        if (sim_hold_lock)
            lockmgr(&sim_lock, LK_EXCLUSIVE);

        /* Unlocked traversal (the bug) โ€” or locked (the fix) */
        LIST_FOREACH(np, &sim_hash, n_hash) {
            /* Read fields โ€” on freed/poisoned memory this panics */
            volatile char c = ((char *)np)[0];
            (void)c;
        }

        if (sim_hold_lock)
            lockmgr(&sim_lock, LK_RELEASE);

        iter++;
        if (iter % 10000 == 0)
            kprintf("DF-0901 traverse: %d iterations\n", iter);
    }
    kprintf("DF-0901 traverse_thread exiting after %d iterations\n", iter);
    wakeup(&trav_td);
}

/*
 * Thread B: simulates smbfs_reclaim โ€” removes + frees entries from the list.
 */
static void
free_thread(void *arg)
{
    struct sim_smbnode *np, *np2;
    int iter = 0;

    while (sim_running) {
        lockmgr(&sim_lock, LK_EXCLUSIVE);

        /* Remove one entry from the list (simulates LIST_REMOVE at line 310) */
        np = LIST_FIRST(&sim_hash);
        if (np != NULL) {
            LIST_REMOVE(np, n_hash);
        }

        lockmgr(&sim_lock, LK_RELEASE);

        /* Free it OUTSIDE the lock (simulates kfree at line 319, which is
         * also outside the hash lock in smbfs_reclaim) */
        if (np != NULL) {
            kfree(np, M_RACE_SMBNODE);
            /* Allocate a replacement to keep the list populated */
            np2 = kmalloc(sizeof(struct sim_smbnode), M_RACE_SMBNODE,
                          M_WAITOK | M_ZERO);
            lockmgr(&sim_lock, LK_EXCLUSIVE);
            LIST_INSERT_HEAD(&sim_hash, np2, n_hash);
            lockmgr(&sim_lock, LK_RELEASE);
        }

        iter++;
        if (iter % 10000 == 0)
            kprintf("DF-0901 free: %d iterations\n", iter);
    }
    kprintf("DF-0901 free_thread exiting after %d iterations\n", iter);
    wakeup(&free_td);
}

static int
race_load(struct module *m, int what, void *arg)
{
    int error = 0;
    struct sim_smbnode *np;
    int i;

    switch (what) {
    case MOD_LOAD:
        LIST_INIT(&sim_hash);
        lockinit(&sim_lock, "raceh", 0, 0);

        /* Populate the hash list */
        for (i = 0; i < num_entries; i++) {
            np = kmalloc(sizeof(struct sim_smbnode), M_RACE_SMBNODE,
                         M_WAITOK | M_ZERO);
            LIST_INSERT_HEAD(&sim_hash, np, n_hash);
        }

        sim_running = 1;
        kprintf("DF-0901: race harness loaded (%d entries, holdlock=%d)\n",
                num_entries, sim_hold_lock);
        kprintf("DF-0901: if holdlock=0 (bug), expect UAF panic shortly\n");

        error = kthread_create(&traverse_thread, NULL, &trav_td, "race_trav");
        if (error)
            return error;
        error = kthread_create(&free_thread, NULL, &free_td, "race_free");
        if (error) {
            sim_running = 0;
            tsleep(&trav_td, 0, "racex", hz);
            return error;
        }
        break;

    case MOD_UNLOAD:
        sim_running = 0;
        /* Wait for threads to exit */
        tsleep(&trav_td, 0, "racex1", 5*hz);
        tsleep(&free_td, 0, "racex2", 5*hz);

        /* Free remaining entries */
        while ((np = LIST_FIRST(&sim_hash)) != NULL) {
            LIST_REMOVE(np, n_hash);
            kfree(np, M_RACE_SMBNODE);
        }
        lockuninit(&sim_lock);
        kprintf("DF-0901: race harness unloaded\n");
        break;

    default:
        break;
    }
    return error;
}

static moduledata_t race_mod = {
    "race_harness",
    race_load,
    NULL
};

DECLARE_MODULE(race_harness, race_mod, SI_SUB_EXEC, SI_ORDER_ANY);
MODULE_VERSION(race_harness, 1);