DragonFlyBSD Kernel Audit
DF-1176 / harness.c
← back to finding ↓ download raw
/*
 * DF-1176 — userspace harness for gfx_v9_0_init_microcode integer overflow + OOB.
 *
 * Reconstructs the kmalloc + copy loops from
 *   sys/dev/drm/amd/amdgpu/gfx_v9_0.c:691-713
 *
 * Two bugs:
 *  (1) Integer overflow: reg_list_format_size_bytes + reg_list_size_bytes
 *      are u32 fields from the firmware header; their sum is computed in
 *      32-bit arithmetic and can wrap to a small value, producing an
 *      undersized kmalloc.  The subsequent copy loops then write past the
 *      allocation.
 *  (2) Unvalidated offsets: reg_list_format_array_offset_bytes and
 *      reg_list_array_offset_bytes are used as offsets into the firmware
 *      buffer without any bounds check against fw->datasize.  A large
 *      offset -> OOB read past the firmware buffer.
 *
 * amdgpu_ucode_validate() at amdgpu_ucode.c:251 only verifies
 * `fw->datasize == hdr->size_bytes`; it does NOT validate any internal
 * offsets or sizes, so a crafted firmware file passes validation.
 *
 * Build:  cc -O2 -o harness harness.c
 * Run:    ./harness
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>

static sigjmp_buf jb;
static volatile int got_sig;
static void handler(int s, siginfo_t *si, void *uc) {
	(void)si; (void)uc;
	got_sig = s;
	siglongjmp(jb, 1);
}

int main(void)
{
	struct sigaction sa;
	memset(&sa, 0, sizeof(sa));
	sa.sa_sigaction = handler;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_SIGINFO;
	sigaction(SIGSEGV, &sa, NULL);
	sigaction(SIGBUS,  &sa, NULL);

	/* ---------- Bug 1: u32 overflow in kmalloc size ---------- */
	puts("=== Bug 1: u32 overflow in kmalloc(size_a + size_b) at gfx_v9_0.c:696-697 ===");

	uint32_t size_a = 0x80000010u;  /* 2 GB + 16 */
	uint32_t size_b = 0x80000010u;  /* 2 GB + 16 */
	uint32_t sum_u32 = size_a + size_b;    /* wraps in u32: 0x20 = 32 bytes */
	printf("  reg_list_format_size_bytes = 0x%08x (%u)\n", size_a, size_a);
	printf("  reg_list_size_bytes        = 0x%08x (%u)\n", size_b, size_b);
	printf("  u32 sum                    = 0x%08x (%u)   <-- WRAPS\n",
	       sum_u32, sum_u32);
	printf("  (true 64-bit sum           = 0x%016llx)\n",
	       (unsigned long long)size_a + size_b);

	/* The kernel then does kmalloc(sum_u32).  In the harness we model
	 * this as a 32-byte allocation, then show the copy loop writing
	 * past it. */
	size_t kmalloc_size = sum_u32;  /* what the kernel actually allocates */
	uint8_t *alloc = calloc(1, kmalloc_size + 4096); /* +guard for harness safety */
	printf("  kmalloc(%zu) returns %p\n", kmalloc_size, (void *)alloc);
	uint32_t *dst = (uint32_t *)alloc;

	/* Copy loop at gfx_v9_0.c:705-706:  for (i=0 ; i<(size_a >> 2); i++)
	 *                                                  dst[i] = src[i];
	 * size_a = 0x80000010 -> 0x20000004 iterations = 2 GB + 4 u32 words */
	uint32_t iter = size_a >> 2;
	uint32_t bytes_written = iter * 4;
	printf("  copy loop runs %u iterations, writing %u bytes into %zu-byte buffer\n",
	       iter, bytes_written, kmalloc_size);
	printf("  -> %u-byte heap overflow past kmalloc'd buffer\n",
	       bytes_written - (uint32_t)kmalloc_size);
	puts("  (in the kernel: structured heap overflow into whatever follows the\n"
	     "   register_list_format allocation -- classic slab-corruption primitive)");

	/* Demonstrate a small portion of the overflow writing past alloc */
	for (uint32_t i = 0; i < (kmalloc_size / 4) + 16; i++)
		dst[i] = 0xDEADBEEF;
	printf("  wrote 0xDEADBEEF into %u slots past the nominal %zu-byte allocation\n",
	       16u, kmalloc_size);
	free(alloc);

	/* ---------- Bug 2: unvalidated firmware offset ---------- */
	puts("");
	puts("=== Bug 2: unvalidated reg_list_format_array_offset_bytes at gfx_v9_0.c:703-706 ===");

	uint32_t fw_datasize = 4096;        /* typical small firmware blob size */
	uint8_t *fw = calloc(1, fw_datasize + 4096); /* +guard for harness safety */
	uint32_t offset = 0x10000;          /* attacker-supplied; way past datasize */
	printf("  fw->datasize                       = %u\n", fw_datasize);
	printf("  reg_list_format_array_offset_bytes = 0x%x (%u)  <-- > datasize\n",
	       offset, offset);

	uint32_t *src = (uint32_t *)((uintptr_t)fw + offset);
	uint32_t read_iter = size_a >> 2;   /* huge; we just show the first fault */
	puts("  loop reads from (fw + offset) with no check that offset+size <= datasize");
	fflush(stdout);

	got_sig = 0;
	if (sigsetjmp(jb, 1) == 0) {
		volatile uint32_t first = src[0];
		(void)first;
		printf("  (read succeeded at fw+%u -- in the kernel this is unmapped\n"
		       "   kernel memory: OOB read past firmware kmalloc -> panic)\n",
		       offset);
	} else {
		printf("  BUG: OOB read faulted (signal %d) -- kernel equivalent: page\n"
		       "       fault reading fw+%u past the firmware buffer in\n"
		       "       gfx_v9_0_init_microcode -> panic\n",
		       got_sig, offset);
	}
	free(fw);
	(void)read_iter;

	puts("");
	puts("DF-1176 harness: both bugs demonstrated.");
	puts("  Impact ceiling: heap overflow (slab corruption) + OOB read panic.");
	return 0;
}