⬢ DragonFlyBSD Kernel Audit
DF-5183 / lwqdemo.c
← back to finding ↓ download raw
/*
 * lwqdemo.c - runtime PoC harness for the DragonFly drm LinuxKPI wait shim
 * (sys/dev/drm/linux_wait.c + sys/dev/drm/include/linux/wait.h).
 *
 * The module compiles the REAL, UNMODIFIED sys/dev/drm/linux_wait.c into the
 * KLD (the guest kernel does not include drm), and drives the real
 * wait_event_timeout() macro from <linux/wait.h> against it.
 *
 * Experiment 1 (DF-5182): lost wakeup in the wait_event family.
 *   The macro evaluates the condition BEFORE calling tsleep_interlock():
 *
 *       __wait_event_prefix(&wq, flags);      (set state)
 *       if (condition) break;                 (condition eval)
 *       tsleep_interlock(current, flags);     (interlock)
 *       tsleep(current, PINTERLOCKED|flags, ...);
 *
 *   A wake_up() landing between the condition evaluation and the interlock
 *   is dropped (nobody is interlocked yet), and autoremove_wake_function()
 *   unconditionally unlinks the wait entry (wake_up_process() always
 *   returns 1), so no later wake_up() can reach the stranded sleeper
 *   either: it burns its full timeout and wait_event_timeout() returns 0
 *   ("condition never became true") even though the condition WAS set
 *   true before the wake.
 *
 *   A pure timing race could not hit this in 1.67M rounds on this guest
 *   (an external waker's store->wakeup() delivery pipeline is slower than
 *   the waiter's condition->interlock path, see VERDICT.md).  The window
 *   is realistically entered when (a) the condition expression itself has
 *   width between reading the awaited state and returning - production
 *   drm conditions do (e.g. radeon_fence_wait_seq_timeout() evaluates
 *   radeon_fence_any_seq_signaled() which walks RADEON_NUM_BITS atomic64
 *   counters), or (b) the waiter is preempted between check and
 *   interlock.  The harness models (a) deterministically: the condition
 *   helper reads `gen' and then completes only when the waker pokes it,
 *   giving the waker a precisely controlled in-window slot:
 *
 *     WINDOWED round:
 *       waiter: gen_snap = gen; window_open = 1; spin until poke
 *       waker:  (sees window_open) gen++; wake_up()   [FIRST wake:
 *               delivered between the condition's state read and the
 *               interlock -> lost + entry auto-removed]
 *               poke = 1                      [release the condition]
 *               ... waiter interlocks, tsleeps, burns its FULL timeout
 *               ~100us later: wake_up() again [RESCUE wake: entry was
 *               unlinked, cannot reach the sleeper]
 *       outcome: ret == 0 (timeout) while gen != snap  => MISREPORT
 *
 *     CONTROL round (proves the harness works when the wake does NOT
 *     land in the window): the waker bumps + wakes only after the
 *     condition has completed and the waiter is interlocked-asleep;
 *     the waiter returns immediately with ret > 0.
 *
 * Experiment 2 (DF-5183): timeout parameter truncation.
 *   wait_on_bit()/wait_on_bit_timeout(..., MAX_SCHEDULE_TIMEOUT) pass
 *   (unsigned long)LONG_MAX into tsleep()'s `int timo', truncating to -1;
 *   _callout_setup_quick() clamps negative expiry to the next tick, so the
 *   "infinite" sleep actually returns EWOULDBLOCK after ~1 tick.
 *   Demonstrated directly at the tsleep() boundary with no waker.
 *
 * Results are printed with the "LWQ:" prefix; unload refused while running.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/proc.h>
#include <sys/kthread.h>
#include <sys/spinlock.h>
#include <sys/module.h>
#include <machine/cpufunc.h>

#include <linux/wait.h>
#include <linux/wait_bit.h>
#include <linux/sched.h>

MALLOC_DEFINE(M_LWQDEMO, "lwqdemo", "lwqdemo task_structs");

/*
 * local linux_task_alloc() - trimmed copy of sys/dev/drm/linux_sched.c:59
 * (the guest kernel has no drm, so we provide the allocator the `current'
 * macro lazily calls; kthreads have no proc/mm which is fine here).
 */
struct task_struct *
linux_task_alloc(struct thread *td)
{
	struct task_struct *task;

	task = kmalloc(sizeof(*task), M_LWQDEMO, M_WAITOK | M_ZERO);
	task->dfly_td = td;
	task->pid = -1;
	spin_init(&task->kt_spin, "lwqtsp");
	td->td_linux_task = task;

	return task;
}

static wait_queue_head_t	demo_wq;

