⬢ DragonFlyBSD Kernel Audit
DF-4625 / df4625.c
← back to finding ↓ download raw
/*
 * DF-4625 KLD — repro for:
 *   output_poll_execute()'s self-rearm structurally survives
 *   drm_kms_helper_poll_disable()/poll_fini() because the DragonFly
 *   drm workqueue shim's cancel_delayed_work_sync()
 *   (sys/dev/drm/linux_workqueue.c:415-424):
 *     1. cancels the callout BEFORE waiting out the running callback,
 *     2. the callback's schedule_delayed_work() (drm_probe_helper.c:654)
 *        then arms a FRESH timer nothing ever cancels,
 *     3. _cancel_work() (:348-385) breaks as soon as the work is idle --
 *        a pending timer is invisible to it,
 *     4. when the timer fires, queue_work() CLEARS work->canceled
 *        (linux_workqueue.c:135) and the full poll cycle runs again.
 *
 * This module compiles the shim's functions VERBATIM (build.sh seds them
 * out of the guest's /usr/src/sys/dev/drm/linux_workqueue.c) against the
 * REAL kernel primitives (lockmgr, lksleep, callout_reset/callout_cancel,
 * lwkt threads) and then races a self-rearming delayed_work exactly the
 * way drm_probe_helper.c's poll worker does.
 *
 * Build the FIXED variant with -DDF4625_FIXED (see build.sh) which only
 * replaces cancel_delayed_work_sync() with the converging loop.
 *
 * The module refuses to unload (its worker thread loops forever — the
 * same DF-1977 wart); reboot the guest afterwards.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/module.h>
#include <sys/malloc.h>
#include <sys/queue.h>
#include <sys/lock.h>
#include <sys/proc.h>
#include <sys/thread.h>
#include <sys/callout.h>
#include <machine/cpufunc.h>

MALLOC_DEFINE(M_DF4625, "df4625", "DF-4625 test allocations");

/* ---- struct environment, copied field-for-field from
 *      sys/dev/drm/include/linux/workqueue.h ---- */
struct workqueue_worker;

struct work_struct {
	STAILQ_ENTRY(work_struct) ws_entries;
	void	(*func)(struct work_struct *);
	struct workqueue_worker *worker;
	bool	on_queue;
	bool	running;
	bool	canceled;
};

struct workqueue_worker {
	STAILQ_HEAD(ws_list, work_struct) ws_list_head;
	struct thread *worker_thread;
	struct lock worker_lock;
};

struct workqueue_struct {
	bool	is_draining;
	int	num_workers;
	struct	workqueue_worker (*workers)[];
};

struct delayed_work {
	struct work_struct	work;
	struct callout		timer;
};

struct workqueue_struct *system_wq;

#define INIT_WORK(work, _func)			\
do {						\
	(work)->ws_entries.stqe_next = NULL;	\
	(work)->func = (_func);			\
	(work)->on_queue = false;		\
	(work)->running = false;		\
	(work)->canceled = false;		\
} while (0)

#define INIT_DELAYED_WORK(_work, _func)					\
do {									\
	INIT_WORK(&(_work)->work, _func);				\
	callout_init_mp(&(_work)->timer);				\
} while (0)

static inline struct delayed_work *
to_delayed_work(struct work_struct *work)
{
	return (void *)((char *)work - offsetof(struct delayed_work, work));
}

static inline bool
schedule_delayed_work(struct delayed_work *dwork, unsigned long delay)
{
	return queue_delayed_work(system_wq, dwork, delay);
}

/* ---- the shim, VERBATIM (df4625_extracted.inc, generated by build.sh):
 *      process_all_work, wq_worker_thread, queue_work, _delayed_work_fn,
 *      queue_delayed_work, _cancel_work, cancel_work_sync,
 *      cancel_delayed_work, cancel_delayed_work_sync ---- */
#ifdef DF4625_FIXED
#define cancel_delayed_work_sync	cancel_delayed_work_sync_orig
#include "df4625_extracted.inc"
#undef cancel_delayed_work_sync

/* Fixed variant: loop until the timer is dead AND the work is idle. */
static bool
cancel_delayed_work_sync(struct delayed_work *dwork)
{
	struct work_struct *work = &dwork->work;
	bool ret;

	do {
		ret = _cancel_work(work, true);
	} while (callout_cancel(&dwork->timer) ||
		 work->on_queue || work->running);
	return ret;
}
#else
#include "df4625_extracted.inc"
#endif

/* ---- the race scenario ---- */
static struct delayed_work poll_work;
static struct lock seq_lock;
static volatile int exec_count;
static volatile int cb_running;
static volatile int hold_release;
static volatile int cancel_done;
static volatile int final_stop;
static int rearm_ticks = 4;

/* emulates output_poll_execute's tail (drm_probe_helper.c:653-654):
 * ... if (repoll) schedule_delayed_work(delayed_work, DRM_OUTPUT_POLL_PERIOD); */
