โฌข DragonFlyBSD Kernel Audit
DF-1306 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-1306 harness โ€” kv_parse_power_table nonClockInfoIndex OOB heap read
 *
 * Reproduces the vulnerable access pattern of
 *   sys/dev/drm/radeon/kv_dpm.c:2668-2670
 * in userspace.
 *
 * The kernel function kv_parse_power_table() parses the powerplay tables from
 * the GPU VBIOS. At line 2668-2670:
 *
 *   non_clock_array_index = power_state->v2.nonClockInfoIndex;  // u8 0..255 VBIOS
 *   non_clock_info = (struct _ATOM_PPLIB_NONCLOCK_INFO *)
 *       &non_clock_info_array->nonClockInfo[non_clock_array_index];
 *
 * `non_clock_array_index` comes straight from the VBIOS (ATOM_PPLIB_STATE_V2.
 * nonClockInfoIndex, a u8). It is used to index nonClockInfo[] with NO bounds
 * check against non_clock_info_array->ucNumEntries. Contrast: the CLOCK path
 * at line 2683 DOES check `if (clock_array_index >= ucNumEntries) continue;`.
 *
 * nonClockInfo is flex[1] of 24-byte ATOM_PPLIB_NONCLOCK_INFO entries
 * (pptable.h:298-309,456-464). nonClockInfoIndex=255 reads at byte offset
 * 255*24 = 6120 past nonClockInfo[0] -> heap OOB read.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 *
 * Proof strategy: place nonClockInfo[0] at the very end of a page-backed
 * region with the next page unmapped; set nonClockInfoIndex=255 so the read
 * at nonClockInfo[255] faults on the unmapped page (in-kernel: heap OOB read
 * off the kmalloc'd bios buffer).
 */

#include <stdio.h>
#include <stdint.h>
#include <stddef.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); }

typedef uint8_t  UCHAR;
typedef uint16_t USHORT;
typedef uint32_t ULONG;

/* ATOM_PPLIB_NONCLOCK_INFO, pptable.h:298-309 โ€” 24 bytes */
typedef struct {
    USHORT usClassification;    /* 2 */
    UCHAR  ucMinTemperature;    /* 1 */
    UCHAR  ucMaxTemperature;    /* 1 */
    ULONG  ulCapsAndSettings;   /* 4 */
    UCHAR  ucRequiredPower;     /* 1 */
    USHORT usClassification2;   /* 2 */
    ULONG  ulVCLK;              /* 4 */
    ULONG  ulDCLK;              /* 4 */
    UCHAR  ucUnused[5];         /* 5  = 24 total */
} ATOM_PPLIB_NONCLOCK_INFO;

/* _NonClockInfoArray, pptable.h:456-464 */
typedef struct {
    UCHAR ucNumEntries;         /* VBIOS: how many real entries */
    UCHAR ucEntrySize;          /* sizeof(ATOM_PPLIB_NONCLOCK_INFO) = 24 */
    ATOM_PPLIB_NONCLOCK_INFO nonClockInfo[1];   /* flex[1] */
} NonClockInfoArray;

/* Faithful replica of the vulnerable access (kv_dpm.c:2668-2670). */
static ULONG read_nonclock_classification(NonClockInfoArray *arr,
                                          uint8_t non_clock_array_index)
{
    ATOM_PPLIB_NONCLOCK_INFO *info =
        &arr->nonClockInfo[non_clock_array_index];   /* :2670 NO bounds check */
    return info->ulCapsAndSettings;                   /* the OOB read */
}

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-1306 kv_parse_power_table nonClockInfoIndex OOB read harness\n");
    printf("sizeof(ATOM_PPLIB_NONCLOCK_INFO)=%zu  sizeof(NonClockInfoArray)=%zu\n",
           sizeof(ATOM_PPLIB_NONCLOCK_INFO), sizeof(NonClockInfoArray));

    /* Lay out a page with nonClockInfo[0] at the very end, next page unmapped. */
    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; }
    munmap(base + pagesize, (size_t)pagesize);   /* unmap page 2 */

    /* NonClockInfoArray at the tail of page 1, with only nonClockInfo[0]
     * fitting. Place the array so that nonClockInfo[0] ends exactly at the
     * page boundary. */
    size_t arr_overhead = offsetof(NonClockInfoArray, nonClockInfo); /* 2 bytes */
    size_t need = arr_overhead + sizeof(ATOM_PPLIB_NONCLOCK_INFO);   /* 2 + 24 */
    NonClockInfoArray *arr = (NonClockInfoArray *)(base + pagesize - need);
    arr->ucNumEntries = 1;         /* only 1 real entry (nonClockInfo[0]) */
    arr->ucEntrySize  = (UCHAR)sizeof(ATOM_PPLIB_NONCLOCK_INFO);
    memset(&arr->nonClockInfo[0], 0xAA, sizeof(ATOM_PPLIB_NONCLOCK_INFO));

    /* Crafted VBIOS power_state: nonClockInfoIndex = 255 (way past ucNumEntries=1).
     * Access nonClockInfo[255] = byte offset 255*24 = 6120 -> faults on unmapped page. */
    uint8_t non_clock_array_index = 255;

    printf("NonClockInfoArray @ %p (nonClockInfo[0] @ %p), ucNumEntries=%u\n",
           (void *)arr, (void *)&arr->nonClockInfo[0], arr->ucNumEntries);
    printf("VBIOS nonClockInfoIndex = %u  (NO check vs ucNumEntries=%u)\n",
           non_clock_array_index, arr->ucNumEntries);
    printf("access byte offset = %u * %zu = %u  (past the single valid entry)\n",
           non_clock_array_index, sizeof(ATOM_PPLIB_NONCLOCK_INFO),
           non_clock_array_index * (unsigned)sizeof(ATOM_PPLIB_NONCLOCK_INFO));

    if (setjmp(jb) == 0) {
        ULONG v = read_nonclock_classification(arr, non_clock_array_index);
        printf("UNEXPECTED: read succeeded (value=0x%08x) โ€” layout too loose\n", v);
    } else {
        printf("FAULT (signal %d): OOB read at nonClockInfo[%u] off the bios buffer\n",
               got_fault, non_clock_array_index);
        printf("  -> in-kernel equivalent: heap OOB read up to %u bytes past nonClockInfo[0]\n",
               non_clock_array_index * (unsigned)sizeof(ATOM_PPLIB_NONCLOCK_INFO));
        printf("RESULT: OOB read CONFIRMED at kv_dpm.c:2670 "
               "(nonClockInfoIndex used with NO bounds check vs ucNumEntries)\n");
    }

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