static volatile u_int		gen;		/* condition generation */
static volatile u_int		rounds_started;
static volatile u_int		rounds_done;
static volatile u_int		fast_rounds;
static volatile u_int		misreports;
static volatile u_int		control_ok;
static volatile u_int		rescues_fired;
static volatile int		round_windowed;	/* current round mode */
static volatile int		window_open;	/* condition read done, not returned */
static volatile int		poke;		/* waker -> condition: complete */
static volatile int		stopping;
static volatile int		done;
static volatile u_int		gen_snap;	/* condition's view of gen */
static volatile u_int		waiter_g0;	/* this round's g0 */

#define TIMEOUT_TICKS	(2 * hz)
#define MAX_MISREPORTS	8
#define RUN_SECONDS	90

static __inline void
pause1(void)
{
	__asm __volatile("pause");
}

static __inline void
spin_until(uint64_t target)
{
	while (rdtsc() < target)
		pause1();
}

/*
 * The wait condition.  Models a condition with non-trivial width between
 * its read of the awaited state (gen) and its return, as production drm
 * conditions have (radeon_fence_any_seq_signaled, i915_sw_finish_query,
 * ...).  In WINDOWED rounds the waker is allowed into that slot.
 */
static int
cond_expr(void)
{
	gen_snap = gen;			/* the state read */
	if (round_windowed) {
		window_open = 1;
		while (!poke && !stopping)
			pause1();
		window_open = 0;
		poke = 0;
	}
	return (gen_snap != waiter_g0);
}

/*
 * ------------------------------------------------------------------
 * Experiment 1: waiter thread
 * ------------------------------------------------------------------
 */
static void
waiter_thread(void *arg)
{
	long ret;
	int start, dt, windowed;

	kprintf("LWQ: waiter started (tid %p)\n", curthread);
	while (!stopping) {
		windowed = (rounds_started & 1) == 0;	/* alternate */
		round_windowed = windowed;
		waiter_g0 = gen;
		rounds_started++;
		start = ticks;
		ret = wait_event_timeout(demo_wq, cond_expr(), TIMEOUT_TICKS);
		dt = ticks - start;
		rounds_done++;

		if (ret > 0) {
			fast_rounds++;
			if (!windowed)
				control_ok++;
			else
				kprintf("LWQ: windowed round returned fast "
				    "(unexpected - wake NOT lost?)\n");
		} else if (ret == 0) {
			if (gen != waiter_g0) {
				misreports++;
				kprintf("LWQ: MISREPORT #%u (windowed) "
				    "dt=%d ticks: wait_event_timeout "
				    "returned 0 (timeout) but condition "
				    "gen %u != %u is TRUE - the in-window "
				    "wake was lost and the rescue wake "
				    "could not find the auto-removed "
				    "entry\n",
				    misreports, dt, gen, waiter_g0);
			} else if (windowed) {
				kprintf("LWQ: windowed round true-timeout "
				    "(waker failed to bump?)\n");
			} else {
				kprintf("LWQ: NATURAL RACE HIT in a control "
				    "round (no widened condition)! "
				    "dt=%d\n", dt);
			}
		}
	}
	kprintf("LWQ: waiter exiting\n");
	wakeup(&done);
	kthread_exit();
}

/*
 * ------------------------------------------------------------------
 * Experiment 1: waker thread
 * ------------------------------------------------------------------
 */
static void
waker_thread(void *arg)
{
	u_int served;

	kprintf("LWQ: waker started (tid %p)\n", curthread);
	while (!stopping) {
		/* wait for a round to be in flight */
		while (rounds_done + 1 != rounds_started && !stopping)
			pause1();
		if (stopping)
			break;
		served = rounds_started;

		if (round_windowed) {
			/* wait until the waiter is INSIDE the condition
			 * window (state read done, not yet returned) */
			while (!window_open && !stopping)
				pause1();
			if (stopping)
				break;

			gen++;			/* condition becomes true */
			wake_up(&demo_wq);	/* FIRST (in-window) wake */

			/* let the waiter finish the condition and go to
			 * sleep (interlocked) */
			poke = 1;

			/* ~100 us: waiter now stranded asleep with its
			 * wait entry unlinked */
			spin_until(rdtsc() + 300000);
			rescues_fired++;
			wake_up(&demo_wq);	/* RESCUE wake - misses */

			/* round completes at full timeout */
		} else {
			/* CONTROL round: bump + wake AFTER the condition
			 * has completed and the waiter is interlocked and
			 * asleep - this is the path that works. */
			spin_until(rdtsc() + 20000);	/* ~7 us */
			gen++;
			wake_up(&demo_wq);
		}

		/* wait for the round to complete */
		while (rounds_done != served && !stopping)
			pause1();
	}
	kprintf("LWQ: waker exiting\n");
	wakeup(&done);
	kthread_exit();
}

