โฌข DragonFlyBSD Kernel Audit
DF-1251 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-1251 harness โ€” unbounded COMBIOS table-walking loops over-read rdev->bios
 * and feed heap-sourced values into WREG32 (GPU MMIO writes).
 *
 * sys/dev/drm/radeon/radeon_combios.c:
 *   combios_parse_mmio_table (:3005): while (RBIOS16(offset)) { ...
 *       addr = RBIOS16(offset) & 0x1fff;  offset += 2;
 *       val  = RBIOS32(offset);           offset += 4;   WREG32(addr, val); (:3021)
 *   combios_parse_pll_table  (:3084): while (RBIOS8(offset))  { ... WREG32_PLL(addr,val); (:3100)
 *
 * where (sys/dev/drm/radeon/radeon.h:2686-2688):
 *   #define RBIOS8(i)  (rdev->bios[i])
 *   #define RBIOS16(i) (RBIOS8(i) | (RBIOS8((i)+1) << 8))
 *   #define RBIOS32(i) ((RBIOS16(i)) | (RBIOS16((i)+2) << 16))
 *
 * The loops advance `offset` purely from BIOS content and have NO upper bound
 * against the BIOS allocation. A malicious ROM whose table never contains a
 * zero terminator within the BIOS allocation drives RBIOS16/RBIOS32 to read
 * PAST rdev->bios into adjacent kernel heap, and those heap-sourced addr/val
 * pairs are written to GPU MMIO via WREG32 โ€” arbitrary GPU register writes.
 *
 * Kernel trigger requires an AMD/ATI radeon GPU with a malicious Video BIOS
 * (e.g. hotplug PCIe card with crafted ROM) โ€” absent from this guest. This
 * harness replicates the mmio-table walk over a small BIOS slab whose table
 * has no in-bounds terminator, proving the loop reads past the allocation.
 */
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

static uint8_t *g_bios;
static size_t   g_bios_len;
static unsigned g_oob_reads = 0;
static unsigned g_mmio_writes = 0;

/* Verbatim radeon.h:2686-2688 semantics, with OOB accounting. */
static uint8_t  rbios8(uint16_t i)  { if ((size_t)i >= g_bios_len) g_oob_reads++; return g_bios[i]; }
static uint16_t rbios16(uint16_t i) { return rbios8(i) | (rbios8((uint16_t)(i+1)) << 8); }
static uint32_t rbios32(uint16_t i) { return rbios16(i) | (rbios16((uint16_t)(i+2)) << 16); }

static void wreg32(uint32_t addr, uint32_t val) { g_mmio_writes++; (void)addr; (void)val; }

/* Verbatim structure of combios_parse_mmio_table (radeon_combios.c:3005-3030). */
static void combios_parse_mmio_table(uint16_t offset) {
    if (!offset) return;
    while (rbios16(offset)) {                       /* :3010 โ€” NO bound on offset */
        uint16_t cmd  = (rbios16(offset) & 0xe000) >> 13;
        uint32_t addr =  rbios16(offset) & 0x1fff;
        offset += 2;
        switch (cmd) {
        case 0: {                                   /* :3018-3022 */
            uint32_t val = rbios32(offset); offset += 4;
            wreg32(addr, val);
            break;
        }
        default:                                    /* other cmds also advance + write */
            offset += 4; wreg32(addr, 0);
            break;
        }
        /* safety for the harness only โ€” kernel has none */
        if (offset > g_bios_len * 4) { printf("[harness cap hit]\n"); break; }
    }
}

int main(void) {
    g_bios_len = 512;                       /* small BIOS slab */
    g_bios = calloc(1, g_bios_len);

    /* Build a malicious mmio table starting at offset 0x40 that never
     * produces a zero RBIOS16 terminator inside the allocation: every word
     * is non-zero. */
    uint16_t table = 0x40;
    for (size_t i = table; i + 1 < g_bios_len; i += 2) {
        g_bios[i]     = 0x01;               /* cmd=0, addr=1, non-zero word */
        g_bios[i + 1] = 0x00;
    }
    /* Ensure the loop's terminating condition (a zero word) does NOT appear
     * inside the allocation. (calloc zeroed the tail; fill it.) */
    for (size_t i = 0; i < g_bios_len; i++) if (g_bios[i] == 0) g_bios[i] = 0x55;

    combios_parse_mmio_table(table);

    printf("DF-1251 combios_parse_mmio_table harness\n");
    printf("BIOS allocation = %zu bytes; table starts at 0x%x (no in-bounds terminator)\n",
           g_bios_len, table);
    printf("RBIOS8/16/32 reads past BIOS allocation (OOB): %u\n", g_oob_reads);
    printf("WREG32 GPU-MMIO writes with heap-sourced addr/val: %u\n", g_mmio_writes);

    if (g_oob_reads > 0) {
        printf("\nPRIMITIVE CONFIRMED: unbounded loop read %u words past rdev->bios into heap,\n",
               g_oob_reads);
        printf("and wrote %u (addr,val) pairs to GPU MMIO (WREG32). Bug is REAL.\n",
               g_mmio_writes);
        free(g_bios);
        return 0;
    }
    printf("\nUNEXPECTED: no OOB\n");
    free(g_bios);
    return 1;
}