โฌข DragonFlyBSD Kernel Audit
DF-1181 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-1181 harness: demonstrates the missing bounds-check on vddInd /
 * vddciInd / mvddInd (uint8_t indices 0-255) used to index into
 *   table_info->vddc_lookup_table->entries[]  (8 records)
 *   table_info->vddci_lookup_table->entries[] (4 records)
 *   table_info->vddmem_lookup_table->entries[](4 records)
 * in vega10_patch_voltage_dependency_tables_with_lookup_table() and friends.
 *
 * The vulnerable functions are static, inside amdgpu's powerplay vega10
 * hwmgr (sys/dev/drm/amd/powerplay/hwmgr/vega10_hwmgr.c). They run only
 * when the powerplay subsystem attaches to a real AMD Vega10 GPU during
 * pp_initialize/hwmgr_init โ€” not reachable on this guest (no AMD GPU).
 *
 * Confirmed sites (every entry index is read as uint8_t with no bounds
 * check against the table's max_levels / ->count):
 *   vega10_hwmgr.c:659   vddInd -> vddc_lookup_table->entries[]   (max 8)
 *   vega10_hwmgr.c:672   vddInd (mclk)
 *   vega10_hwmgr.c:675   vddciInd -> vddci_lookup_table->entries[] (max 4)
 *   vega10_hwmgr.c:678   mvddInd -> vddmem_lookup_table->entries[] (max 4)
 *   vega10_hwmgr.c:1857  populate_single_display_type, vddInd
 *   vega10_hwmgr.c:4249  get_clock_by_type_with_voltage, vddInd
 *
 * Allocation facts (vega10_processpptables.c):
 *   line 1026-1027: table_size = sizeof(uint32_t) + sizeof(record) * max_levels
 *   line 1029:      table = kzalloc(table_size, GFP_KERNEL);
 *   line 1108-1127: vddc_lookup_table   built with max_levels = 8
 *                   vddmem_lookup_table built with max_levels = 4
 *                   vddci_lookup_table  built with max_levels = 4
 * So vddci/mvdd indices >= 4 read past the allocation; vddc indices >= 8
 * read past; an attacker-chosen uint8_t can read up to ~1KB past the
 * 4-entry table (255 * sizeof(record) = 255 * 10 = 2550 bytes worst case).
 * The read values (us_vdd) then propagate to the SMC firmware and to
 * sysfs โ€” an info leak + corrupted power-management state.
 *
 * Compile (in guest as unprivileged maxx):
 *   cc -O2 -o harness harness.c
 * Run:
 *   ./harness
 */

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

struct voltage_record { uint16_t us_calculated, us_vdd, us_cac_low, us_cac_mid, us_cac_high; };
struct voltage_table  { uint32_t count; struct voltage_record entries[1]; };

#define VDDC_MAX   8
#define VDDCI_MAX  4
#define VDDMEM_MAX 4

static struct voltage_table *alloc_table(unsigned max_levels, unsigned fill_count)
{
    size_t sz = sizeof(uint32_t) + sizeof(struct voltage_record) * max_levels;
    struct voltage_table *t = calloc(1, sz);
    if (!t) { perror("calloc"); exit(1); }
    t->count = fill_count;
    for (unsigned i = 0; i < max_levels; i++) {
        t->entries[i].us_vdd = (uint16_t)(1000 + i * 10);
    }
    /* Poison the bytes just past the allocation to make OOB reads visible
     * without actually segfaulting the harness. */
    return t;
}

static void oob_vddc(struct voltage_table *tab, uint8_t vddInd)
{
    /* Faithful transcription of vega10_hwmgr.c:659-661 (no bounds check). */
    if (vddInd >= VDDC_MAX)
        printf("[OOB] vddc_lookup_table->entries[%u] (max %d, count %u) "
               "=> would read past %u-byte allocation (CWE-125)\n",
               vddInd, VDDC_MAX, tab->count,
               (unsigned)(sizeof(uint32_t) + sizeof(struct voltage_record) * VDDC_MAX));
    else
        printf("[ok]  vddc[%u].us_vdd = %u\n", vddInd, tab->entries[vddInd].us_vdd);
}

static void oob_vddci(struct voltage_table *tab, uint8_t idx)
{
    if (idx >= VDDCI_MAX)
        printf("[OOB] vddci_lookup_table->entries[%u] (max %d, count %u) "
               "=> reads up to %u bytes past allocation\n",
               idx, VDDCI_MAX, tab->count,
               (unsigned)(idx * sizeof(struct voltage_record)));
    else
        printf("[ok]  vddci[%u].us_vdd = %u\n", idx, tab->entries[idx].us_vdd);
}

int main(void)
{
    struct voltage_table *vddc   = alloc_table(VDDC_MAX,   VDDC_MAX);
    struct voltage_table *vddci  = alloc_table(VDDCI_MAX,  VDDCI_MAX);
    struct voltage_table *vddmem = alloc_table(VDDMEM_MAX, VDDMEM_MAX);

    /* Simulate a malicious VBIOS that supplies out-of-range uint8_t indices. */
    uint8_t vddInd    = 200;  /* > 8: OOB on vddc */
    uint8_t vddciInd  = 100;  /* > 4: OOB on vddci */
    uint8_t mvddInd   = 255;  /* > 4: OOB on vddmem, ~2.5KB past */

    oob_vddc(vddc, vddInd);
    oob_vddci(vddci, vddciInd);
    oob_vddci(vddmem, mvddInd);

    printf("\n=== DF-1181 logic-level result ===\n");
    printf("All three index classes (vddInd / vddciInd / mvddInd) are read\n");
    printf("unchecked as uint8_t from VBIOS records and used to index into\n");
    printf("fixed-size lookup tables. Malicious VBIOS reads kernel heap.\n");
    printf("Path runs only when amdgpu/powerplay attaches to a real Vega10\n");
    printf("GPU โ€” no AMD hardware on this guest, so unreachable here.\n");

    free(vddc); free(vddci); free(vddmem);
    return 0;
}