static void
poll_emul_fn(struct work_struct *work)
{
	int n = ++exec_count;

	kprintf("df4625: EXEC #%d entered\n", n);
	if (final_stop)
		return;

	/* hold, so the canceller passes callout_cancel() and parks inside
	 * _cancel_work()'s lksleep while this callback is running */
	lockmgr(&seq_lock, LK_EXCLUSIVE);
	cb_running = 1;
	wakeup(&cb_running);
	while (!hold_release)
		lksleep(&hold_release, &seq_lock, 0, "hold", 1);
	lockmgr(&seq_lock, LK_RELEASE);

	kprintf("df4625: EXEC #%d re-arming (%d ticks)\n", n, rearm_ticks);
	schedule_delayed_work(&poll_work, rearm_ticks);
}

static void
canceller_thread(void *arg)
{
	kprintf("df4625: canceller: cancel_delayed_work_sync() ENTER\n");
	cancel_delayed_work_sync(&poll_work);
	kprintf("df4625: canceller: cancel_delayed_work_sync() RETURN "
	    "(timer pending=%d on_queue=%d running=%d)\n",
	    callout_pending(&poll_work.timer), poll_work.work.on_queue,
	    poll_work.work.running);
	lockmgr(&seq_lock, LK_EXCLUSIVE);
	cancel_done = 1;
	wakeup(&cancel_done);
	lockmgr(&seq_lock, LK_RELEASE);
	lwkt_exit();
}

static void
wait_flag(volatile int *p, int ticks)
{
	int i;

	for (i = 0; i < ticks && !*p; i++)
		tsleep(p, 0, "df4625", 1);
}

static int
df4625_runtest(void)
{
	struct workqueue_worker *worker;
	thread_t td;
	int i, exec_at_cancel;

	system_wq = kmalloc(sizeof(struct workqueue_struct),
	    M_DF4625, M_WAITOK | M_ZERO);
	system_wq->num_workers = 1;
	system_wq->workers = kmalloc(sizeof(struct workqueue_worker),
	    M_DF4625, M_WAITOK | M_ZERO);
	worker = &(*system_wq->workers)[0];
	lockinit(&worker->worker_lock, "df4625w", 0, 0);
	STAILQ_INIT(&worker->ws_list_head);
	if (lwkt_create(wq_worker_thread, worker, &worker->worker_thread,
	    NULL, TDF_NOSTART, -1, "df4625w") != 0)
		return (ENOMEM);
	lwkt_setpri_initial(worker->worker_thread, TDPRI_KERN_DAEMON);
	lwkt_schedule(worker->worker_thread);

	lockinit(&seq_lock, "df4625s", 0, 0);
	INIT_DELAYED_WORK(&poll_work, poll_emul_fn);

	kprintf("df4625: === scenario: cancel_delayed_work_sync() called while "
	    "the self-rearming callback is executing ===\n");

	queue_delayed_work(system_wq, &poll_work, 1);
	wait_flag(&cb_running, hz);		/* callback now holding */

	lwkt_create(canceller_thread, NULL, &td, NULL, 0, -1, "df4625c");
	/* let the canceller pass callout_cancel() and enter _cancel_work() */
	tsleep(&exec_count, 0, "df4625m", 3);

	/* release the callback: it re-arms the timer and returns */
	lockmgr(&seq_lock, LK_EXCLUSIVE);
	hold_release = 1;
	wakeup(&hold_release);
	lockmgr(&seq_lock, LK_RELEASE);

	wait_flag(&cancel_done, 10 * hz);
	exec_at_cancel = exec_count;

	kprintf("df4625: cancel returned with exec_count=%d; watching %d "
	    "ticks for a post-cancel EXEC\n", exec_at_cancel, rearm_ticks * 3);
	for (i = 0; i < rearm_ticks * 3 + hz / 2; i++) {
		tsleep(&seq_lock, 0, "df4625p", 1);
		if (exec_count > exec_at_cancel)
			break;
	}

	if (exec_count > exec_at_cancel) {
		kprintf("df4625: DF-4625 %s: work executed AFTER "
		    "cancel_delayed_work_sync() returned (exec_count %d -> %d)\n",
#ifdef DF4625_FIXED
		    "UNEXPECTED (fixed build)",
#else
		    "REPRODUCED",
#endif
		    exec_at_cancel, exec_count);
	} else {
		kprintf("df4625: %s: no execution after cancel_delayed_work_sync() "
		    "returned within the window (exec_count=%d)\n",
#ifdef DF4625_FIXED
		    "FIX VALIDATED",
#else
		    "NOT reproduced",
#endif
		    exec_count);
	}

	/* teardown the chain; worker thread stays (module unloads never) */
	final_stop = 1;
	cancel_delayed_work(&poll_work);
	callout_cancel(&poll_work.timer);
	return (0);
}

static int
df4625_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		kprintf("df4625: module load (%s build)\n",
#ifdef DF4625_FIXED
		    "FIXED"
#else
		    "BASELINE"
#endif
		    );
		return (df4625_runtest());
	case MOD_UNLOAD:
		/* the verbatim wq_worker_thread loops forever (DF-1977);
		 * never unload so its code is never unmapped */
		return (EBUSY);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t df4625_mod = {
	"df4625",
	df4625_modevent,
	NULL
};

DECLARE_MODULE(df4625, df4625_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_VERSION(df4625, 1);