DF-3573 / fwsize_bypass.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 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 | /* * DF-3573 trigger: legacy-firmware size-check bypass in cik_init_microcode() * * This is a userland transcription of the *exact* acceptance state machine of * sys/dev/drm/radeon/cik.c:cik_init_microcode() (DragonFly BSD tree, lines * 2049-2272, CHIP_BONAIRE / non-IGP path), followed by a simulation of the * legacy ucode load loops that consume the accepted firmware * (cik_cp_gfx_load_microcode cik.c:3960-3983, cik_cp_compute_load_microcode * cik.c:4312-4329, cik_rlc_resume cik.c:5979-6004; constants from * radeon_ucode.h:38-66). * * It exists because the audit guest is virtio-only (no CIK radeon hardware), * so the kernel path cannot run; this harness proves the control-flow bug * deterministically: a legacy firmware file with a WRONG (truncated) size is * ACCEPTED (err==0, kept in rdev->X_fw) whenever any later request_firmware() * succeeds and overwrites err -- and the legacy loaders then read * CIK_*_UCODE_SIZE*4 bytes out of the smaller allocation (heap OOB read). * * Build: cc -O -o fwsize_bypass fwsize_bypass.c (baseline, stock code) * cc -O -DFIXED -o fwsize_bypass_fixed ... (with the fix.diff applied) * Run: ./fwsize_bypass */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <err.h> /* ---- constants transcribed from sys/dev/drm/radeon/radeon_ucode.h ---- */ #define CIK_PFP_UCODE_SIZE 2144 /* radeon_ucode.h:38 */ #define CIK_ME_UCODE_SIZE 2144 /* radeon_ucode.h:39 */ #define CIK_CE_UCODE_SIZE 2144 /* radeon_ucode.h:40 */ #define CIK_MEC_UCODE_SIZE 4192 /* radeon_ucode.h:43 */ #define BONAIRE_RLC_UCODE_SIZE 2048 /* radeon_ucode.h:52 */ #define CIK_SDMA_UCODE_SIZE 1050 /* cik_sdma.c:33 */ #define BONAIRE_MC_UCODE_SIZE 7866 /* radeon_ucode.h:65 */ #define BONAIRE_MC2_UCODE_SIZE 7948 /* radeon_ucode.h:66 */ /* one firmware "file" on disk: name + actual size */ struct fw_file { const char *name; /* radeonkmsfw_bonaire_<x> (new lower-case path) */ size_t datasize; /* bytes actually present */ int exists; /* request_firmware() success if exists */ }; /* per-fw driver state after cik_init_microcode() */ struct fw_state { const char *var; /* rdev-><x>_fw */ size_t datasize; /* 0 == NULL/rejected */ int accepted; }; /* * cik_init_microcode() for CHIP_BONAIRE, new-style (lower-case) files ABSENT * so every block takes the legacy branch -- exactly like a stock system whose * /boot/kernel only has radeonkmsfw_BONAIRE_*.ko (uppercase legacy names). * This mirrors cik.c:2049-2272 statement for statement: * - err = -EINVAL after "Bogus length" WITHOUT goto out (all but pfp), * - err re-assigned by every subsequent request/validate, * - final `if (new_fw == 0) ... else if (new_fw < num_fw) ... else`. * `files` holds the scenario; `out` receives the installed fw table. */ static int cik_init_microcode_bonaire_legacy(struct fw_file files[], int nfiles, struct fw_state out[], int nout, const char **installed_names, int *ninstalled, const char **err_last_fw) { int new_fw = 0, err = 0, num_fw = 8; size_t pfp_req = CIK_PFP_UCODE_SIZE * 4, me_req = CIK_ME_UCODE_SIZE * 4; size_t ce_req = CIK_CE_UCODE_SIZE * 4, mec_req = CIK_MEC_UCODE_SIZE * 4; size_t rlc_req = BONAIRE_RLC_UCODE_SIZE * 4, sdma_req = CIK_SDMA_UCODE_SIZE * 4; size_t mc_req = BONAIRE_MC_UCODE_SIZE, mc2_req = BONAIRE_MC2_UCODE_SIZE; struct fw_file *f; int i; struct fw_file *find_fw(struct fw_file *, int, const char *); struct fw_file dummy = { NULL, 0, 0 }; #define FIND(var) (find_fw(files, nfiles, var)) (void)nfiles; (void)installed_names; (void)ninstalled; for (i = 0; i < nout; i++) memset(&out[i], 0, sizeof(out[0])); *ninstalled = 0; /* * Each stage models cik.c exactly: * err = request_firmware(...) <-- err = 0 ON SUCCESS (the clobber!) * if (err) goto out; * if (datasize != req) { ...; err = -22; [goto out;] } * Lower-case (new-style) modules are absent, so the fallback to the * legacy uppercase name succeeds and takes the legacy branch. */ #define STAGE(var, idx) do { \ f = FIND(var); if (!f) f = &dummy; \ err = f->exists ? 0 : -1; /* err = request_firmware() */ \ if (err) goto out; \ } while (0) #define BOGUS(var, req) do { \ if (f->datasize != (req)) { \ warnx("[" var "] Bogus length %zu (req %zu)", \ f->datasize, (size_t)(req)); \ err = -22; \ goto out; /* parity with cik.c:2060 (pfp) */ \ } \ } while (0) #define BOGUS_NOGOTO(var, req) do { \ if (f->datasize != (req)) { \ warnx("[" var "] Bogus length %zu (req %zu)", \ f->datasize, (size_t)(req)); \ err = -22; /* stock cik.c: NO goto out */ \ } \ } while (0) #define KEEP(idx, varname) do { \ out[(idx)].var = (varname); out[(idx)].datasize = f->datasize; \ out[(idx)].accepted = 1; \ } while (0) /* ---- pfp (cik.c:2049-2071): bogus length HAS goto out ---- */ STAGE("pfp", 0); BOGUS("pfp", pfp_req); KEEP(0, "pfp_fw"); /* ---- me (cik.c:2073-2094): bogus length, NO goto out ---- */ STAGE("me", 1); BOGUS_NOGOTO("me", me_req); #ifdef FIXED if (err == -22) goto out; /* fix.diff adds goto out */ #endif KEEP(1, "me_fw"); /* ---- ce (cik.c:2096-2117) ---- */ STAGE("ce", 2); BOGUS_NOGOTO("ce", ce_req); #ifdef FIXED if (err == -22) goto out; #endif KEEP(2, "ce_fw"); /* ---- mec (cik.c:2119-2140) ---- */ STAGE("mec", 3); BOGUS_NOGOTO("mec", mec_req); #ifdef FIXED if (err == -22) goto out; #endif KEEP(3, "mec_fw"); /* KAVERI-only mec2 skipped (BONAIRE) */ /* ---- rlc (cik.c:2157-2178) ---- */ STAGE("rlc", 4); BOGUS_NOGOTO("rlc", rlc_req); #ifdef FIXED if (err == -22) goto out; #endif KEEP(4, "rlc_fw"); /* ---- sdma (cik.c:2180-2201) ---- */ STAGE("sdma", 5); BOGUS_NOGOTO("sdma", sdma_req); #ifdef FIXED if (err == -22) goto out; #endif KEEP(5, "sdma_fw"); /* ---- mc (cik.c:2205-2232), non-IGP ---- */ STAGE("mc", 6); if (f->datasize != mc_req && f->datasize != mc2_req) { warnx("[mc] Bogus length %zu (req %zu|%zu)", f->datasize, mc_req, mc2_req); err = -22; #ifdef FIXED goto out; #endif } KEEP(6, "mc_fw"); /* ---- smc (cik.c:2234-2262); new_smc=false for this BONAIRE stepping */ STAGE("smc", 7); if (0) { /* size check elided: file is stock */ } KEEP(7, "smc_fw"); /* ---- final new_fw decision (cik.c:2265-2272) ---- */ if (new_fw == 0) { /* rdev->new_fw = false; err keeps whatever the LAST assignment left */ } else if (new_fw < num_fw) { warnx("ci_fw: mixing new and old firmware!"); err = -22; } else { /* rdev->new_fw = true */ } /* fall through to out: */ out: /* * cik.c:2275-2297: `out:` releases every firmware and NULLs the * pointers when err != 0 -- model that: nothing survives an error. */ if (err != 0) for (i = 0; i < 8; i++) { out[i].datasize = 0; out[i].accepted = 0; } for (i = 0; i < 8; i++) if (out[i].accepted) installed_names[(*ninstalled)++] = out[i].var; *err_last_fw = NULL; return err; #undef STAGE #undef BOGUS #undef BOGUS_NOGOTO #undef KEEP } struct fw_file * find_fw(struct fw_file *files, int n, const char *suffix) { char name[64]; int i; snprintf(name, sizeof(name), "radeonkmsfw_bonaire_%s", suffix); for (i = 0; i < n; i++) if (strcmp(files[i].name, name) == 0) return (&files[i]); return (NULL); } /* * Simulate the LEGACY load loops (rdev->new_fw == false) and report the * out-of-bounds span each accepted firmware would suffer: * gfx: cik.c:3966/3973/3980 -- CIK_{PFP,CE,ME}_UCODE_SIZE dwords * mec: cik.c:4318/4326 -- CIK_MEC_UCODE_SIZE dwords * rlc: cik.c:6001-6002 -- BONAIRE_RLC_UCODE_SIZE dwords * sdma: cik_sdma.c:501/508 -- CIK_SDMA_UCODE_SIZE dwords * mc: cik.c:1880 -- datasize/4 (SELF-BOUNDED, safe) * The loops index fw_data++ from rdev->X_fw->data -- reading * (CONSTANT*4 - datasize) bytes past the allocation when datasize < req. */ static void simulate_legacy_loaders(struct fw_state out[], int nout) { static const struct { const char *var; size_t dwords; const char *loop; } legacy[] = { { "pfp_fw", CIK_PFP_UCODE_SIZE, "cik.c:3966" }, { "ce_fw", CIK_CE_UCODE_SIZE, "cik.c:3973" }, { "me_fw", CIK_ME_UCODE_SIZE, "cik.c:3980" }, { "mec_fw", CIK_MEC_UCODE_SIZE, "cik.c:4318,4326" }, { "rlc_fw", BONAIRE_RLC_UCODE_SIZE, "cik.c:6001" }, { "sdma_fw", CIK_SDMA_UCODE_SIZE, "cik_sdma.c:501,508" }, }; int i, j; for (i = 0; i < nout; i++) { if (!out[i].accepted) continue; for (j = 0; j < (int)(sizeof(legacy)/sizeof(legacy[0])); j++) { if (strcmp(out[i].var, legacy[j].var) != 0) continue; size_t want = legacy[j].dwords * 4; if (out[i].datasize < want) { printf(" OOB READ: %s holds %zu bytes, %s loop " "reads %zu -> %zd bytes past allocation\n", out[i].var, out[i].datasize, legacy[j].loop, want, (ssize_t)(want - out[i].datasize)); } else { printf(" ok: %s (%zu bytes >= %zu)\n", out[i].var, out[i].datasize, want); } } /* mc_fw: legacy loader uses datasize/4 -- never OOB */ if (strcmp(out[i].var, "mc_fw") == 0) printf(" ok: mc_fw (legacy loader bounded by datasize, cik.c:1880)\n"); } } static void run_scenario(const char *title, struct fw_file files[], int nfiles) { struct fw_state out[8]; const char *names[8]; int ninstalled = 0, err, i; const char *last = NULL; printf("== %s ==\n", title); err = cik_init_microcode_bonaire_legacy(files, nfiles, out, 8, names, &ninstalled, &last); printf(" cik_init_microcode -> err=%d (%s), installed fw: ", err, err == 0 ? "ACCEPTED" : "rejected"); if (ninstalled == 0) printf("(none)"); for (i = 0; i < ninstalled; i++) { struct fw_state *s = NULL; int k; for (k = 0; k < 8; k++) if (out[k].accepted && strcmp(out[k].var, names[i])==0) s=&out[k]; printf("%s(%zuB)%s", names[i], s ? s->datasize : 0, i + 1 < ninstalled ? ", " : ""); } printf("\n"); if (err == 0 && ninstalled) simulate_legacy_loaders(out, 8); else if (err == 0) printf(" (no fw kept: all rejected)\n"); printf("\n"); } int main(void) { /* stock sizes for a real BONAIRE system (legacy uppercase modules) */ struct fw_file stock[] = { { "radeonkmsfw_bonaire_pfp", CIK_PFP_UCODE_SIZE * 4, 1 }, { "radeonkmsfw_bonaire_me", CIK_ME_UCODE_SIZE * 4, 1 }, { "radeonkmsfw_bonaire_ce", CIK_CE_UCODE_SIZE * 4, 1 }, { "radeonkmsfw_bonaire_mec", CIK_MEC_UCODE_SIZE * 4, 1 }, { "radeonkmsfw_bonaire_rlc", BONAIRE_RLC_UCODE_SIZE * 4, 1 }, { "radeonkmsfw_bonaire_sdma", CIK_SDMA_UCODE_SIZE * 4, 1 }, { "radeonkmsfw_bonaire_mc", BONAIRE_MC_UCODE_SIZE, 1 }, { "radeonkmsfw_bonaire_smc", 0x1FDEC, 1 }, }; /* S1: root installs a TRUNCATED legacy me firmware (e.g. bad copy, * interrupted install, or cross-ASIC mixup); everything else fine */ struct fw_file me_bad[8]; memcpy(me_bad, stock, sizeof(stock)); me_bad[1].datasize = 1024; /* != CIK_ME_UCODE_SIZE*4 */ /* S2 (control): truncated pfp -- pfp's bogus-length block HAS goto out */ struct fw_file pfp_bad[8]; memcpy(pfp_bad, stock, sizeof(stock)); pfp_bad[0].datasize = 512; /* S3 (control): all stock */ run_scenario("S3 control: all firmware stock", stock, 8); run_scenario("S2 control: truncated PFP (block has goto out)", pfp_bad, 8); run_scenario("S1: truncated legacy ME firmware", me_bad, 8); return 0; } |