โฌข DragonFlyBSD Kernel Audit
DF-4001 / trigger_analysis.c
โ† back to finding โ†“ download raw
/*
 * DF-4001..DF-4006 guest demonstrator โ€” fiji_smumgr.c SMC-table population walks.
 *
 * Reproduces, in userspace, the exact index arithmetic of:
 *   fiji_populate_all_graphic_levels   (fiji_smumgr.c:1026-1093)  -> GraphicsLevel[8]
 *   fiji_populate_all_memory_levels    (fiji_smumgr.c:1238-1272)  -> MemoryLevel[4]
 *   fiji_program_memory_timing_parameters (fiji_smumgr.c:1537-1546) -> STACK MCArbDramTimingTable entries[8][4]
 *   fiji_populate_smc_vce_level        (fiji_smumgr.c:1436-1457)  -> VceLevel[8]
 *   fiji_populate_smc_uvd_level        (fiji_smumgr.c:1572-1601)  -> UvdLevel[8]
 *   fiji_populate_smc_acp_level        (fiji_smumgr.c:1475-1494)  -> AcpLevel[8]
 *   fiji_populate_cac_table            (fiji_smumgr.c:772-779)    -> BapmVddcVid{Lo,Hi}Sidd[16] + vddc_voltage_table.entries[32] read
 *   fiji_populate_clock_stretcher_data_table (fiji_smumgr.c:1706-1719,1792-1810) -> Sclk_voltageOffset[8]
 *   fiji_get_dependency_volt_by_clk    (fiji_smumgr.c:413)        -> entries[count].mvdd one-entry OOB heap read
 *
 * Struct sizes/offsets are the kernel's own: smu73.h / smu73_discrete.h are
 * compiled in verbatim (#pragma pack(1), self-contained).
 *
 * Counts provenance (all VBIOS/PowerPlay-table controlled, unclamped):
 *   process_pptables_v1_0.c: sclk/mclk dep count = ucNumEntries (u8, <=255)
 *   (get_sclk_voltage_dependency_table :436, get_mclk_... :389,
 *    get_mm_clock_voltage_table :708, get_vddc_lookup_table :176-178),
 *   smu7_hwmgr.c:783-810 dedups sclk/mclk into dpm_table.*.count (no clamp vs 8),
 *   fiji pinned on this path: hwmgr.c:86 default PP_TABLE_V1 (only CI/TOPAZ -> V0).
 */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>
#include <stddef.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "smu73.h"
#include "smu73_discrete.h"

static int waitpid_helper(int *st) { while (wait(st) < 0) {} return 0; }
#define REDZONE 4096
static unsigned long oob_writes, oob_bytes;

/* heap object with guard page-ish redzone: writes past obj[n] land in the redzone */
static void *rz_alloc(size_t sz, unsigned char **obj, const char **rz) {
    unsigned char *base = malloc(sz + REDZONE);
    memset(base, 0xAA, sz + REDZONE);       /* 0xAA pattern everywhere        */
    memset(base, 0x00, sz);                 /* object zeroed, redzone 0xAA     */
    *obj = base; *rz = (const char *)(base + sz);
    return base;
}
static long rz_scan(const unsigned char *base, size_t sz) {
    long n = 0;
    for (size_t k = sz; k < sz + REDZONE; k++) if (base[k] != 0xAA) n++;
    return n;
}

static void hdr(const char *s) { printf("\n=== %s ===\n", s); }

