⬢ DragonFlyBSD Kernel Audit
DF-5533 / dfharness.c
← back to finding ↓ download raw
/*
 * DF-5532 / DF-5533 PoC harness — exercises the *production*
 * sys/bus/u4b/usb_process.c machinery (usb_proc_create / msignal /
 * mwait / drain) from a KLD, the same evidence class as the
 * usb-cfg-stall family rigs (DF-5367..DF-5504).
 *
 * DF-5532 (mwait starvation, production-faithful mode):
 *   - "churn" message pair whose callback does a *minimal* amount of
 *     work (drops the lock for 1 tick, like any real cfg callback
 *     doing a USB request) and re-queues itself  => the usb_process
 *     queue is never empty, forever, with NO slow callback anywhere.
 *   - "victim" message pair that the waiter thread mwait()s on; the
 *     victim callback runs and completes almost immediately.
 *   - usb_proc_mwait() still does not return until the churn is
 *     stopped, because the ONLY up_drain broadcasts are the
 *     empty-queue transition (usb_process.c:163-167) and drain
 *     teardown (:445-447)  => the waiter sleeps uninterruptibly
 *     (cv_wait, no PCATCH: kern_condvar.c _cv_timedwait wakesig=0)
 *     long after its own messages have completed.
 *   - churn_ticks=0 selects the extreme spin mode: an instantly
 *     self-requeuing message monopolizes the usb_process thread AND
 *     its lock (observed: 8.9e9 callbacks / 275 s; even lockmgr
 *     acquisition by other threads starves).
 *
 * DF-5533 (cold-path drain hang):
 *   - sysctl kern.dfcold.fire=1 sets cold=1, then calls
 *     usb_proc_drain() on a live usb_process: the cold branch
 *     (usb_process.c:433-439) calls suspend_kproc() on a thread
 *     whose kthread_suspend_check() is #if 0'd out
 *     (usb_process.c:86-89)  => tsleep(td,0,...,timo=0) forever,
 *     bus lock held.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/lock.h>
#include <sys/condvar.h>
#include <sys/thread.h>
#include <sys/kthread.h>
#include <sys/sysctl.h>
#include <sys/time.h>
#include <bus/u4b/usb.h>
#include <bus/u4b/usbdi.h>
#include <bus/u4b/usb_process.h>

struct hmsg {
	struct usb_proc_msg hdr;
	int id;
};

static struct lock	 hlock;
static struct usb_process up;
static struct hmsg	 vm[2];			/* victim pair */
static struct hmsg	 cm[2];			/* churn pair  */

static volatile int	 churn_on;
static int		 churn_ticks = 1;	/* 0 = spin mode */
static volatile long	 churn_count;
static volatile long	 victim_ran;
static volatile long	 victim_ran_ms;
static volatile long	 waiter_woke_ms;
static volatile int	 waiter_done;
static volatile long	 started_ms;
static struct thread	*waiter_td;

