DF-4842 / df4842.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 | /* * df4842.c โ DF-4842: DragonFly's linux-kthread shim kthread_stop() frees a * struct thread it does not own (and cannot prove has exited) after a fixed * 1-second tsleep. * * sys/dev/drm/linux_kthread.c:89-101: * * int kthread_stop(struct task_struct *ts) * { * set_bit(KTHREAD_SHOULD_STOP, &ts->kt_flags); * kthread_unpark(ts); * wake_up_process(ts); * tsleep(kthread_stop, 0, "kstop", hz); <-- fixed 1s, proves nothing * lwkt_free_thread(ts->dfly_td); <-- frees a thread it does * return ts->kt_exitvalue; not own * } * * The DFly thread reclaims itself: when the kthread body returns, * cpu_set_thread_handler() runs kthread_exit() -> lwkt_exit() which parks * the td in gd->gd_freetd for the NEXT exiting thread on that cpu to * objcache_put() (lwkt_thread.c:1684-1744). kthread_stop's extra * lwkt_free_thread() therefore either: * * case A (body exited < 1s): the td is parked in gd_freetd[cpu] AND * objcache_put() into thread_cache by kthread_stop -> the same td * exists in two freelists; the next thread exit on that cpu puts * it into the cache a SECOND time -> two live threads can be * handed the same struct thread. * * case B (body still running at +1s, e.g. drm_sched_main() busy inside * run_job()): lwkt_free_thread() on a TDF_RUNNING thread -> * KKASSERT "lwkt_free_thread" fatal panic (or, if merely blocked, * a freed-but-scheduled live td). * * Reached in-tree from drm_sched_fini() (sys/dev/drm/scheduler/ * sched_main.c:652-656) โ every amdgpu ring teardown / driver unload โ and * from intel_engine_fini_breadcrumbs() (i915). * * test=1: case B โ deterministic 'panic: lwkt_free_thread' ~1s after the * sysctl (guest wedges at db>). * test=2: case A โ staged double-ownership: shows gd_freetd[cpu] == tdS * after fini, drives the second put, then scans the * thread cache from several cpus for tdS reappearing in * allocations it must not be part of. * * Root-only module test on the audit guest (no GPU required): * kldload amdgpu # pulls in drm.ko; scheduler symbols * kldload ./df4842.ko * sysctl hw.df4842_test=1 # case B: panic * sysctl hw.df4842_test=2 # case A: staged double-free evidence */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/systm.h> #include <sys/sysctl.h> #include <sys/lock.h> #include <sys/thread.h> #include <sys/thread2.h> #include <sys/globaldata.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 DF4842_NSCAN 16 struct dfjob { struct drm_sched_job base; }; struct dffence { struct dma_fence base; struct lock lock; }; static struct drm_sched_entity g_ent; static struct drm_sched_rq *g_rq_list[1]; static volatile int g_inrun; /* run_job entered (test 1) */ static int g_runcount; /* run_job calls */ /* case A staging */ static struct thread *g_tds; /* the sched thread's DFly td */ static int g_cpus; /* cpu the sched thread ran on */ static volatile int g_phase; /* handshake with pinned helpers */ static struct thread *g_probe_td, *g_sacr_td, *g_scanA, *g_scanB, *g_scanC; static int g_cpuF0, g_cpuF1; /* sysctl cpu before/after fini */ static volatile struct thread *g_hit_td[8]; static volatile int g_hits; static int g_t1_done; /* ------------------------------------------------------------------ */ /* fake dma-fence (never signalled) */ static const char *ff_drv(struct dma_fence *f) { return ("df4842"); } static const char *ff_tl(struct dma_fence *f) { return ("df4842f"); } 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); int t0 = ticks; lockinit(&f->lock, "df4842f", 0, LK_CANRECURSE); dma_fence_init(&f->base, &g_fence_ops, &f->lock, 0x48420000 + g_runcount, 1); g_runcount++; kprintf("[DF4842] run_job(job=%p): entered on td=%p cpu=%d - " "now BUSY-SPINNING 4s (no blocking: td stays TDF_RUNNING)\n", (void *)job, curthread, mycpu->gd_cpuid); g_inrun = 1; /* pure busy-wait: no tsleep, no yield, no preemption of our state. * NOTE: ticks must be read through a volatile qualifier or -O2 * register-caches the load and the loop never terminates. */ while (*(volatile int *)&ticks - t0 < 3 * hz) ; kprintf("[DF4842] run_job(job=%p): spin done (too late for " "kthread_stop's 1s window)\n", (void *)job); return (&f->base); } static void op_free_job(struct drm_sched_job *job) { kfree(job); } static void op_timedout_job(struct drm_sched_job *job) { kprintf("[DF4842] 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, }; /* ------------------------------------------------------------------ */ /* case A helpers (pinned lwkt threads; they NEVER exit once started) */ static void df4842_probe(void *arg) { struct thread *ftd = mycpu->gd_freetd; kprintf("[DF4842] probe: cpu=%d gd_freetd=%p tdS=%p -> %s\n", mycpu->gd_cpuid, ftd, (void *)g_tds, (ftd == g_tds) ? "== tdS STILL PARKED in gd_freetd (while kthread_stop already " "objcache_put() it into thread_cache: DOUBLE OWNERSHIP)" : "not tdS (unexpected)"); g_phase = 1; for (;;) tsleep(&df4842_probe, 0, "df4842p", 0); } static void df4842_sacr(void *arg) { /* * Returning makes the DFly kthread wrapper run kthread_exit() -> * lwkt_exit(), which on this cpu consumes gd_freetd == tdS and * objcache_put()s it into thread_cache a SECOND time. */ kprintf("[DF4842] sacr: exiting on cpu=%d - lwkt_exit() will now " "objcache_put(gd_freetd=tdS %p) AGAIN (2nd put)\n", mycpu->gd_cpuid, (void *)g_tds); } static void df4842_scan(void *arg) { struct thread *td; int i, myhits = 0; for (i = 0; i < DF4842_NSCAN; i++) { /* * Inert allocations (never scheduled): each pulls a struct * thread out of freetd/cache exactly like any real * kthread/ithread creation would. */ td = lwkt_alloc_thread(NULL, LWKT_THREAD_STACK, mycpu->gd_cpuid, 0); if (td == NULL) { kprintf("[DF4842] scan(cpu=%d): alloc %d FAILED\n", mycpu->gd_cpuid, i); break; } if (td == g_tds) { int n; kprintf("[DF4842] scan(cpu=%d) alloc %d: thread_cache " "handed out tdS %p โ SAME struct thread in a NEW " "allocation while %s\n", mycpu->gd_cpuid, i, (void *)td, (g_phase >= 3) ? "the 2nd put also sits in the cache" : "gd_freetd also held it"); n = atomic_fetchadd_int(&g_hits, 1); if (n < 8) g_hit_td[n] = td; myhits++; } } kprintf("[DF4842] scan(cpu=%d): done, %d tdS hits in %d allocs\n", mycpu->gd_cpuid, myhits, DF4842_NSCAN); atomic_add_int(&g_phase, 10); for (;;) tsleep(&df4842_scan, 0, "df4842s", 0); } /* ------------------------------------------------------------------ */ static int df4842_run_test1(void) { struct drm_gpu_scheduler *s; struct dfjob *j; int i; kprintf("[DF4842] test1 (case B): scheduler thread busy in run_job() " "when drm_sched_fini() runs\n"); s = kzalloc(sizeof(*s), GFP_KERNEL); drm_sched_init(s, &g_ops, 1, 0, MAX_SCHEDULE_TIMEOUT, "df4842b"); g_rq_list[0] = &s->sched_rq[DRM_SCHED_PRIORITY_NORMAL]; drm_sched_entity_init(&g_ent, g_rq_list, 1, NULL); kprintf("[DF4842] sched thread: task=%p dfly_td=%p cpu=%d\n", (void *)s->thread, (void *)s->thread->dfly_td, s->thread->dfly_td->td_gd->gd_cpuid); j = kzalloc(sizeof(*j), GFP_KERNEL); drm_sched_job_init(&j->base, &g_ent, NULL); drm_sched_entity_push_job(&j->base, &g_ent); for (i = 0; i < 100 && g_inrun == 0; i++) tsleep(&g_inrun, 0, "df4842w", 1); if (g_inrun == 0) { kprintf("[DF4842] run_job never entered?\n"); return (ETIMEDOUT); } kprintf("[DF4842] calling drm_sched_fini(): kthread_stop sets " "SHOULD_STOP, tsleeps its FIXED 1s, then lwkt_free_thread() on " "the still-TDF_RUNNING td %p -> expect KKASSERT panic\n", (void *)s->thread->dfly_td); g_t1_done = 1; drm_sched_fini(s); /* never returns on an unpatched kernel */ kprintf("[DF4842] drm_sched_fini RETURNED (no panic?!)\n"); kfree(s); g_t1_done = 2; return (0); } static int df4842_run_test2(void) { struct drm_gpu_scheduler *s; int i, rounds, hits; kprintf("[DF4842] test2 (case A): idle sched thread exits fast; " "td parked in gd_freetd AND objcache_put by kthread_stop\n"); for (rounds = 1; rounds <= 3; rounds++) { g_hits = 0; g_phase = 0; g_cpuF0 = mycpu->gd_cpuid; s = kzalloc(sizeof(*s), GFP_KERNEL); drm_sched_init(s, &g_ops, 1, 0, MAX_SCHEDULE_TIMEOUT, "df4842a"); g_tds = s->thread->dfly_td; g_cpus = g_tds->td_gd->gd_cpuid; kprintf("[DF4842] round %d: sched thread td=%p on cpu=%d; " "calling drm_sched_fini() (kthread_stop: 1s tsleep + " "lwkt_free_thread)\n", rounds, (void *)g_tds, g_cpus); drm_sched_fini(s); /* idle: body exits, td parked + put */ g_cpuF1 = mycpu->gd_cpuid; kfree(s); kprintf("[DF4842] drm_sched_fini returned; sysctl cpu was " "%d, now %d; staging gd_freetd proof on cpu %d\n", g_cpuF0, g_cpuF1, g_cpus); /* 1: prove the td is still parked in gd_freetd[cpuS] */ lwkt_create(df4842_probe, NULL, &g_probe_td, NULL, TDF_NOSTART, g_cpus, "df4842probe"); lwkt_schedule(g_probe_td); for (i = 0; i < 100 && g_phase == 0; i++) tsleep(&g_phase, 0, "df4842h", 1); if (g_phase == 0) { kprintf("[DF4842] probe never ran?!\n"); return (ETIMEDOUT); } g_phase = 2; /* 2: force the SECOND objcache_put via a thread exit on cpuS */ kprintf("[DF4842] launching sacrificial exit thread on " "cpu=%d\n", g_cpus); lwkt_create(df4842_sacr, NULL, &g_sacr_td, NULL, TDF_NOSTART, g_cpus, "df4842sacr"); lwkt_schedule(g_sacr_td); tsleep(&df4842_sacr, 0, "df4842x", 2); g_phase = 3; /* 3: scan the cache from the involved cpus */ lwkt_create(df4842_scan, NULL, &g_scanA, NULL, TDF_NOSTART, g_cpus, "df4842scanA"); lwkt_schedule(g_scanA); if (g_cpuF1 != g_cpus) { lwkt_create(df4842_scan, NULL, &g_scanB, NULL, TDF_NOSTART, g_cpuF1, "df4842scanB"); lwkt_schedule(g_scanB); } lwkt_create(df4842_scan, NULL, &g_scanC, NULL, TDF_NOSTART, mycpu->gd_cpuid, "df4842scanC"); lwkt_schedule(g_scanC); for (i = 0; i < 200 && g_phase < 32; i++) tsleep(&g_phase, 0, "df4842h", 1); hits = g_hits; kprintf("[DF4842] round %d result: %d allocation hit(s) for " "tdS %p after the double put\n", rounds, hits, (void *)g_tds); if (hits > 0) break; kprintf("[DF4842] (magazine locality miss; retrying with a " "fresh scheduler)\n"); } if (g_hits == 0) { kprintf("[DF4842] test2: double-put is proven by construction " "(gd_freetd==tdS probe + code path); cache dispense not " "observed in these rounds\n"); } else if (g_hits >= 2) { kprintf("[DF4842] *** DOUBLE DISPENSE CONFIRMED: %d " "allocations received the SAME struct thread %p โ two " "live threads would share one td ***\n", g_hits, (void *)g_tds); } return (0); } static int sysctl_df4842_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 (df4842_run_test1()); if (v == 2) return (df4842_run_test2()); return (0); } SYSCTL_PROC(_hw, OID_AUTO, df4842_test, CTLTYPE_INT | CTLFLAG_RW, 0, 0, sysctl_df4842_test, "I", "df4842 test selector (1=caseB panic, " "2=caseA double-put)"); static int df4842_init(struct module *mod, int what, void *arg) { switch (what) { case MOD_LOAD: kprintf("[DF4842] module loaded: sysctl hw.df4842_test=1 " "(case B panic) or =2 (case A double-put)\n"); break; default: break; } return (0); } static int df4842_uninit(struct module *mod, int what, void *arg) { switch (what) { case MOD_UNLOAD: default: break; } return (0); } static moduledata_t df4842_mod = { "df4842", df4842_init, df4842_uninit }; DECLARE_MODULE(df4842, df4842_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE); |