DF-1307 / 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 | /* * DF-1307 harness โ kv_parse_power_table VCE clk_idx OOB heap read * * Reproduces the vulnerable access pattern of * sys/dev/drm/radeon/kv_dpm.c:2705-2707 * in userspace. * * The kernel function kv_parse_power_table() fills VCE power states at the * tail of the powerplay-table parse. At line 2705-2707: * * clock_array_index = rdev->pm.dpm.vce_states[i].clk_idx; // 6-bit 0..63 VBIOS * clock_info = (union pplib_clock_info *) * &clock_info_array->clockInfo[clock_array_index * clock_info_array->ucEntrySize]; * * `clk_idx` originates from the VBIOS VCE state record: * r600_dpm.c:1127-1128: * rdev->pm.dpm.vce_states[i].clk_idx = state_entry->ucClockInfoIndex & 0x3f; * It is used to byte-index clockInfo[] (a UCHAR flex[1] array, pptable.h:453) * with NO bounds check against clock_info_array->ucNumEntries. Contrast: the * main clock loop at kv_dpm.c:2683 DOES check * if (clock_array_index >= clock_info_array->ucNumEntries) continue; * The VCE loop at 2705 does NOT. * * With clk_idx up to 63 and ucEntrySize up to 255 (UCHAR), the byte offset * can reach 63*255 = 16065 past clockInfo[0] -> heap OOB read. * * Build: cc -O2 -Wall -o harness harness.c * Run: ./harness * * Proof strategy: place clockInfo[0] at the end of a page-backed region with * the next page unmapped; craft clk_idx=63 + ucEntrySize=255 so the read at * byte offset 63*255=16065 faults on the unmapped page. */ #include <stdio.h> #include <stdint.h> #include <stddef.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <setjmp.h> #include <unistd.h> #include <sys/mman.h> static sig_atomic_t got_fault = 0; static jmp_buf jb; static void fault_handler(int sig) { (void)sig; got_fault = sig; longjmp(jb, 1); } typedef uint8_t UCHAR; typedef uint16_t USHORT; /* _ClockInfoArray, pptable.h:446-454 */ typedef struct { UCHAR ucNumEntries; /* VBIOS: how many real clock entries */ UCHAR ucEntrySize; /* sizeof(union pplib_clock_info), up to 255 */ UCHAR clockInfo[1]; /* UCHAR flex array, byte-indexed */ } ClockInfoArray; /* Minimal slice of union pplib_clock_info (radeon_dpm.h) โ we read 4 bytes. */ typedef struct { USHORT usEngineClockLow; UCHAR ucEngineClockHigh; UCHAR pad; } clock_info_slice; /* Faithful replica of the vulnerable access (kv_dpm.c:2705-2707). * noinline + volatile inputs so gcc -O2 cannot constant-fold the array index * and elide the OOB load as UB (clockInfo[1] indexed past [0]). The real * kernel reads clk_idx and ucEntrySize at runtime from VBIOS, so gcc emits the * real address computation and load. */ __attribute__((noinline)) static UCHAR read_vce_clock_engine_high(volatile ClockInfoArray *arr, uint8_t clk_idx) { /* byte-index the flat UCHAR array โ NO bounds check vs ucNumEntries */ clock_info_slice *ci = (clock_info_slice *) &arr->clockInfo[(uint32_t)clk_idx * arr->ucEntrySize]; return ci->ucEngineClockHigh; /* the OOB read */ } int main(void) { struct sigaction sa; sa.sa_handler = fault_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); printf("DF-1307 kv_parse_power_table VCE clk_idx OOB read harness\n"); printf("sizeof(ClockInfoArray) overhead = %zu (clockInfo is UCHAR flex[1])\n", offsetof(ClockInfoArray, clockInfo) + 1); long pagesize = (long)getpagesize(); /* The OOB can reach 63*255 = 16065 bytes (~4 pages), so a single guard * page is not enough โ the access would skip over it into random memory. * Allocate enough pages and mprotect a large guard region after the data * page so ANY OOB access (up to ~20KB) faults. */ size_t npages = 6; /* page 0 = data; pages 1-5 = PROT_NONE guard (20480 B) */ size_t maplen = (size_t)pagesize * npages; uint8_t *base = mmap(NULL, maplen, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (base == MAP_FAILED) { perror("mmap"); return 1; } /* Guard: pages 1..(npages-1) become PROT_NONE -> any read faults. */ if (mprotect(base + pagesize, (size_t)pagesize * (npages - 1), PROT_NONE) != 0) { perror("mprotect"); return 1; } /* Place clockInfo[0] near the END of the data page so even a small OOB * crosses into the guard. */ size_t overhead = offsetof(ClockInfoArray, clockInfo); volatile ClockInfoArray *arr = (volatile ClockInfoArray *)(base + pagesize - overhead - 16); arr->ucNumEntries = 2; /* only 2 real entries */ arr->ucEntrySize = 255; /* max UCHAR โ amplifies the 6-bit clk_idx */ memset((void *)&arr->clockInfo[0], 0xBB, 16); /* Crafted VBIOS VCE state: clk_idx = 63 (max 6-bit value from * state_entry->ucClockInfoIndex & 0x3f at r600_dpm.c:1128). volatile so * gcc treats it as an unknown runtime value. */ volatile uint8_t clk_idx = 63; unsigned byte_off = (unsigned)clk_idx * arr->ucEntrySize; /* 63*255 = 16065 */ printf("ClockInfoArray @ %p (clockInfo[0] @ %p), ucNumEntries=%u, ucEntrySize=%u\n", (void *)arr, (void *)&arr->clockInfo[0], arr->ucNumEntries, arr->ucEntrySize); printf("VBIOS VCE clk_idx = %u (NO check vs ucNumEntries=%u)\n", clk_idx, arr->ucNumEntries); printf("access byte offset = %u * %u = %u (past the %u valid entries)\n", clk_idx, arr->ucEntrySize, byte_off, arr->ucNumEntries); if (setjmp(jb) == 0) { UCHAR v = read_vce_clock_engine_high(arr, clk_idx); printf("UNEXPECTED: read succeeded (ucEngineClockHigh=0x%02x) โ layout too loose\n", v); } else { printf("FAULT (signal %d): OOB read at clockInfo[%u] off the bios buffer\n", got_fault, byte_off); printf(" -> in-kernel equivalent: heap OOB read up to %u bytes past clockInfo[0]\n", byte_off); printf("RESULT: OOB read CONFIRMED at kv_dpm.c:2707 " "(VCE clk_idx byte-index with NO bounds check vs ucNumEntries)\n"); } munmap(base, maplen); return 0; } |