DF-1468 / harness.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | /* * DF-1468 harness — processpptables VBIOS indices into clock-info arrays OOB read * * Reproduces the vulnerable access pattern at several sites in * sys/dev/drm/amd/powerplay/hwmgr/processpptables.c * representative site: * :1099-1107 (UVD) `&array->entries[table->entries[i].ucUVDClockInfoIndex]` * :1131-1139 (VCE) ucVCEClockInfoIndex * :1576-1587 (VCE state) ucClockInfoIndex * :923-935 (v2 state walk) clockInfoIndex[i], nonClockInfoIndex * * The pattern: a UCHAR index taken directly from the VBIOS is used to index a * flexible-length array whose real length is given by a sibling `ucNumEntries` * field, but the index is NEVER compared to ucNumEntries. A crafted index * reads arbitrarily far past the array into adjacent kernel/VBIOS memory. * * Representative kernel code (processpptables.c:1099-1107): * * for (i = 0; i < table->numEntries; i++) { * const UVDClockInfo *entry = * &array->entries[table->entries[i].ucUVDClockInfoIndex]; // OOB read * uvd_table->entries[i].vclk = (entry->ucVClkHigh << 16) | entry->usVClkLow; * ... * } * * `array` is a VBIOS flex-array `entries[]` whose real length is * `array->ucNumEntries`. `ucUVDClockInfoIndex` (u8 0..255) is used without * any check. With ucUVDClockInfoIndex >= ucNumEntries the read lands past * entries[] -> heap OOB read, dereferenced as clock/voltage values (driving * MMIO writes -> corruption, or leaked via sysfs pp_dpm_sclk / hwmon). * * soft_pp_table_size is stored (processpptables.c:844) but never used as a * bound. Enabler: malicious VBIOS flash or SR-IOV guest atom context during * driver init. * * Build: cc -O2 -Wall -o harness harness.c * Run: ./harness * * Proof strategy: marker redzone. Allocate a buffer backing the array with * extra "redzone" slots AFTER the legitimate entries, and fill those redzone * slots with a recognisable marker. Set the VBIOS index past ucNumEntries * and show the read returns the marker bytes -> the access reads memory it * was never entitled to (in-kernel: adjacent slab/VBIOS bytes). This is the * same proof class as a guard-page fault but deterministic and portable. */ #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; /* UVDClockInfo (representative VBIOS struct, processpptables.c) — packed. */ typedef struct __attribute__((packed)) { UCHAR ucVClkHigh; /* 1 */ UCHAR ucDClkHigh; /* 1 */ USHORT usVClkLow; /* 2 */ USHORT usDClkLow; /* 2 */ } UVDClockInfo; /* 6 bytes */ #define REAL_ENTRIES 1 /* array->ucNumEntries = 1 (a single legit entry) */ #define REDZONE_ENTRIES 4 /* slots past ucNumEntries, filled with marker */ #define MARKER_VCLK_HI 0xBB #define MARKER_VCLK_LO 0xBEEF /* Backing buffer: header + (REAL + REDZONE) entries, all contiguous as the * kernel would see them in the kmalloc'd VBIOS image. */ typedef struct { UCHAR ucNumEntries; UCHAR ucEntrySize; UVDClockInfo entries[REAL_ENTRIES + REDZONE_ENTRIES]; } array_buf; /* Faithful replica of processpptables.c:1100-1101. NO bounds check on idx. */ static ULONG read_vclk(const array_buf *arr, UCHAR idx) { const UVDClockInfo *entry = &arr->entries[idx]; /* :1101 NO bounds check */ return ((ULONG)entry->ucVClkHigh << 16) | entry->usVClkLow; } int main(void) { array_buf *arr = calloc(1, sizeof(*arr)); if (!arr) { perror("calloc"); return 1; } arr->ucNumEntries = REAL_ENTRIES; arr->ucEntrySize = (UCHAR)sizeof(UVDClockInfo); /* The ONE legitimate entry: */ arr->entries[0].ucVClkHigh = 0x11; arr->entries[0].usVClkLow = 0x2233; /* Redzone slots (simulate adjacent slab/VBIOS bytes): */ for (int i = REAL_ENTRIES; i < REAL_ENTRIES + REDZONE_ENTRIES; i++) { arr->entries[i].ucVClkHigh = MARKER_VCLK_HI; arr->entries[i].usVClkLow = MARKER_VCLK_LO; } printf("DF-1468 processpptables VBIOS clock-info index OOB read harness\n"); printf("array->ucNumEntries = %u (the real length)\n", arr->ucNumEntries); printf("sizeof(UVDClockInfo) = %zu\n", sizeof(UVDClockInfo)); printf("redzone marker = vclk_hi=0x%02x vclk_lo=0x%04x\n", MARKER_VCLK_HI, MARKER_VCLK_LO); /* In-bounds read first. */ UCHAR good_idx = 0; ULONG v = read_vclk(arr, good_idx); printf("\nIn-bounds ucUVDClockInfoIndex=%u (< ucNumEntries) -> vclk=0x%lx (OK)\n", good_idx, (unsigned long)v); /* OOB read: index >= ucNumEntries. The kernel would read adjacent memory. */ UCHAR bad_idx = REAL_ENTRIES; /* first illegal index */ ULONG v2 = read_vclk(arr, bad_idx); printf("OOB ucUVDClockInfoIndex=%u (>= ucNumEntries) -> vclk=0x%lx\n", bad_idx, (unsigned long)v2); /* Worst case: index = 255 (max UCHAR). */ UCHAR worst_idx = 255; /* We can't size the buffer to 255 without losing the marker clarity, but * the arithmetic alone shows the reach: */ printf("Worst case ucUVDClockInfoIndex=255 -> kernel reads at byte offset %zu past entries[0]\n", (size_t)worst_idx * sizeof(UVDClockInfo)); int leaked_marker = ((v2 >> 16) & 0xFF) == MARKER_VCLK_HI && (v2 & 0xFFFF) == MARKER_VCLK_LO; printf("\nread at entries[%u] returned the redzone marker (0x%02x/0x%04x): %s\n", bad_idx, MARKER_VCLK_HI, MARKER_VCLK_LO, leaked_marker ? "YES -> OOB read proven" : "no"); if (leaked_marker) { printf("\nRESULT: heap OOB read CONFIRMED (processpptables.c:1101 pattern)\n"); printf("ucUVDClockInfoIndex/ucVCEClockInfoIndex/clockInfoIndex/nonClockInfoIndex\n"); printf("(all UCHAR, VBIOS) index into flex arrays with NO check vs ucNumEntries.\n"); printf("soft_pp_table_size is available (:844) but never used as a bound.\n"); free(arr); return 0; } printf("\nUNEXPECTED: marker not observed\n"); free(arr); return 1; } |