DragonFlyBSD Kernel Audit
DF-1946 / validate_bypass.c
← back to finding ↓ download raw
/*
 * DF-1946 — amdgpu_ucode_validate() missing minimum-size / bounds checks.
 *
 * Source under audit: sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260
 *
 *   int amdgpu_ucode_validate(const struct firmware *fw)
 *   {
 *       const struct common_firmware_header *hdr =
 *           (const struct common_firmware_header *)fw->data;
 *       if (fw->datasize == le32_to_cpu(hdr->size_bytes))
 *           return 0;
 *       return -EINVAL;
 *   }
 *
 * The function ONLY verifies that the buffer length matches the header's
 * self-declared size_bytes.  It does NOT check:
 *   (a) datasize >= sizeof(struct common_firmware_header) == 32, so a 4-byte
 *       firmware whose first 4 bytes equal datasize passes; every field past
 *       offset 4 (header_size_bytes at offset 4, ..., ucode_size_bytes at
 *       offset 20, ucode_array_offset_bytes at offset 24, crc32 at offset 28)
 *       is read out of bounds by every caller.
 *   (b) ucode_array_offset_bytes is within [0, datasize), so a header can
 *       claim a payload that begins past the end of the buffer.
 *   (c) ucode_array_offset_bytes + ucode_size_bytes <= datasize (overflow-
 *       safe), so callers (e.g. amdgpu_ucode_init_single_fw at L347-351) will
 *       memcpy from a pointer past fw->data and/or past fw->data+datasize.
 *
 * This is the root gateway for 30+ callers (gfx_v7/8/9_0.c,
 * psp_v3_1/v10/v11_0.c, gmc_v7/8_0.c, amdgpu_uvd/vce.c, smu*_smumgr.c).
 *
 * THREAT MODEL: HW-gated. amdgpu_ucode_validate runs during amdgpu device
 * attach (and resume), which requires an AMD GPU to be present. On a system
 * WITH an AMD GPU, an attacker who can plant/replace a firmware file in the
 * kernel firmware search path (root, or writable /lib/firmware) can pass an
 * OOB-reading header through validate(); the resulting OOB read feeds the
 * DF-1947 OOB write.
 *
 * This harness replicates the validate() logic EXACTLY in userspace and shows
 * that crafted firmware images pass it while being objectively unsafe to
 * dereference. It then prints the offset arithmetic each caller would
 * perform, proving the OOB read.
 */

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

/* Verbatim copy of the struct from sys/dev/drm/amd/amdgpu/amdgpu_ucode.h:26 */
struct common_firmware_header {
	uint32_t size_bytes;                 /* offset 0  */
	uint32_t header_size_bytes;          /* offset 4  */
	uint16_t header_version_major;       /* offset 8  */
	uint16_t header_version_minor;       /* offset 10 */
	uint16_t ip_version_major;           /* offset 12 */
	uint16_t ip_version_minor;           /* offset 14 */
	uint32_t ucode_version;              /* offset 16 */
	uint32_t ucode_size_bytes;           /* offset 20 */
	uint32_t ucode_array_offset_bytes;   /* offset 24 */
	uint32_t crc32;                      /* offset 28 */
};                                      /* sizeof == 32 */

struct firmware {
	const uint8_t *data;
	size_t datasize;
};

/* Verbatim replica of amdgpu_ucode_validate (amdgpu_ucode.c:251-260). */
static int amdgpu_ucode_validate(const struct firmware *fw)
{
	const struct common_firmware_header *hdr =
		(const struct common_firmware_header *)fw->data;

	if (fw->datasize == hdr->size_bytes)   /* le32 removed; host-endian demo */
		return 0;

	return -22;  /* -EINVAL */
}

/* The "fixed" version we propose. */
static int amdgpu_ucode_validate_fixed(const struct firmware *fw)
{
	const struct common_firmware_header *hdr =
		(const struct common_firmware_header *)fw->data;
	uint32_t size_bytes, ucode_size_bytes, ucode_array_offset_bytes;

	if (fw->datasize < sizeof(*hdr))
		return -22;

	size_bytes               = hdr->size_bytes;
	ucode_size_bytes         = hdr->ucode_size_bytes;
	ucode_array_offset_bytes = hdr->ucode_array_offset_bytes;

	if (fw->datasize != size_bytes)
		return -22;

	if (ucode_array_offset_bytes > size_bytes ||
	    ucode_size_bytes > size_bytes - ucode_array_offset_bytes)
		return -22;

	return 0;
}

