DF-1299 / 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 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | /* * DF-1299 harness โ bios_parser.c VBIOS flex-array OOB reads * * Reproduces the vulnerable access pattern of * sys/dev/drm/amd/display/dc/bios/bios_parser.c * get_bios_object() at :1990-1998 * in userspace. * * The kernel pattern (confirmed in source): * 1. GET_IMAGE(ATOM_OBJECT_TABLE, offset) expands to * bios_get_image(bp, offset, sizeof(ATOM_OBJECT_TABLE)) * -> bios_parser_helper.c:40 validates ONLY * `offset + sizeof(ATOM_OBJECT_TABLE) < bios_size`, * i.e. header(4) + asObjects[1](8) = 12 bytes. * 2. Then bios_parser.c:1994 does * for (i=0; i < tbl->ucNumberOfObjects; i++) * ... le16_to_cpu(tbl->asObjects[i].usObjectID) ... * with NO re-validation that offset+4+8*i+8 <= bios_size. * ucNumberOfObjects is a VBIOS-controlled u8 (0..255), so asObjects[1..254] * reads up to ~2032 bytes past the validated single-element extent โ and * off the end of the kmalloc'd bios buffer when the table sits near the * bios image tail. * * Sibling sites sharing the same pattern (all in bios_parser.c): * - get_device_tag:354 (asDeviceTag[device_tag_index], bound=ucNumberOfDevice) * - get_ss_info_v3_1:675 (tbl[i], bound=usStructureSize-derived count) * - get_gpio_pin_info:1812 (asGPIO_Pin[i], bound=usStructureSize-derived count) * * Build: cc -O2 -Wall -o harness harness.c * Run: ./harness * * Proof strategy: place a crafted ATOM_OBJECT_TABLE at the very end of a * page-backed "bios image" such that asObjects[1] would fall on an unmapped * page -> the OOB read faults (SIGSEGV). If GET_IMAGE had validated the FULL * array extent, the loop would never have started. */ #include <stdio.h> #include <stdint.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); } /* Mirror of the relevant ATOM types (little-endian on x86). */ typedef uint8_t UCHAR; typedef uint16_t USHORT; typedef struct { /* ATOM_OBJECT, atombios.h:4536 */ USHORT usObjectID; USHORT usSrcDstTableOffset; USHORT usRecordOffset; USHORT usReserved; } ATOM_OBJECT; typedef struct { /* ATOM_OBJECT_TABLE, atombios.h:4544 */ UCHAR ucNumberOfObjects; UCHAR ucPadding[3]; ATOM_OBJECT asObjects[1]; /* flex array of 1 โ the root cause */ } ATOM_OBJECT_TABLE; #define ATOM_OBJECT_TABLE_SIZE sizeof(ATOM_OBJECT_TABLE) /* = 12 */ /* Faithful replica of bios_get_image (bios_parser_helper.c:36-44). */ static uint8_t *bios_get_image(uint8_t *bios, uint32_t bios_size, uint32_t offset, uint32_t size) { if (bios && offset + size < bios_size) return bios + offset; else return NULL; } #define GET_IMAGE(bios, bios_size, type, offset) \ ((type *) bios_get_image((bios), (bios_size), (offset), sizeof(type))) /* Faithful replica of get_bios_object's loop body (bios_parser.c:1994-1998). * Returns the number of objects actually accessed before we stop (match or end). */ static int get_bios_object_walk(uint8_t *bios, uint32_t bios_size, uint32_t offset) { ATOM_OBJECT_TABLE *tbl; uint32_t i; int accessed = 0; tbl = GET_IMAGE(bios, bios_size, ATOM_OBJECT_TABLE, offset); if (!tbl) { printf(" GET_IMAGE returned NULL (offset+12 >= bios_size): access PREVENTED\n"); return -1; } printf(" GET_IMAGE OK: validated only offset..offset+12 (sizeof=12)\n"); printf(" tbl->ucNumberOfObjects = %u (VBIOS-controlled)\n", tbl->ucNumberOfObjects); /* bios_parser.c:1994 โ NO per-element bounds check against bios_size */ for (i = 0; i < tbl->ucNumberOfObjects; i++) { /* This is the OOB read: tbl->asObjects[i].usObjectID. * On x86 le16_to_cpu is a noop, so the raw load is the fault site. */ USHORT objid = tbl->asObjects[i].usObjectID; /* <- OOB read here */ (void)objid; accessed++; if (tbl->asObjects[i].usObjectID == 0xFFFF) /* pretend "found" */ break; } return accessed; } 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-1299 bios_parser.c VBIOS flex-array OOB read harness\n"); printf("sizeof(ATOM_OBJECT)=%zu sizeof(ATOM_OBJECT_TABLE)=%zu (header+flex[1])\n", sizeof(ATOM_OBJECT), sizeof(ATOM_OBJECT_TABLE)); /* Build a "bios image" at the END of an mmap'd page, with the next page * unmapped, so any access past the bios end faults. The bios holds a * crafted ATOM_OBJECT_TABLE whose ucNumberOfObjects forces a walk past * the single validated flex element. */ long pagesize = (long)getpagesize(); size_t maplen = (size_t)pagesize * 2; uint8_t *base = mmap(NULL, maplen, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (base == MAP_FAILED) { perror("mmap"); return 1; } /* Unmap the second page so reads past page1 fault. */ munmap(base + pagesize, (size_t)pagesize); /* Put a tiny bios image at the tail of page1. The bios must be large * enough for GET_IMAGE to pass (offset + 12 < bios_size) but small enough * that asObjects[1] lands on the unmapped page. */ uint32_t bios_size = 20; /* just enough to pass GET_IMAGE */ uint32_t offset_in_bios = 0; /* table at start of bios image */ uint8_t *bios = base + pagesize - bios_size; /* bios tail-aligned to page end */ /* Crafted ATOM_OBJECT_TABLE: * ucNumberOfObjects = 255 (forces a long walk; only asObjects[0] is in-bounds) * asObjects[0] = a real-looking object (not 0xFFFF so the loop continues) */ ATOM_OBJECT_TABLE crafted; crafted.ucNumberOfObjects = 255; crafted.ucPadding[0] = crafted.ucPadding[1] = crafted.ucPadding[2] = 0; crafted.asObjects[0].usObjectID = 0x1000; /* not 0xFFFF -> keep walking */ crafted.asObjects[0].usSrcDstTableOffset = 0; crafted.asObjects[0].usRecordOffset = 0; crafted.asObjects[0].usReserved = 0; memcpy(bios + offset_in_bios, &crafted, sizeof(crafted)); printf("bios image: %u bytes at page-tail %p (next page unmapped)\n", bios_size, (void *)bios); printf("object table at bios offset %u; asObjects[0] @ %p, asObjects[1] @ %p\n", offset_in_bios, (void *)&((ATOM_OBJECT_TABLE *)(bios+offset_in_bios))->asObjects[0], (void *)&((ATOM_OBJECT_TABLE *)(bios+offset_in_bios))->asObjects[1]); printf("GET_IMAGE validates offset+%zu < bios_size(%u) -> %s\n", ATOM_OBJECT_TABLE_SIZE, bios_size, (offset_in_bios + ATOM_OBJECT_TABLE_SIZE < bios_size) ? "PASS (bug)" : "FAIL"); printf("walking ucNumberOfObjects=255 elements with NO per-element check...\n"); if (setjmp(jb) == 0) { int n = get_bios_object_walk(bios, bios_size, offset_in_bios); if (n < 0) { printf("RESULT: access PREVENTED by GET_IMAGE (bios too short) โ benign\n"); } else { printf("RESULT: walked %d objects WITHOUT faulting " "(bios had room) โ retry with tighter layout\n", n); } } else { printf("FAULT (signal %d): OOB read off the end of the bios buffer\n", got_fault); printf(" -> in-kernel equivalent: kmalloc'd bios buffer OOB heap read\n"); printf("RESULT: OOB read CONFIRMED at bios_parser.c:1994 " "(get_bios_object loop past GET_IMAGE-validated flex[1])\n"); } munmap(base, (size_t)pagesize); return 0; } |