DragonFlyBSD Kernel Audit
DF-2870 / tqdead.c
← back to finding ↓ download raw
/*
 * DF-2870 PoC: taskqueue_free() terminate lost-wakeup deadlock.
 *
 * taskqueue_thread_loop() (subr_taskqueue.c:617-619) parks in
 * TQ_SLEEP(tq, tq) after taskqueue_run() WITHOUT re-checking TQ_FLAGS_ACTIVE.
 * taskqueue_terminate() (subr_taskqueue.c:141-147) delivers wakeup(tq)
 * after dropping the queue spinlock; if the worker is executing a task
 * function at that moment (not yet on the sleep queue) the wakeup is lost.
 * The worker later parks unconditionally, the terminator is asleep on
 * tq_threads, nobody is left to wake either -> permanent deadlock.
 *
 * Build: make
 * Run:   kldload ./tqdead.ko
 * Watch: kernel console shows "calling taskqueue_free"; free never returns;
 *        `ps -axH` shows tquafctrl sleeping in "taskqueue_terminate" and
 *        the worker sleeping in "tqthr" forever.
 */
#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 *dtq;
static struct task dt;
static volatile int d_entered;
static volatile int d_release;
static volatile int d_free_returned;
static struct thread *ctrl_td;

static void
d_func(void *arg, int pending)
{
	d_entered = 1;
	while (!d_release)
		tsleep(&d_release, 0, "dfn", 1);
}

static void
tqdead_ctrl(void *arg)
{
	int error;

	dtq = taskqueue_create("tqdead", M_WAITOK,
	    taskqueue_thread_enqueue, &dtq);
	error = taskqueue_start_threads(&dtq, 1, TDPRI_KERN_DAEMON, -1,
	    "tqdead");
	kprintf("tqdead: queue %p started (%d)\n", dtq, error);

	TASK_INIT(&dt, 0, d_func, NULL);
	taskqueue_enqueue(dtq, &dt);
	while (!d_entered)
		tsleep(&d_entered, 0, "dw1", 1);

	/*
	 * Worker dequeued dt (ta_pending == 0) and is now INSIDE ta_func,
	 * holding no lock and not on any sleep queue for ident `tq`.
	 */
	kprintf("tqdead: task in-flight; calling taskqueue_free()...\n");
	taskqueue_free(dtq);

	/* NOT REACHED when the bug reproduces */
	d_free_returned = 1;
	kprintf("tqdead: taskqueue_free RETURNED (not reproduced)\n");
	wakeup(&d_free_returned);
	kthread_exit();
}

static void
tqdead_releaser(void *arg)
{
	int i;

	/* give free() time to park in taskqueue_terminate */
	for (i = 0; i < 10; i++) {
		if (d_free_returned)
			break;
		tsleep(&d_free_returned, 0, "drel1", hz / 2);
	}

	kprintf("tqdead: releasing in-flight task func\n");
	d_release = 1;

	/* worker func returns; worker parks in TQ_SLEEP(tq, tq) "tqthr"
	 * without re-checking TQ_FLAGS_ACTIVE -> sleeps forever. */
	for (i = 0; i < 10; i++) {
		if (d_free_returned)
			break;
		tsleep(&d_free_returned, 0, "drel2", hz);
	}

	if (d_free_returned) {
		kprintf("tqdead: RESULT: free returned - NOT REPRODUCED\n");
	} else {
		kprintf("tqdead: RESULT: DEADLOCK CONFIRMED - "
		    "taskqueue_free() stuck for 10+ seconds; ctrl_td=%p "
		    "(wchan %p), worker parked forever\n",
		    ctrl_td, ctrl_td->td_wchan);
	}
	/* leave the wedged threads for ps(1) inspection */
	kthread_exit();
}

static int
tqdead_ev(module_t mod, int what, void *arg)
{
	switch (what) {
	case MOD_LOAD:
		kthread_create(tqdead_ctrl, NULL, &ctrl_td, "tqdeadctrl");
		kthread_create(tqdead_releaser, NULL, NULL, "tqdeadrel");
		return (0);
	case MOD_UNLOAD:
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

DEV_MODULE(tqdead, tqdead_ev, NULL);