โฌข DragonFlyBSD Kernel Audit
DF-1469 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-1469 harness โ€” processpptables VBIOS entry-count trusted without validation
 *
 * Reproduces the vulnerable pattern across the table parsers in
 *   sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
 * representative site:
 *   :383-399  get_clock_voltage_dependency_table()
 * Other sites: :1089-1107 (uvd), :1122-1139 (vce), :1153-1167 (samu),
 * :1181-1195 (acp), :1379-1399 (cac_leakage), :1520-1537 (phase_shed).
 *
 * The pattern: the parser sizes the destination buffer by the VBIOS-supplied
 * UCHAR count AND iterates that many times reading `table->entries[i]` from
 * the VBIOS region. The count is never validated to actually fit inside the
 * firmware image (soft_pp_table_size is available at :844 but unused).
 *
 * Representative kernel code (processpptables.c:383-399):
 *
 *   table_size = sizeof(unsigned long) +
 *                sizeof(struct phm_clock_voltage_dependency_table) *
 *                table->ucNumEntries;          // trusted VBIOS count
 *   dep_table = kzalloc(table_size, GFP_KERNEL);
 *   dep_table->count = (unsigned long)table->ucNumEntries;
 *   for (i = 0; i < dep_table->count; i++) {
 *       dep_table->entries[i].clk = ... table->entries[i].ucClockHigh ...;  // OOB read
 *       dep_table->entries[i].v   = ... table->entries[i].usVoltage ...;
 *   }
 *
 * `table->entries[]` is a flex array in the VBIOS region. With ucNumEntries
 * inflated beyond what the image actually holds, the loop reads entries off
 * the end of the actual VBIOS data into adjacent kernel memory. The parsed
 * (corrupted) values are then used as clocks/voltages, driving MMIO writes
 * (corruption) and/or leaked via sysfs pp_dpm_sclk / hwmon.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 *
 * Proof strategy: marker redzone. The "real" VBIOS table holds REAL entries;
 * the attacker-inflated ucNumEntries makes the loop walk past them into a
 * marker-filled redzone (adjacent slab/VBIOS bytes). Show the parsed values
 * equal the marker -> the loop read memory beyond the legitimate table.
 */

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

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

#define REAL_ENTRIES    2     /* the genuine table has 2 entries */
#define REDZONE_ENTRIES 6     /* marker-filled slots past the real table */
#define MARKER_CLK      0xABCDEF
#define MARKER_VOLT     0x1234

/* ATOM_PPLIB_Clock_Voltage_Dependency_Record โ€” representative VBIOS entry. */
typedef struct __attribute__((packed)) {
    UCHAR  ucClockHigh;   /* 1 */
    USHORT usClockLow;    /* 2 */
    USHORT usVoltage;     /* 2 */
} dep_record;             /* 5 bytes */

/* ATOM_PPLIB_Clock_Voltage_Dependency_Table โ€” flex array. The buffer below
 * holds REAL + REDZONE entries to expose what the kernel reads off the end. */
typedef struct {
    UCHAR ucNumEntries;
    dep_record entries[REAL_ENTRIES + REDZONE_ENTRIES];
} dep_table_vbios;

/* Faithful replica of processpptables.c:391-399. The loop bound is whatever
 * ucNumEntries says, with NO check that it fits the image. */
static void parse_dep_table(ULONG *out_clks, ULONG *out_volts,
                            const dep_table_vbios *t, ULONG count)
{
    ULONG i;
    for (i = 0; i < count; i++) {
        out_clks[i]  = ((ULONG)t->entries[i].ucClockHigh << 16) |
                       t->entries[i].usClockLow;     /* :394-396 */
        out_volts[i] = t->entries[i].usVoltage;       /* :397-398 */
    }
}

int main(void)
{
    dep_table_vbios *t = calloc(1, sizeof(*t));
    ULONG *clks = calloc(REAL_ENTRIES + REDZONE_ENTRIES, sizeof(ULONG));
    ULONG *volts = calloc(REAL_ENTRIES + REDZONE_ENTRIES, sizeof(ULONG));
    if (!t || !clks || !volts) { perror("calloc"); return 1; }

    /* Real entries: */
    t->ucNumEntries = REAL_ENTRIES;
    for (int i = 0; i < REAL_ENTRIES; i++) {
        t->entries[i].ucClockHigh = 0x01;
        t->entries[i].usClockLow  = 0x1000 + i;
        t->entries[i].usVoltage   = 0x0800 + i;
    }
    /* Redzone (adjacent kernel/VBIOS bytes the attacker-reached loop will read): */
    for (int i = REAL_ENTRIES; i < REAL_ENTRIES + REDZONE_ENTRIES; i++) {
        t->entries[i].ucClockHigh = (MARKER_CLK >> 16) & 0xFF;
        t->entries[i].usClockLow  = MARKER_CLK & 0xFFFF;
        t->entries[i].usVoltage   = MARKER_VOLT;
    }

    printf("DF-1469 processpptables VBIOS entry-count OOB read harness\n");
    printf("real ucNumEntries        = %u (what the image actually holds)\n", REAL_ENTRIES);
    printf("sizeof(dep_record)       = %zu\n", sizeof(dep_record));
    printf("marker clk=0x%06x volt=0x%04x placed in redzone slots %d..%d\n",
           MARKER_CLK, MARKER_VOLT, REAL_ENTRIES, REAL_ENTRIES + REDZONE_ENTRIES - 1);

    /* In-bounds: attacker supplies the truthful count. */
    parse_dep_table(clks, volts, t, REAL_ENTRIES);
    printf("\nIn-bounds  ucNumEntries=%u -> clk[0]=0x%lx volt[0]=0x%lx (OK)\n",
           REAL_ENTRIES, (unsigned long)clks[0], (unsigned long)volts[0]);

    /* OOB: attacker inflates ucNumEntries to REAL + REDZONE. The loop walks
     * past the real entries into the redzone. */
    ULONG inflated = REAL_ENTRIES + REDZONE_ENTRIES;
    parse_dep_table(clks, volts, t, inflated);
    printf("OOB        ucNumEntries=%lu (inflated) -> clk[%u]=0x%lx volt[%u]=0x%lx\n",
           (unsigned long)inflated,
           REAL_ENTRIES, (unsigned long)clks[REAL_ENTRIES],
           REAL_ENTRIES, (unsigned long)volts[REAL_ENTRIES]);
    printf("                                  (expected marker clk=0x%06x volt=0x%04x)\n",
           MARKER_CLK, MARKER_VOLT);

    /* Worst case: UCHAR max = 255. */
    printf("Worst case ucNumEntries=255 -> loop reads %d entries, %zu bytes past entries[0]\n",
           255, (size_t)255 * sizeof(dep_record));

    int leaked = (clks[REAL_ENTRIES] == MARKER_CLK && volts[REAL_ENTRIES] == MARKER_VOLT);

    if (leaked) {
        printf("\nRESULT: heap OOB read CONFIRMED (processpptables.c:393 count loop)\n");
        printf("Every table parser trusts the VBIOS UCHAR count to size BOTH the\n");
        printf("destination kmalloc AND the source loop; soft_pp_table_size (:844)\n");
        printf("is available but never used as a bound. Inflated count -> OOB read\n");
        printf("of adjacent kernel memory, parsed as clocks/voltages.\n");
        free(t); free(clks); free(volts);
        return 0;
    }
    printf("\nUNEXPECTED: marker not observed\n");
    free(t); free(clks); free(volts);
    return 1;
}