โฌข DragonFlyBSD Kernel Audit
DF-1299 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-1299 harness โ€” bios_parser.c VBIOS flex-array OOB reads
 *
 * Reproduces the vulnerable access pattern of
 *   sys/dev/drm/amd/display/dc/bios/bios_parser.c
 *   get_bios_object() at :1990-1998
 * in userspace.
 *
 * The kernel pattern (confirmed in source):
 *   1. GET_IMAGE(ATOM_OBJECT_TABLE, offset) expands to
 *      bios_get_image(bp, offset, sizeof(ATOM_OBJECT_TABLE))
 *      -> bios_parser_helper.c:40 validates ONLY
 *         `offset + sizeof(ATOM_OBJECT_TABLE) < bios_size`,
 *         i.e. header(4) + asObjects[1](8) = 12 bytes.
 *   2. Then bios_parser.c:1994 does
 *        for (i=0; i < tbl->ucNumberOfObjects; i++)
 *            ... le16_to_cpu(tbl->asObjects[i].usObjectID) ...
 *      with NO re-validation that offset+4+8*i+8 <= bios_size.
 *      ucNumberOfObjects is a VBIOS-controlled u8 (0..255), so asObjects[1..254]
 *      reads up to ~2032 bytes past the validated single-element extent โ€” and
 *      off the end of the kmalloc'd bios buffer when the table sits near the
 *      bios image tail.
 *
 * Sibling sites sharing the same pattern (all in bios_parser.c):
 *   - get_device_tag:354         (asDeviceTag[device_tag_index], bound=ucNumberOfDevice)
 *   - get_ss_info_v3_1:675       (tbl[i], bound=usStructureSize-derived count)
 *   - get_gpio_pin_info:1812     (asGPIO_Pin[i], bound=usStructureSize-derived count)
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 *
 * Proof strategy: place a crafted ATOM_OBJECT_TABLE at the very end of a
 * page-backed "bios image" such that asObjects[1] would fall on an unmapped
 * page -> the OOB read faults (SIGSEGV). If GET_IMAGE had validated the FULL
 * array extent, the loop would never have started.
 */

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>
#include <sys/mman.h>

static sig_atomic_t got_fault = 0;
static jmp_buf jb;

static void fault_handler(int sig) { (void)sig; got_fault = sig; longjmp(jb, 1); }

/* Mirror of the relevant ATOM types (little-endian on x86). */
typedef uint8_t  UCHAR;
typedef uint16_t USHORT;

typedef struct {                 /* ATOM_OBJECT, atombios.h:4536 */
    USHORT usObjectID;
    USHORT usSrcDstTableOffset;
    USHORT usRecordOffset;
    USHORT usReserved;
} ATOM_OBJECT;

typedef struct {                 /* ATOM_OBJECT_TABLE, atombios.h:4544 */
    UCHAR  ucNumberOfObjects;
    UCHAR  ucPadding[3];
    ATOM_OBJECT asObjects[1];    /* flex array of 1 โ€” the root cause */
} ATOM_OBJECT_TABLE;

#define ATOM_OBJECT_TABLE_SIZE  sizeof(ATOM_OBJECT_TABLE)  /* = 12 */

/* Faithful replica of bios_get_image (bios_parser_helper.c:36-44). */
static uint8_t *bios_get_image(uint8_t *bios, uint32_t bios_size,
                               uint32_t offset, uint32_t size)
{
    if (bios && offset + size < bios_size)
        return bios + offset;
    else
        return NULL;
}
#define GET_IMAGE(bios, bios_size, type, offset) \
    ((type *) bios_get_image((bios), (bios_size), (offset), sizeof(type)))

/* Faithful replica of get_bios_object's loop body (bios_parser.c:1994-1998).
 * Returns the number of objects actually accessed before we stop (match or end). */
static int get_bios_object_walk(uint8_t *bios, uint32_t bios_size, uint32_t offset)
{
    ATOM_OBJECT_TABLE *tbl;
    uint32_t i;
    int accessed = 0;

    tbl = GET_IMAGE(bios, bios_size, ATOM_OBJECT_TABLE, offset);
    if (!tbl) {
        printf("  GET_IMAGE returned NULL (offset+12 >= bios_size): access PREVENTED\n");
        return -1;
    }
    printf("  GET_IMAGE OK: validated only offset..offset+12 (sizeof=12)\n");
    printf("  tbl->ucNumberOfObjects = %u (VBIOS-controlled)\n", tbl->ucNumberOfObjects);

    /* bios_parser.c:1994 โ€” NO per-element bounds check against bios_size */
    for (i = 0; i < tbl->ucNumberOfObjects; i++) {
        /* This is the OOB read: tbl->asObjects[i].usObjectID.
         * On x86 le16_to_cpu is a noop, so the raw load is the fault site. */
        USHORT objid = tbl->asObjects[i].usObjectID;   /* <- OOB read here */
        (void)objid;
        accessed++;
        if (tbl->asObjects[i].usObjectID == 0xFFFF)    /* pretend "found" */
            break;
    }
    return accessed;
}