static void run_case(const char *name, const uint8_t *buf, size_t len)
{
	struct firmware fw = { .data = buf, .datasize = len };
	int rc_vanilla, rc_fixed;

	printf("=== %s (datasize=%zu) ===\n", name, len);
	rc_vanilla = amdgpu_ucode_validate(&fw);
	rc_fixed   = amdgpu_ucode_validate_fixed(&fw);
	printf("  vanilla amdgpu_ucode_validate: %s (rc=%d)%s\n",
		rc_vanilla == 0 ? "PASS" : "reject", rc_vanilla,
		rc_vanilla == 0 ? "  <- bug: caller will deref OOB" : "");
	printf("  fixed    amdgpu_ucode_validate: %s (rc=%d)\n",
		rc_fixed == 0 ? "pass" : "REJECT", rc_fixed);

	/* Replicate what callers do once validate() returns 0: they dereference
	 * hdr->ucode_size_bytes (offset 20) and hdr->ucode_array_offset_bytes
	 * (offset 24) and feed them to a memcpy. We compute, WITHOUT actually
	 * reading OOB, how many bytes past fw->data the deref / memcpy would
	 * touch. */
	if (rc_vanilla == 0) {
		size_t need_for_fields = 28; /* up through ucode_array_offset_bytes */
		size_t field_oob = (len < need_for_fields)
			? (need_for_fields - len) : 0;
		printf("  CALLER deref of hdr->ucode_size_bytes (off 20):\n");
		if (len < 24) {
			printf("    reads 4 bytes starting at data+20; datasize=%zu "
			       "=> %zu-byte OOB READ past fw->data\n", len, 24 - len);
		} else {
			printf("    in-bounds by luck\n");
		}
		if (field_oob)
			printf("    header field read alone needs %zu bytes past end-of-buffer\n",
			       field_oob);

		if (len >= 28) {
			uint32_t ucode_size = ((const uint32_t *)(buf + 20))[0];
			uint32_t arr_off    = ((const uint32_t *)(buf + 24))[0];
			uint64_t end = (uint64_t)arr_off + (uint64_t)ucode_size;
			printf("  CALLER memcpy(data+%u, %u bytes): end-of-copy=%llu, "
			       "datasize=%zu => %s\n",
			       arr_off, ucode_size,
			       (unsigned long long)end, len,
			       end > len ? "OOB READ (crosses end of firmware)" : "in-bounds");
		}
	}
	printf("\n");
}

int main(void)
{
	uint8_t buf[256];

	printf("DF-1946 harness: amdgpu_ucode_validate missing size/bounds checks\n");
	printf("Reference: sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260\n");
	printf("sizeof(struct common_firmware_header) = %zu\n\n",
	       sizeof(struct common_firmware_header));

	/* --- Case 1: 4-byte firmware, size_bytes==datasize. Passes validate(). */
	memset(buf, 0, sizeof(buf));
	((uint32_t *)buf)[0] = 4;   /* size_bytes = 4 == datasize */
	run_case("CASE 1: 4-byte fw, size_bytes=datasize=4 (no header at all)",
		 buf, 4);

	/* --- Case 2: 16-byte firmware, datasize==size_bytes==16. Passes; callers
	 *     dereference fields at offsets 20 and 24, both past datasize. */
	memset(buf, 0, sizeof(buf));
	((uint32_t *)buf)[0] = 16;  /* size_bytes = 16 */
	run_case("CASE 2: 16-byte fw, size_bytes=datasize=16 "
		 "(ucode_size_bytes field at off 20 is OOB)", buf, 16);

	/* --- Case 3: well-formed header size (32) but payload claims to begin
	 *     at offset 0x100 and span 0x100 bytes (i.e. far past the buffer).
	 *     datasize==size_bytes==32 -> validate PASSES; callers dereference
	 *     ucode_array_offset_bytes (0x100) and memcpy 0x100 bytes from
	 *     data+0x100, all OOB. */
	memset(buf, 0, sizeof(buf));
	((uint32_t *)buf)[0] = 32;   /* size_bytes   = 32  */
	((uint32_t *)buf)[1] = 32;   /* header_size  = 32  */
	((uint32_t *)buf)[5] = 0x100;/* ucode_size_bytes        = 256 */
	((uint32_t *)buf)[6] = 0x100;/* ucode_array_offset_bytes= 256 */
	run_case("CASE 3: 32-byte fw, claims 256-byte payload at offset 256 "
		 "(passes validate, OOB read in every caller)", buf, 32);

	/* --- Case 4: legitimately-formed 64-byte firmware with a real 32-byte
	 *     payload at offset 32. validate passes; fixed validate passes too.
	 *     Sanity baseline. */
	memset(buf, 0, sizeof(buf));
	((uint32_t *)buf)[0] = 64;   /* size_bytes   = 64  */
	((uint32_t *)buf)[1] = 32;   /* header_size  = 32  */
	((uint32_t *)buf)[5] = 32;   /* ucode_size_bytes        = 32 */
	((uint32_t *)buf)[6] = 32;   /* ucode_array_offset_bytes= 32 */
	run_case("CASE 4: well-formed 64-byte fw (baseline; both pass)", buf, 64);

	/* --- Case 5: integer-overflow attack: ucode_array_offset_bytes = 0xFFFFFF00
	 *     and ucode_size_bytes = 0x100, so a naive `arr_off + ucode_size`
	 *     in 32-bit wraps to 0x0 and the size check passes against datasize=32.
	 *     Vanilla validate does not check this; fixed version uses 64-bit math
	 *     so it rejects. */
	memset(buf, 0, sizeof(buf));
	((uint32_t *)buf)[0] = 32;       /* size_bytes = 32 == datasize */
	((uint32_t *)buf)[5] = 0x100;    /* ucode_size_bytes         */
	((uint32_t *)buf)[6] = 0xFFFFFF00; /* ucode_array_offset_bytes */
	run_case("CASE 5: 32-byte fw with wrap-around offset+size "
		 "(vanilla accepts, fixed rejects)", buf, 32);

	return 0;
}