static long
now_ms(void)
{
	struct timespec ts;
	getnanotime(&ts);
	return (ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}

static void
churn_cb(struct usb_proc_msg *pm)
{
	/* entered with hlock held (dispatcher convention) */
	churn_count++;
	if (churn_on)
		usb_proc_msignal(&up, &cm[0].hdr, &cm[1].hdr);
	if (churn_ticks > 0) {
		/*
		 * Stand-in for the lock-drop every real cfg callback
		 * performs while doing its USB request (cf.
		 * usbd_do_request_flags() in usb_request.c): the lock
		 * is released for 1 tick, then re-acquired before
		 * returning to the dispatcher.
		 */
		lockmgr(&hlock, LK_RELEASE);
		tsleep(curthread, 0, "churn", churn_ticks);
		lockmgr(&hlock, LK_EXCLUSIVE);
	}
}

static void
victim_cb(struct usb_proc_msg *pm)
{
	victim_ran++;
	victim_ran_ms = now_ms();
}

static void
waiter_thread(void *arg)
{
	/*
	 * Stand-in for the unprivileged-syscall waiters of the
	 * production consumers: ucom_queue_command()
	 * (usb_serial.c:655-672, tcsetattr/close on /dev/ttyU*) and
	 * usbd_transfer_unsetup() (usb_transfer.c:1286).
	 */
	tsleep(curthread, 0, "d0", hz / 2);	/* let churn establish */

	lockmgr(&hlock, LK_EXCLUSIVE);
	usb_proc_msignal(&up, &vm[0].hdr, &vm[1].hdr);	/* queue victim */
	usb_proc_mwait(&up, &vm[0].hdr, &vm[1].hdr);	/* sleep on up_drain */
	waiter_woke_ms = now_ms();
	waiter_done = 1;
	lockmgr(&hlock, LK_RELEASE);
	wakeup(&waiter_done);
	kthread_exit();
}

static int
sysctl_report(SYSCTL_HANDLER_ARGS)
{
	char buf[512];

	ksnprintf(buf, sizeof(buf),
	    "started_ms=%ld victim_ran=%ld victim_ran_ms=%ld "
	    "churn_count=%ld waiter_done=%d waiter_woke_ms=%ld now_ms=%ld "
	    "starve_ms=%ld\n",
	    started_ms, victim_ran, victim_ran_ms, churn_count,
	    waiter_done, waiter_woke_ms, now_ms(),
	    waiter_done ? (waiter_woke_ms - victim_ran_ms) : -1);
	sysctl_handle_string(oidp, buf, sizeof(buf), req);
	return (0);
}

static int
sysctl_stop(SYSCTL_HANDLER_ARGS)
{
	int v = 0;

	if (req->newptr == NULL)
		return (EINVAL);
	sysctl_handle_int(oidp, &v, 0, req);
	if (v == 1)
		churn_on = 0;		/* let the queue drain */
	return (0);
}

static int
sysctl_fire_cold(SYSCTL_HANDLER_ARGS)
{
	int v = 0;

	if (req->newptr == NULL)
		return (EINVAL);
	sysctl_handle_int(oidp, &v, 0, req);
	if (v != 1)
		return (0);

	/*
	 * DF-5533: drain while "cold".  Mirrors a USB controller
	 * attach-failure during autoconf (cold==1) reaching
	 * usb_proc_free()/usb_proc_drain() (usb_controller.c:227-236).
	 * Expected: NEVER returns — suspend_kproc() tsleeps forever on a
	 * thread that never runs kthread_suspend_check().
	 */
	cold = 1;
	usb_proc_drain(&up);
	cold = 0;
	kprintf("df5533: drain RETURNED (unexpected)\n");
	return (0);
}

static SYSCTL_NODE(_kern, OID_AUTO, dfstarv, CTLFLAG_RW, 0, "usb_process mwait starvation");
SYSCTL_PROC(_kern_dfstarv, OID_AUTO, report, CTLTYPE_STRING|CTLFLAG_RD,
    0, 0, sysctl_report, "A", "starvation state");
SYSCTL_PROC(_kern_dfstarv, OID_AUTO, stop, CTLTYPE_INT|CTLFLAG_RW,
    0, 0, sysctl_stop, "I", "stop the churn");
SYSCTL_PROC(_kern_dfstarv, OID_AUTO, churn_ticks, CTLTYPE_INT|CTLFLAG_RW,
    &churn_ticks, 0, sysctl_handle_int, "I", "churn sleep ticks (0=spin)");
SYSCTL_NODE(_kern, OID_AUTO, dfcold, CTLFLAG_RW, 0, "usb_process cold drain");
SYSCTL_PROC(_kern_dfcold, OID_AUTO, fire, CTLTYPE_INT|CTLFLAG_RW,
    0, 0, sysctl_fire_cold, "I", "fire cold-path drain");

static int
dfharness_modevent(module_t mod, int type, void *data)
{
	int error;

	switch (type) {
	case MOD_LOAD:
		lockinit(&hlock, "dfhz", 0, 0);
		memset(&up, 0, sizeof(up));
		memset(vm, 0, sizeof(vm));
		memset(cm, 0, sizeof(cm));
		vm[0].hdr.pm_callback = &victim_cb;
		vm[1].hdr.pm_callback = &victim_cb;
		cm[0].hdr.pm_callback = &churn_cb;
		cm[1].hdr.pm_callback = &churn_cb;

		error = usb_proc_create(&up, &hlock, "dfhz", USB_PRI_MED);
		if (error) {
			kprintf("dfharness: usb_proc_create failed %d\n",
			    error);
			return (error);
		}
		started_ms = now_ms();
		/* start the perpetual churn */
		churn_on = 1;
		lockmgr(&hlock, LK_EXCLUSIVE);
		usb_proc_msignal(&up, &cm[0].hdr, &cm[1].hdr);
		lockmgr(&hlock, LK_RELEASE);

		/* spawn the waiter (queues victim, then mwaits) */
		error = kthread_create(&waiter_thread, NULL, &waiter_td,
		    "dfwait");
		if (error) {
			kprintf("dfharness: kthread_create failed %d\n",
			    error);
			return (error);
		}
		kprintf("dfharness: loaded; churn (ticks=%d) running, "
		    "waiter will mwait the victim pair\n", churn_ticks);
		return (0);
	case MOD_UNLOAD:
		churn_on = 0;
		while (!waiter_done)
			tsleep(&waiter_done, 0, "dfw8", hz);
		usb_proc_free(&up);
		lockuninit(&hlock);
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t dfharness_mod = { "dfharness", dfharness_modevent, 0 };
DECLARE_MODULE(dfharness, dfharness_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
MODULE_VERSION(dfharness, 1);