int main(void) {
    /* ---- attacker-chosen VBIOS counts (each a single PowerPlay-table byte) ---- */
    const uint32_t sclk_count = 12;   /* > SMU73_MAX_LEVELS_GRAPHICS (8)  */
    const uint32_t mclk_count = 6;    /* > SMU73_MAX_LEVELS_MEMORY (4)    */
    const uint32_t mm_count   = 20;   /* > SMU73_MAX_LEVELS_{VCE,UVD,ACP} (8) */
    const uint32_t vddc_lookup_count = 40; /* > SMU73_MAX_LEVELS_VDDC (16), > PP_ATOMCTRL_MAX_VOLTAGE_ENTRIES (32) */
    const uint32_t dep_sclk_raw = 12; /* raw vdd_dep_on_sclk->count       */

    printf("fiji_smumgr.c demonstrator โ€” kernel struct geometry (kernel headers):\n");
    printf("  sizeof(SMU73_Discrete_DpmTable)=%zu  GraphicsLevel@+%zu MemoryLevel@+%zu Sclk_voltageOffset@+%zu\n",
           sizeof(SMU73_Discrete_DpmTable),
           offsetof(SMU73_Discrete_DpmTable, GraphicsLevel),
           offsetof(SMU73_Discrete_DpmTable, MemoryLevel),
           offsetof(SMU73_Discrete_DpmTable, Sclk_voltageOffset));
    printf("  GraphicsLevel=%zuB MemoryLevel=%zuB Uvd=%zuB Ext=%zuB ArbEntry=%zuB ArbTable(stack)=%zuB\n",
           sizeof(SMU73_Discrete_GraphicsLevel), sizeof(SMU73_Discrete_MemoryLevel),
           sizeof(SMU73_Discrete_UvdLevel), sizeof(SMU73_Discrete_ExtClkLevel),
           sizeof(SMU73_Discrete_MCArbDramTimingTableEntry),
           sizeof(SMU73_Discrete_MCArbDramTimingTable));

    /* ============ DF-4001a: GraphicsLevel[8] heap walk (:1026) ============ */
    hdr("DF-4001a fiji_populate_all_graphic_levels -> GraphicsLevel[8] (heap, inside struct fiji_smumgr)");
    {
        const size_t n = SMU73_MAX_LEVELS_GRAPHICS;
        unsigned char *obj; const char *rz;
        rz_alloc(sizeof(SMU73_Discrete_GraphicsLevel) * n, &obj, &rz);
        for (uint32_t i = 0; i < sclk_count; i++) {           /* :1026 loop, no clamp */
            SMU73_Discrete_GraphicsLevel *lv = (SMU73_Discrete_GraphicsLevel *)obj + i;
            memset(lv, 0x5A, sizeof(*lv));                    /* populate writes whole level */
        }
        oob_writes = sclk_count > n ? sclk_count - n : 0;
        oob_bytes  = oob_writes * sizeof(SMU73_Discrete_GraphicsLevel);
        printf("  sclk_count=%u: %lu OOB level writes, %lu bytes past GraphicsLevel[8]\n",
               sclk_count, oob_writes, oob_bytes);
        printf("  redzone dirty bytes observed: %ld\n", rz_scan(obj, sizeof(SMU73_Discrete_GraphicsLevel) * n));
        printf("  levels[sclk_count-1].DisplayWatermark store at :1042 also OOB (count-1=%u > 7)\n", sclk_count - 1);
        printf("  with ucNumEntries=255: %u bytes past the array; DpmTable tail after GraphicsLevel is ~%zuB -> blows past smc_state_table and the whole fiji_smumgr kzalloc\n",
               (255 - 8) * (unsigned)sizeof(SMU73_Discrete_GraphicsLevel),
               sizeof(SMU73_Discrete_DpmTable) - offsetof(SMU73_Discrete_DpmTable, GraphicsLevel)
                   - SMU73_MAX_LEVELS_GRAPHICS * sizeof(SMU73_Discrete_GraphicsLevel));
        free(obj);
    }

    /* ============ DF-4001b: MemoryLevel[4] heap walk (:1238) ============ */
    hdr("DF-4001b fiji_populate_all_memory_levels -> MemoryLevel[4] (heap)");
    {
        const size_t n = SMU73_MAX_LEVELS_MEMORY;
        unsigned char *obj; const char *rz;
        rz_alloc(sizeof(SMU73_Discrete_MemoryLevel) * n, &obj, &rz);
        for (uint32_t i = 0; i < mclk_count; i++) {           /* :1238 loop, no clamp */
            SMU73_Discrete_MemoryLevel *lv = (SMU73_Discrete_MemoryLevel *)obj + i;
            memset(lv, 0x5A, sizeof(*lv));
        }
        printf("  mclk_count=%u (Fiji SMU73 bound is 4!): %u OOB writes, %lu bytes past MemoryLevel[4]\n",
               mclk_count,
               mclk_count > (uint32_t)n ? mclk_count - (uint32_t)n : 0,
               (unsigned long)((mclk_count > n ? mclk_count - n : 0) * sizeof(SMU73_Discrete_MemoryLevel)));
        printf("  redzone dirty bytes observed: %ld\n", rz_scan(obj, sizeof(SMU73_Discrete_MemoryLevel) * n));
        free(obj);
    }

    /* ============ DF-4001c: STACK MCArbDramTimingTable (:1533,:1537-1546) ============ */
    hdr("DF-4001c fiji_program_memory_timing_parameters -> SMU73_Discrete_MCArbDramTimingTable arb_regs (384B STACK object)");
    {
        /*
         * Exact stack replica: the 384-byte object followed by 256 bytes of
         * "caller frame" (as the kernel function's own frame would be).
         * Writes past the 384B table land in the caller-frame region โ€”
         * in the kernel that is saved registers / return address.
         */
        unsigned char frame[sizeof(SMU73_Discrete_MCArbDramTimingTable) + 256];
        SMU73_Discrete_MCArbDramTimingTable *arb_regs =
            (SMU73_Discrete_MCArbDramTimingTable *)(void *)frame;
        memset(frame, 0, sizeof frame);
        uint32_t rc = 0;
        for (uint32_t i = 0; i < sclk_count; i++) {           /* :1537 */
            for (uint32_t j = 0; j < mclk_count; j++) {       /* :1538 */
                /* fiji_populate_memory_timing_parameters writes the 12-byte entry */
                SMU73_Discrete_MCArbDramTimingTableEntry *e = &arb_regs->entries[i][j]; /* :1542 */
                memset(e, 0x5A, sizeof(*e));                  /* OOB for i*4+j >= 32 */
                rc++;
            }
        }
        unsigned long writes_oob = 0, max_lin = 0, smashed = 0;
        for (uint32_t i = 0; i < sclk_count; i++)
            for (uint32_t j = 0; j < mclk_count; j++) {
                unsigned long lin = (unsigned long)i * SMU__NUM_MCLK_DPM_LEVELS + j;
                if (lin >= 32) { writes_oob++; if (lin > max_lin) max_lin = lin; }
            }
        for (size_t k = sizeof *arb_regs; k < sizeof frame; k++)
            if (frame[k] == 0x5A) smashed++;                  /* bytes landing in caller frame */
        unsigned long last_off = (max_lin + 1) * sizeof(SMU73_Discrete_MCArbDramTimingTableEntry);
        printf("  sclk=%u mclk=%u: %u entry writes total, %lu PAST the 32-entry table (max linear entry %lu of 32)\n",
               sclk_count, mclk_count, rc, writes_oob, max_lin);
        printf("  deepest write offset %luB into a %zuB stack object -> %ld bytes into the caller frame (%lu frame bytes actually smashed here)\n",
               last_off, sizeof *arb_regs, (long)(last_off - sizeof *arb_regs), smashed);
        printf("  with ucNumEntries=255/255 the walk reaches offset %luB past the 384B object (kernel stack smash, return-address direction)\n",
               (255UL * 4 + 255) * sizeof(SMU73_Discrete_MCArbDramTimingTableEntry));
        printf("  (the earlier version of this demo smashed its OWN stack frame and died of SIGBUS โ€” that bus error was the bug demonstrating itself)\n");
    }

    /* ============ DF-4002: VCE/UVD/ACP ExtClkLevel walks ============ */
    hdr("DF-4002 fiji_populate_smc_{vce,acp,uvd}_level -> VceLevel[8]/AcpLevel[8]/UvdLevel[8] (heap)");
    {
        unsigned char *o1, *o2, *o3; const char *r;
        rz_alloc(sizeof(SMU73_Discrete_ExtClkLevel) * SMU73_MAX_LEVELS_VCE, &o1, &r);
        for (uint8_t c = 0; c < (uint8_t)mm_count; c++)       /* :1436 loop, VceLevelCount=(u8)mm_table->count */
            memset((SMU73_Discrete_ExtClkLevel *)o1 + c, 0x5A, sizeof(SMU73_Discrete_ExtClkLevel));
        printf("  VceLevel[8]:  mm_count=%u -> %u OOB writes, %u bytes past\n", mm_count,
               mm_count - SMU73_MAX_LEVELS_VCE, (mm_count - SMU73_MAX_LEVELS_VCE) * (unsigned)sizeof(SMU73_Discrete_ExtClkLevel));
        free(o1);

        rz_alloc(sizeof(SMU73_Discrete_ExtClkLevel) * SMU73_MAX_LEVELS_ACP, &o2, &r);
        for (uint8_t c = 0; c < (uint8_t)mm_count; c++)       /* :1475 */
            memset((SMU73_Discrete_ExtClkLevel *)o2 + c, 0x5A, sizeof(SMU73_Discrete_ExtClkLevel));
        printf("  AcpLevel[8]:  mm_count=%u -> %u OOB writes, %u bytes past\n", mm_count,
               mm_count - SMU73_MAX_LEVELS_ACP, (mm_count - SMU73_MAX_LEVELS_ACP) * (unsigned)sizeof(SMU73_Discrete_ExtClkLevel));
        free(o2);

        rz_alloc(sizeof(SMU73_Discrete_UvdLevel) * SMU73_MAX_LEVELS_UVD, &o3, &r);
        for (uint8_t c = 0; c < (uint8_t)mm_count; c++)       /* :1572 */
            memset((SMU73_Discrete_UvdLevel *)o3 + c, 0x5A, sizeof(SMU73_Discrete_UvdLevel));
        printf("  UvdLevel[8]:  mm_count=%u -> %u OOB writes, %u bytes past\n", mm_count,
               mm_count - SMU73_MAX_LEVELS_UVD, (mm_count - SMU73_MAX_LEVELS_UVD) * (unsigned)sizeof(SMU73_Discrete_UvdLevel));
        printf("  (followed in-struct by VceLevel/AcpLevel/SamuLevel/Ulv/Smio[32]; count 255 runs ~3KB past, beyond the fiji_smumgr allocation)\n");
        free(o3);
    }

    /* ============ DF-4003: BapmVddcVid{Lo,Hi}Sidd[16] + entries[32] read ============ */
    hdr("DF-4003 fiji_populate_cac_table -> BapmVddcVidLoSidd/HiSidd[16] write + vddc_voltage_table.entries[32] read");
    {
        unsigned char *obj; const char *rz;
        /* the two arrays are adjacent in DpmTable (:228-:230 HiSidd[16], LoSidd[16], HiSidd2[16]) */
        const size_t n = SMU73_MAX_LEVELS_VDDC;
        rz_alloc(n, &obj, &rz);                 /* LoSidd[16] replica */
        uint8_t  vddc_volt_entries[32];         /* pp_atomctrl_voltage_table.entries[PP_ATOMCTRL_MAX_VOLTAGE_ENTRIES=32] */
        memset(vddc_volt_entries, 0, sizeof vddc_volt_entries);
        unsigned oob_read = 0, oob_wr = 0;
        for (uint32_t c = 0; c < vddc_lookup_count; c++) {    /* :772 loop over lookup_table->count */
            if (c >= 32) oob_read++;                          /* :774 data->vddc_voltage_table.entries[c] */
            volatile uint8_t v = vddc_volt_entries[c < 32 ? c : 31]; (void)v;
            if (c < n) obj[c] = 0x5A; else oob_wr++;          /* :775/:777 writes */
        }
        printf("  vddc_lookup_count=%u: %u OOB writes past BapmVddcVidLoSidd[16] (and same for HiSidd), %u OOB reads of vddc_voltage_table.entries[32]\n",
               vddc_lookup_count, oob_wr, oob_read);
        printf("  count comes from get_vddc_lookup_table ->count = ucNumEntries (u8<=255) [process_pptables_v1_0.c:176]; with 255: 239B past each array, 223 reads past entries[32]\n");
        free(obj);
    }

    /* ============ DF-4004: Sclk_voltageOffset[8] walk (:1706-1719) ============ */
    hdr("DF-4004 fiji_populate_clock_stretcher_data_table -> Sclk_voltageOffset[8]");
    {
        unsigned char *obj; const char *rz;
        const size_t n = 8;
        rz_alloc(n, &obj, &rz);
        uint8_t volt_offset = 0;
        for (uint32_t i = 0; i < dep_sclk_raw; i++) {         /* :1706 loop over table_info->vdd_dep_on_sclk->count (raw!) */
            volt_offset = (uint8_t)(i + 1);                   /* computed cks offset (:1716) */
            obj[i] = volt_offset;                             /* :1718 Sclk_voltageOffset[i] = volt_offset; */
        }
        printf("  dep_sclk->count=%u (RAW count, not the dedup'd dpm count): writes at [8..%u] are OOB\n",
               dep_sclk_raw, dep_sclk_raw - 1);
        printf("  array sits at DpmTable+%zu of %zuB, followed by ClockStretcherDataTable/CKS_LOOKUPTable (struct tail); count 255 writes 247B past the array, off the end of smc_state_table\n",
               offsetof(SMU73_Discrete_DpmTable, Sclk_voltageOffset), sizeof(SMU73_Discrete_DpmTable));
        printf("  note: the walk (:1706) runs BEFORE the stretch_amount validation at :1731, so even a 'rejected' VBIOS value smears the table first\n");
        free(obj);
    }

    /* ============ DF-4005: entries[count].mvdd one-entry OOB heap read (:413) ============ */
    hdr("DF-4005 fiji_get_dependency_volt_by_clk fall-through reads dep_table->entries[i].mvdd with i == count");
    {
        /* phm_ppt_v1_clock_voltage_dependency_record: clk,vddc,vddci,mvdd,cks_* โ€” use the
           real 4-field head (20B packed) + tail to 32B total as in hwmgr.h */
#pragma pack(push,1)
        struct dep_rec { uint32_t clk, vddc, vddci; uint16_t mvdd; uint8_t cks_enable, cks_voffset; };
#pragma pack(pop)
        const uint32_t cnt = 3;
        unsigned char *base = malloc(cnt * sizeof(struct dep_rec) + 64);
        memset(base, 0, cnt * sizeof(struct dep_rec) + 64);
        /* poison the mvdd field of the phantom entries[count] record exactly
           like adjacent-slab garbage would appear */
        base[cnt * sizeof(struct dep_rec) + offsetof(struct dep_rec, mvdd)]     = 0x01;
        base[cnt * sizeof(struct dep_rec) + offsetof(struct dep_rec, mvdd) + 1] = 0x00;
        struct dep_rec *entries = (struct dep_rec *)base;
        /* clock > every entries[].clk -> loop falls through with i == count */
        uint32_t i = 0, clock = 0xFFFFFFFF;
        for (i = 0; i < cnt; i++) if (entries[i].clk >= clock) break;
        /* :399 in-bounds */ volatile uint32_t vddc = entries[i - 1].vddc; (void)vddc;
        /* :413 THE BUG: reads entries[count].mvdd โ€” 2 bytes past the allocation */
        volatile uint16_t mvdd_oob = entries[i].mvdd;
        printf("  count=%u, i=%u after fall-through; entries[i].mvdd reads bytes at +%zu, %zu bytes past the %u-entry heap object\n",
               cnt, i, (size_t)((unsigned char *)&entries[i].mvdd - base),
               (size_t)((unsigned char *)&entries[i].mvdd - base) - cnt * sizeof(struct dep_rec), cnt);
        printf("  value read = 0x%04x (nonzero -> *mvdd set from entries[i-1] (in-bounds); zero -> *mvdd left unset) โ€” kernel heap adjacent-slab bytes consumed as a branch condition\n", mvdd_oob);
        free(base);
    }

    /* ============ DF-4006: NULL mm_dep_table deref (:1433 etc.) ============ */
    hdr("DF-4006 fiji_populate_smc_vce_level with usMMDependencyTableOffset==0 -> mm_dep_table NULL -> mm_table->count deref");
    {
        struct phm_ppt_v1_information { void *mm_dep_table; } ti = { NULL };  /* process_pptables_v1_0.c:796 leaves NULL when offset==0 */
        void *mm_table = ti.mm_dep_table;
        printf("  mm_dep_table==NULL when PowerPlay table has usMMDependencyTableOffset==0 (parser keeps result=0 and continues)\n");
        printf("  fiji_init_smc_table -> fiji_populate_smc_vce_level:1433 'table->VceLevelCount = (uint8_t)(mm_table->count)'\n");
        fflush(stdout);
        /* prove the derec faults: child performs it, parent reports the trap */
        {
            pid_t pid = fork();
            if (pid == 0) {
                volatile uint32_t count = *(volatile uint32_t *)mm_table; (void)count;
                _exit(0);                    /* not reached */
            }
            int st = 0; waitpid_helper(&st);
            if (WIFSIGNALED(st))
                printf("  child took signal %d on mm_table->count โ€” in-kernel equivalent: page fault on NULL+offset during fiji_init_smc_table (driver attach / resume) => panic\n", WTERMSIG(st));
            else
                printf("  !! deref did not fault (unexpected)\n");
        }
    }
    printf("\nDEMONSTRATOR COMPLETE: all walks reproduced; counts are single VBIOS bytes (ucNumEntries) reaching the fixed SMC arrays unclamped.\n");
    return 0;
}