/*
 * ------------------------------------------------------------------
 * Supervisor
 * ------------------------------------------------------------------
 */
static void
ctrl_thread(void *arg)
{
	int i;

	for (i = 0; i < RUN_SECONDS; i++) {
		tsleep(&ctrl_thread, 0, "lwqctl", hz);
		if (misreports >= MAX_MISREPORTS)
			break;
	}
	stopping = 1;
	poke = 1;
	gen++;
	wake_up(&demo_wq);
	tsleep(&ctrl_thread, 0, "lwqfin", TIMEOUT_TICKS + hz);

	kprintf("LWQ: ================= SUMMARY =================\n");
	kprintf("LWQ: rounds_done=%u fast=%u control_ok=%u "
	    "misreports=%u rescues_fired=%u\n",
	    rounds_done, fast_rounds, control_ok, misreports, rescues_fired);
	kprintf("LWQ: verdict: %s\n", misreports ?
	    "REPRODUCED - windowed rounds: in-window wake LOST + auto-removed "
	    "entry strands the sleeper; wait_event_timeout() reports timeout "
	    "although the condition was set true before the wake; rescue "
	    "wake cannot reach it; control rounds behave correctly" :
	    "no misreport observed this run");
	kprintf("LWQ: ===========================================\n");
	done = 1;
	wakeup(&done);
	kthread_exit();
}

/*
 * ------------------------------------------------------------------
 * Experiment 2 (DF-5183): MAX_SCHEDULE_TIMEOUT truncation
 * ------------------------------------------------------------------
 */
static volatile unsigned long f3_word = 1;

static void
f3_thread(void *arg)
{
	int start, dt, rv, timo;

	tsleep(&f3_thread, 0, "lwqf3a", hz);

	timo = (int)MAX_SCHEDULE_TIMEOUT;	/* what tsleep() receives */
	start = ticks;
	rv = tsleep((void *)&f3_word, 0, "lwqf3", timo);
	dt = ticks - start;
	kprintf("LWQ-F3: tsleep(timo=(int)MAX_SCHEDULE_TIMEOUT=%d) returned "
	    "rv=%d after %d ticks with NO waker - 'infinite' timeout actually "
	    "expires every tick (LONG_MAX truncates to int -1, callout clamps "
	    "to next tick)\n", timo, rv, dt);

	start = ticks;
	rv = wait_on_bit_timeout((unsigned long *)&f3_word, 0,
	    TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
	dt = ticks - start;
	kprintf("LWQ-F3: wait_on_bit_timeout(MAX_SCHEDULE_TIMEOUT) on a set "
	    "bit returned rv=%d after %d ticks (only exits because we clear "
	    "the bit below; internally it polls at 1 tick granularity)\n",
	    rv, dt);

	wakeup(&done);
	kthread_exit();
}

static void
f3_clearer(void *arg)
{
	tsleep(&f3_clearer, 0, "lwqf3c", 5 * hz);
	f3_word = 0;
	wake_up_bit((void *)&f3_word, 0);
	wakeup(&done);
	kthread_exit();
}

/*
 * ------------------------------------------------------------------
 * module glue
 * ------------------------------------------------------------------
 */
static int
lwqdemo_load(module_t mod, int what, void *arg)
{
	int error;

	switch (what) {
	case MOD_LOAD:
		init_waitqueue_head(&demo_wq);
		stopping = 0;
		done = 0;
		rounds_started = 0;
		rounds_done = 0;
		error = kthread_create(waiter_thread, NULL, NULL,
		    "lwq_waiter");
		if (error)
			return (error);
		error = kthread_create(waker_thread, NULL, NULL,
		    "lwq_waker");
		if (error)
			return (error);
		error = kthread_create(ctrl_thread, NULL, NULL,
		    "lwq_ctrl");
		if (error)
			return (error);
		error = kthread_create(f3_thread, NULL, NULL, "lwq_f3");
		if (error)
			return (error);
		error = kthread_create(f3_clearer, NULL, NULL,
		    "lwq_f3clr");
		if (error)
			return (error);
		kprintf("LWQ: module loaded, experiments running "
		    "(check dmesg for LWQ:/LWQ-F3: lines)\n");
		return (0);
	case MOD_UNLOAD:
		if (!done) {
			kprintf("LWQ: still running, refusing unload\n");
			return (EBUSY);
		}
		return (0);
	}
	return (EOPNOTSUPP);
}

static moduledata_t lwqdemo_mod = {
	"lwqdemo",
	lwqdemo_load,
	NULL
};
DECLARE_MODULE(lwqdemo, lwqdemo_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);