int main(void)
{
    struct sigaction sa;
    sa.sa_handler = fault_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS,  &sa, NULL);

    printf("DF-1299 bios_parser.c VBIOS flex-array OOB read harness\n");
    printf("sizeof(ATOM_OBJECT)=%zu  sizeof(ATOM_OBJECT_TABLE)=%zu (header+flex[1])\n",
           sizeof(ATOM_OBJECT), sizeof(ATOM_OBJECT_TABLE));

    /* Build a "bios image" at the END of an mmap'd page, with the next page
     * unmapped, so any access past the bios end faults. The bios holds a
     * crafted ATOM_OBJECT_TABLE whose ucNumberOfObjects forces a walk past
     * the single validated flex element. */
    long pagesize = (long)getpagesize();
    size_t maplen = (size_t)pagesize * 2;
    uint8_t *base = mmap(NULL, maplen, PROT_READ | PROT_WRITE,
                         MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (base == MAP_FAILED) { perror("mmap"); return 1; }
    /* Unmap the second page so reads past page1 fault. */
    munmap(base + pagesize, (size_t)pagesize);

    /* Put a tiny bios image at the tail of page1. The bios must be large
     * enough for GET_IMAGE to pass (offset + 12 < bios_size) but small enough
     * that asObjects[1] lands on the unmapped page. */
    uint32_t bios_size = 20;            /* just enough to pass GET_IMAGE */
    uint32_t offset_in_bios = 0;        /* table at start of bios image */
    uint8_t *bios = base + pagesize - bios_size;  /* bios tail-aligned to page end */

    /* Crafted ATOM_OBJECT_TABLE:
     *   ucNumberOfObjects = 255 (forces a long walk; only asObjects[0] is in-bounds)
     *   asObjects[0] = a real-looking object (not 0xFFFF so the loop continues) */
    ATOM_OBJECT_TABLE crafted;
    crafted.ucNumberOfObjects = 255;
    crafted.ucPadding[0] = crafted.ucPadding[1] = crafted.ucPadding[2] = 0;
    crafted.asObjects[0].usObjectID = 0x1000;   /* not 0xFFFF -> keep walking */
    crafted.asObjects[0].usSrcDstTableOffset = 0;
    crafted.asObjects[0].usRecordOffset = 0;
    crafted.asObjects[0].usReserved = 0;
    memcpy(bios + offset_in_bios, &crafted, sizeof(crafted));

    printf("bios image: %u bytes at page-tail %p (next page unmapped)\n",
           bios_size, (void *)bios);
    printf("object table at bios offset %u; asObjects[0] @ %p, asObjects[1] @ %p\n",
           offset_in_bios,
           (void *)&((ATOM_OBJECT_TABLE *)(bios+offset_in_bios))->asObjects[0],
           (void *)&((ATOM_OBJECT_TABLE *)(bios+offset_in_bios))->asObjects[1]);
    printf("GET_IMAGE validates offset+%zu < bios_size(%u) -> %s\n",
           ATOM_OBJECT_TABLE_SIZE, bios_size,
           (offset_in_bios + ATOM_OBJECT_TABLE_SIZE < bios_size) ? "PASS (bug)" : "FAIL");
    printf("walking ucNumberOfObjects=255 elements with NO per-element check...\n");

    if (setjmp(jb) == 0) {
        int n = get_bios_object_walk(bios, bios_size, offset_in_bios);
        if (n < 0) {
            printf("RESULT: access PREVENTED by GET_IMAGE (bios too short) โ€” benign\n");
        } else {
            printf("RESULT: walked %d objects WITHOUT faulting "
                   "(bios had room) โ€” retry with tighter layout\n", n);
        }
    } else {
        printf("FAULT (signal %d): OOB read off the end of the bios buffer\n",
               got_fault);
        printf("  -> in-kernel equivalent: kmalloc'd bios buffer OOB heap read\n");
        printf("RESULT: OOB read CONFIRMED at bios_parser.c:1994 "
               "(get_bios_object loop past GET_IMAGE-validated flex[1])\n");
    }

    munmap(base, (size_t)pagesize);
    return 0;
}