DragonFlyBSD Kernel Audit
DF-1470 / harness.c
← back to finding ↓ download raw
/*
 * DF-1470 harness — processpptables VBIOS USHORT offsets never validated
 *
 * Reproduces the vulnerable pattern across ~50 sites in
 *   sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
 * representative site:
 *   :55-66  get_vce_table_offset() derefs powerplay_table + usExtendendedHeaderOffset
 *   :874-876 pp_tables_get_num_of_entries() derefs powerplay_table + usStateArrayOffset
 *   :917-918 (clock info array), :920-921 (non-clock info array), :60-65 (extended header)
 *
 * The pattern: a USHORT offset from the VBIOS is added to the powerplay_table
 * base pointer and immediately dereferenced as a typed struct pointer, with
 * NO check that the offset lies within [0, soft_pp_table_size). A single
 * malicious USHORT (e.g. 0xFFFF) makes the pointer land ~64 KB past the table
 * -> chained derefs cascade into arbitrary-read-then-corrupt.
 *
 * Representative kernel code (processpptables.c:55-66):
 *
 *   if (powerplay_table3->usExtendendedHeaderOffset > 0) {
 *       const ATOM_PPLIB_EXTENDEDHEADER *extended_header =
 *           (const ATOM_PPLIB_EXTENDEDHEADER *)
 *           (((unsigned long)powerplay_table3) +
 *            le16_to_cpu(powerplay_table3->usExtendendedHeaderOffset));  // OOB ptr
 *       if (le16_to_cpu(extended_header->usSize) >= ...)                 // OOB read #1
 *           vce_table_offset = le16_to_cpu(extended_header->usVCETableOffset); // OOB read #2
 *   }
 *
 * The extended_header pointer is computed from the untrusted offset and then
 * TWO fields are read from it (usSize, usVCETableOffset) -- both OOB.
 *
 * soft_pp_table_size is stored (processpptables.c:844) but never used to
 * validate any offset.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 *
 * Proof strategy: marker redzone. Allocate a buffer larger than the legit
 * powerplay table, fill the bytes past the table with a marker, and set
 * usExtendendedHeaderOffset to land the computed extended_header pointer in
 * that redzone. Show the deref returns the marker bytes -> the kernel would
 * have read memory beyond the powerplay table image.
 */

#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 TABLE_REAL_SIZE   16    /* legit powerplay table is 16 bytes */
#define REDZONE_SIZE      32    /* marker-filled bytes past the table */
#define MARKER_SIZE       0xBEEF
#define MARKER_VCE_OFF    0xCAFE

/* Minimal ATOM_PPLIB_EXTENDEDHEADER (the OOB-deref'd target). */
typedef struct __attribute__((packed)) {
    USHORT usSize;
    USHORT usVCETableOffset;
} ext_header_replica;

/* Minimal powerplay_table3 (header + the malicious offset). */
typedef struct __attribute__((packed)) {
    UCHAR  ucTableFormatRevision;
    UCHAR  ucTableContentRevision;
    ULONG  usStructureSize;
    USHORT filler[3];
    USHORT usExtendendedHeaderOffset;
} pp_table_replica;

#define SIZE_OF_ATOM_PPLIB_EXTENDEDHEADER_V2  8

/* Faithful replica of processpptables.c:55-66. Returns the VCE table offset
 * extracted (forces both OOB derefs to execute). */
static USHORT get_vce_table_offset(const UCHAR *base, USHORT ext_hdr_off)
{
    USHORT vce_table_offset = 0;
    if (ext_hdr_off > 0) {
        const ext_header_replica *extended_header =
            (const ext_header_replica *)(base + ext_hdr_off);   /* :60-62 OOB ptr */
        if (extended_header->usSize >=                          /* :63 OOB read #1 */
            SIZE_OF_ATOM_PPLIB_EXTENDEDHEADER_V2)
            vce_table_offset = extended_header->usVCETableOffset; /* :65 OOB read #2 */
    }
    return vce_table_offset;
}

int main(void)
{
    /* Backing buffer: real table + redzone (what adjacent slab would contain). */
    UCHAR *buf = calloc(1, TABLE_REAL_SIZE + REDZONE_SIZE);
    if (!buf) { perror("calloc"); return 1; }

    pp_table_replica *ppt = (pp_table_replica *)buf;
    ppt->ucTableFormatRevision = 0;
    ppt->usExtendendedHeaderOffset = 0;   /* in-bounds: no ext header */

    /* Place a fake "extended header" in the REDZONE to expose the OOB read:
     * at offset TABLE_REAL_SIZE (just past the legit table). */
    ext_header_replica *fake = (ext_header_replica *)(buf + TABLE_REAL_SIZE);
    fake->usSize          = MARKER_SIZE;
    fake->usVCETableOffset = MARKER_VCE_OFF;

    printf("DF-1470 processpptables VBIOS USHORT offset OOB deref harness\n");
    printf("table real size         = %d bytes\n", TABLE_REAL_SIZE);
    printf("redzone size            = %d bytes (marker-filled)\n", REDZONE_SIZE);
    printf("marker ext_header at    = offset %d (just past the table)\n", TABLE_REAL_SIZE);
    printf("marker usSize           = 0x%04x\n", MARKER_SIZE);
    printf("marker usVCETableOffset = 0x%04x\n", MARKER_VCE_OFF);

    /* In-bounds: offset = 0 -> no deref. */
    USHORT ok = get_vce_table_offset(buf, 0);
    printf("\nIn-bounds  usExtendendedHeaderOffset=0 -> vce_offset=%u (OK)\n", ok);

    /* OOB: attacker sets offset = TABLE_REAL_SIZE (past the table). The kernel
     * computes base+offset into adjacent memory and derefs. */
    USHORT bad_off = TABLE_REAL_SIZE;
    USHORT bad = get_vce_table_offset(buf, bad_off);
    printf("OOB        usExtendendedHeaderOffset=%u (past table) -> vce_offset=0x%04x\n",
           bad_off, bad);
    printf("                                  (matches marker usVCETableOffset=0x%04x)\n",
           MARKER_VCE_OFF);

    /* Worst case: offset = 0xFFFF -> ~64KB past base. */
    printf("Worst case usExtendendedHeaderOffset=0xFFFF -> deref at base+65535,\n");
    printf("           ~64KB past the powerplay table -> cascading arbitrary read.\n");

    int leaked = (bad == MARKER_VCE_OFF);

    if (leaked) {
        printf("\nRESULT: heap OOB read CONFIRMED (processpptables.c:60-65 pattern)\n");
        printf("USHORT offsets (usExtendendedHeaderOffset/usStateArrayOffset/\n");
        printf("usClockInfoArrayOffset/usVCETableOffset/...) added to powerplay_table\n");
        printf("base and deref'd with NO check vs soft_pp_table_size. A single\n");
        printf("malicious offset cascades into arbitrary-read-then-corrupt chains.\n");
        free(buf);
        return 0;
    }
    printf("\nUNEXPECTED: marker not observed\n");
    free(buf);
    return 1;
}