DF-1927 / df1927_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 | /* * DF-1927 — UAF race in drm_sched_entity_fini via broken kthread_park * * Faithful pthread model of the concurrent access to a drm_sched_entity's * spsc_queue job_queue and last_scheduled fence between: * * (a) the scheduler kthread running drm_sched_entity_pop_job() * (sys/dev/drm/scheduler/sched_entity.c:430-456) * (b) the closer thread running drm_sched_entity_fini() * (sys/dev/drm/scheduler/sched_entity.c:263-292) * * The barrier separating them is the kthread_park/kthread_unpark pair at * sched_entity.c:277-278. In DragonFly's LinuxKPI shim (linux_kthread.c) * kthread_park() at line 104-110 does set_bit + wake_up_process + RETURN -- * it does NOT wait for the target thread to actually park, unlike upstream * Linux which blocks on wait_for_completion(&k->parked). * * MODEL: * - Scheduler thread runs drm_sched_main's loop: each iteration, optionally * parkme, then pop_job (peek + put last_scheduled + assign + pop). * - Closer thread runs entity_fini: peek + park + unpark + kill_jobs + put. * - A pthread barrier synchronizes the START of both threads so they begin * the race simultaneously. After that, they run freely. * * In the BUGGY build (default), kthread_park returns immediately (with a * sched_yield to model wake_up_process), so fini's kill_jobs + dma_fence_put * run concurrently with the scheduler's pop_job -> Race A (double-pop / * job UAF) and Race B (double dma_fence_put / fence UAF) fire. * * In the FIXED build (-DFIXED_KTHREAD_PARK), kthread_park blocks on a * condition variable until the scheduler has reached parkme -- the scheduler * therefore completes its current pop_job and parks at drm_sched_blocked * (sched_main.c:514) before fini can proceed. No concurrent access. * * BUILD: * cc -O2 -g -Wall -pthread -o df1927_race df1927_race.c * cc -O2 -g -Wall -pthread -DFIXED_KTHREAD_PARK -o df1927_race_fixed \ * df1927_race.c * * RUN: * ./df1927_race # expect: race fires in most iterations, exit 0 * ./df1927_race_fixed # expect: race NEVER fires, exit 0 */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdatomic.h> #include <stdint.h> #include <stdbool.h> #include <unistd.h> #include <pthread.h> #include <sched.h> #include <time.h> /* ------------------------------------------------------------------ */ /* spsc_queue (faithful copy of sys/dev/drm/include/drm/spsc_queue.h) */ /* ------------------------------------------------------------------ */ struct spsc_node { struct spsc_node *next; }; struct spsc_queue { struct spsc_node *head; struct spsc_node *tail; }; static inline void spsc_queue_init(struct spsc_queue *q) { q->head = NULL; q->tail = (struct spsc_node *)&q->head; } static inline struct spsc_node *spsc_queue_peek(struct spsc_queue *q) { return q->head; } static inline bool spsc_queue_push(struct spsc_queue *q, struct spsc_node *n) { struct spsc_node **slot = (struct spsc_node **)q->tail; n->next = NULL; *slot = n; q->tail = (struct spsc_node *)&n->next; return slot == &q->head; } static inline struct spsc_node *spsc_queue_pop(struct spsc_queue *q) { struct spsc_node *n = q->head; if (!n) return NULL; q->head = n->next; if (!q->head) q->tail = (struct spsc_node *)&q->head; return n; } /* ------------------------------------------------------------------ */ /* dma_fence refcount model */ /* ------------------------------------------------------------------ */ struct dma_fence { _Atomic int refcount; int id; _Atomic int freed; /* detects use-after-free */ }; static inline int dma_fence_put(struct dma_fence *f) { int prev = atomic_fetch_sub(&f->refcount, 1); if (prev == 1) atomic_store(&f->freed, 1); /* last ref -> freed */ return prev; } /* ------------------------------------------------------------------ */ /* drm_sched_job + entity */ /* ------------------------------------------------------------------ */ struct drm_sched_job { struct spsc_node queue_node; int id; _Atomic int freed; }; struct drm_sched_entity { struct spsc_queue job_queue; struct dma_fence *last_scheduled; }; /* ------------------------------------------------------------------ */ /* task_struct + kthread_park model */ /* ------------------------------------------------------------------ */ struct task_struct { _Atomic unsigned long kt_flags; pthread_mutex_t lock; pthread_cond_t parked_cv; pthread_cond_t unpark_cv; _Atomic int parked; }; #define KTHREAD_SHOULD_PARK 2 static inline void set_bit(unsigned long nr, _Atomic unsigned long *a) { atomic_fetch_or(a, (1UL << nr)); } static inline void clear_bit(unsigned long nr, _Atomic unsigned long *a) { atomic_fetch_and(a, ~(1UL << nr)); } static inline bool test_bit(unsigned long nr, _Atomic unsigned long *a) { return (atomic_load(a) >> nr) & 1UL; } #ifdef FIXED_KTHREAD_PARK /* FIXED: synchronous -- blocks until target reaches parkme (upstream Linux) */ static int kthread_park(struct task_struct *ts) { set_bit(KTHREAD_SHOULD_PARK, &ts->kt_flags); pthread_mutex_lock(&ts->lock); pthread_cond_broadcast(&ts->unpark_cv); while (!atomic_load(&ts->parked)) pthread_cond_wait(&ts->parked_cv, &ts->lock); pthread_mutex_unlock(&ts->lock); return 0; } static void kthread_unpark(struct task_struct *ts) { clear_bit(KTHREAD_SHOULD_PARK, &ts->kt_flags); pthread_mutex_lock(&ts->lock); atomic_store(&ts->parked, 0); pthread_cond_broadcast(&ts->unpark_cv); pthread_mutex_unlock(&ts->lock); } static void kthread_parkme(struct task_struct *ts) { if (!test_bit(KTHREAD_SHOULD_PARK, &ts->kt_flags)) return; pthread_mutex_lock(&ts->lock); while (test_bit(KTHREAD_SHOULD_PARK, &ts->kt_flags)) { atomic_store(&ts->parked, 1); pthread_cond_signal(&ts->parked_cv); pthread_cond_wait(&ts->unpark_cv, &ts->lock); } atomic_store(&ts->parked, 0); pthread_mutex_unlock(&ts->lock); } #else /* BUGGY: DFly master DEV linux_kthread.c:104-110 -- returns immediately */ static int kthread_park(struct task_struct *ts) { set_bit(KTHREAD_SHOULD_PARK, &ts->kt_flags); /* wake_up_process analogue: yield + brief sleep to give the target a * chance to run (modeling the wakeup IPI / lwkt_schedule). Critically, * we do NOT wait for the target to actually park. */ sched_yield(); struct timespec ts25 = { .tv_sec = 0, .tv_nsec = 25 * 1000 }; nanosleep(&ts25, NULL); return 0; } static void kthread_unpark(struct task_struct *ts) { clear_bit(KTHREAD_SHOULD_PARK, &ts->kt_flags); sched_yield(); } static void kthread_parkme(struct task_struct *ts) { if (!test_bit(KTHREAD_SHOULD_PARK, &ts->kt_flags)) return; /* DFly: lwkt_deschedule_self once. Modelled as a yield -- the bit * remains set but we don't block (matching the broken shim). We will * loop back to drm_sched_blocked on the next iteration and re-check. */ sched_yield(); } #endif /* ------------------------------------------------------------------ */ /* Race-detection counters (global) */ /* ------------------------------------------------------------------ */ static _Atomic int race_a_double_free = 0; /* spsc_queue double-pop -> job UAF */ static _Atomic int race_b_underflow = 0; /* dma_fence_put kref underflow -> fence UAF */ static _Atomic int race_b_use_after_free = 0;/* dma_fence_put on already-freed fence */ /* ------------------------------------------------------------------ */ /* State per race iteration */ /* ------------------------------------------------------------------ */ static struct task_struct g_thread; static struct drm_sched_entity g_entity; static pthread_barrier_t g_start_barrier; /* ------------------------------------------------------------------ */ /* Scheduler thread: drm_sched_main loop + pop_job */ /* ------------------------------------------------------------------ */ static void *scheduler_main(void *arg) { int iters = (int)(intptr_t)arg; pthread_barrier_wait(&g_start_barrier); for (int i = 0; i < iters; i++) { /* sched_main.c:512 drm_sched_blocked */ if (test_bit(KTHREAD_SHOULD_PARK, &g_thread.kt_flags)) kthread_parkme(&g_thread); /* sched_entity.c:435 peek */ struct drm_sched_job *sched_job = (struct drm_sched_job *)spsc_queue_peek(&g_entity.job_queue); if (!sched_job) { sched_yield(); continue; } /* sched_entity.c:451-452 put + assign last_scheduled. * Yield between the put and the pop to widen the race window. */ struct dma_fence *old = g_entity.last_scheduled; if (old) { /* entity may have been finalized */ int prev = dma_fence_put(old); if (prev <= 0) { atomic_fetch_add(&race_b_underflow, 1); if (atomic_load(&old->freed)) atomic_fetch_add(&race_b_use_after_free, 1); } } struct dma_fence *nf = malloc(sizeof(*nf)); atomic_init(&nf->refcount, 1); atomic_init(&nf->freed, 0); nf->id = sched_job->id; g_entity.last_scheduled = nf; sched_yield(); /* sched_entity.c:454 pop */ struct spsc_node *popped = spsc_queue_pop(&g_entity.job_queue); if (popped) { if (atomic_exchange(&sched_job->freed, 1) != 0) atomic_fetch_add(&race_a_double_free, 1); free(sched_job); } else { /* sched_job was peeked but pop returned NULL: it was concurrently * popped by kill_jobs. sched_job is stale -- UAF. */ if (atomic_load(&sched_job->freed)) atomic_fetch_add(&race_a_double_free, 1); } } return NULL; } /* ------------------------------------------------------------------ */ /* drm_sched_entity_kill_jobs (sched_entity.c:223) */ /* ------------------------------------------------------------------ */ static void kill_jobs(struct drm_sched_entity *entity) { struct drm_sched_job *job; while ((job = (struct drm_sched_job *)spsc_queue_pop(&entity->job_queue))) { if (atomic_exchange(&job->freed, 1) != 0) atomic_fetch_add(&race_a_double_free, 1); free(job); } } /* ------------------------------------------------------------------ */ /* drm_sched_entity_fini (sched_entity.c:263) */ /* ------------------------------------------------------------------ */ static void entity_fini(struct drm_sched_entity *entity) { if (spsc_queue_peek(&entity->job_queue)) { kthread_park(&g_thread); kthread_unpark(&g_thread); kill_jobs(entity); } struct dma_fence *f = entity->last_scheduled; int prev = dma_fence_put(f); if (prev <= 0) { atomic_fetch_add(&race_b_underflow, 1); if (atomic_load(&f->freed)) atomic_fetch_add(&race_b_use_after_free, 1); } entity->last_scheduled = NULL; } static void *fini_thread(void *arg) { (void)arg; pthread_barrier_wait(&g_start_barrier); entity_fini(&g_entity); return NULL; } /* ------------------------------------------------------------------ */ /* Race driver */ /* ------------------------------------------------------------------ */ static int run_one(void) { atomic_store(&race_a_double_free, 0); atomic_store(&race_b_underflow, 0); atomic_store(&race_b_use_after_free, 0); spsc_queue_init(&g_entity.job_queue); g_entity.last_scheduled = malloc(sizeof(struct dma_fence)); atomic_init(&g_entity.last_scheduled->refcount, 1); atomic_init(&g_entity.last_scheduled->freed, 0); g_entity.last_scheduled->id = -1; for (int i = 0; i < 4; i++) { struct drm_sched_job *job = malloc(sizeof(*job)); job->id = i; atomic_init(&job->freed, 0); spsc_queue_push(&g_entity.job_queue, &job->queue_node); } atomic_init(&g_thread.kt_flags, 0); atomic_init(&g_thread.parked, 0); pthread_mutex_init(&g_thread.lock, NULL); pthread_cond_init(&g_thread.parked_cv, NULL); pthread_cond_init(&g_thread.unpark_cv, NULL); pthread_barrier_init(&g_start_barrier, NULL, 2); pthread_t t_sched, t_fini; pthread_create(&t_sched, NULL, scheduler_main, (void *)(intptr_t)6); pthread_create(&t_fini, NULL, fini_thread, NULL); pthread_join(t_sched, NULL); pthread_join(t_fini, NULL); pthread_barrier_destroy(&g_start_barrier); int a = atomic_load(&race_a_double_free); int b = atomic_load(&race_b_underflow); int c = atomic_load(&race_b_use_after_free); if (a || b || c) return 1; return 0; } int main(void) { /* Pin both threads to one CPU to maximize interleaving (single CPU * round-robin = realistic model of an interrupt-driven preemption * between two threads on the same core). */ cpu_set_t cs; CPU_ZERO(&cs); CPU_SET(0, &cs); sched_setaffinity(0, sizeof(cs), &cs); int trips = 0; int iters = 100; for (int i = 0; i < iters; i++) { int rc = run_one(); if (rc) trips++; } int a_total = atomic_load(&race_a_double_free); int b_total = atomic_load(&race_b_underflow); int c_total = atomic_load(&race_b_use_after_free); #ifdef FIXED_KTHREAD_PARK const char *variant = "FIXED_KTHREAD_PARK (synchronous, matches upstream Linux)"; #else const char *variant = "BUGGY (DFly master DEV linux_kthread.c:104-110)"; #endif (void)a_total; (void)b_total; (void)c_total; printf("==== %s ====\n", variant); printf("race tripped in %d/%d iterations\n", trips, iters); printf(" Race A (spsc_queue double-pop / job UAF): %d events total\n", a_total); printf(" Race B (dma_fence_put kref underflow): %d events total\n", b_total); printf(" Race B' (dma_fence_put use-after-free): %d events total\n", c_total); #ifdef FIXED_KTHREAD_PARK /* Fixed variant: race should NEVER fire */ return (trips == 0 && a_total == 0 && b_total == 0 && c_total == 0) ? 0 : 2; #else /* Buggy variant: race SHOULD fire */ return (trips > 0) ? 0 : 2; #endif } |