/*
 * DF-5425 KLD harness -- production-code demonstration of the uslcom
 * (Silicon Labs CP210x) cfg-chain synchronous-request stall.
 *
 * WHAT IT PROVES (with the real in-kernel u4b usb_process machinery, the
 * same taskqueue ucom's cfg callbacks run on):
 *
 *   uslcom_param() (uslcom.c:505-583) chains 3 synchronous EP0 vendor
 *   control requests per tcsetattr of an open tty:
 *     uslcom.c:514-523  USLCOM_BAUD_RATE write   (ucom_cfg_do_request @520)
 *     uslcom.c:551-560  USLCOM_DATA write        (ucom_cfg_do_request @557)
 *     uslcom.c:573-582  USLCOM_SET_FLOWCTRL out  (ucom_cfg_do_request @579)
 *   uslcom_open() (uslcom.c:406-425) adds 1 (USLCOM_UART enable @418).
 *   uslcom_close() (uslcom.c:427-446) adds 1 (USLCOM_UART disable @442).
 *   Every ucom_cfg_do_request() carries a 1000 ms timeout and its
 *   usb_error_t result is DISCARDED ("failed (ignored)" at uslcom.c:420,
 *   522, 559, 581, 444), so no chain step ever aborts.
 *   A control transfer's data/status stage may be NAK'd by the device for
 *   as long as it likes: a silent evil CP210x makes each request eat its
 *   full 1000 ms timeout (usb_request.c:642-644 WXFER loop, 699-704
 *   timeout) and the chain marches on anyway; a slow-but-answer-at-999 ms
 *   evil device makes every request *succeed* with no error at all.
 *
 *   Amplifier specific to uslcom: while the port is open, the watchdog
 *   (uslcom.c:315-326, reset at hz/4 by uslcom_open) keeps a RCTRL control
 *   xfer in flight on the shared ep0 endpoint queue (uslcom.c:756-770,
 *   1 s timeout), so every cfg request can additionally queue behind up
 *   to ~1 s of in-flight RCTRL -- measured timings here are the floor.
 *
 *   The chain runs on sc_super_ucom.sc_tq (per-adapter ucom taskqueue,
 *   usb_serial.c:322).  close(2) is SYNCHRONOUS against that queue:
 *   ucom_close() -> ucom_queue_command(ucom_cfg_close) -> usb_proc_mwait()
 *   (usb_serial.c:671-672, 955-957), uninterruptible cv_wait
 *   (usb_process.c:385-393) while a message stays head-of-queue for the
 *   duration of its callback (usb_process.c:146-158).  Unplug mid-cycle
 *   stalls the USB hub event thread the same way via ucom_detach() ->
 *   usb_proc_drain() (usb_serial.c:394) -- the DF-5367 mechanism, uslcom
 *   instance.  Every tcsetattr of an open tty re-queues the param task
 *   (ucom's sc_param_task[2] double buffer, usb_serial.c:1691-1693), so
 *   an unprivileged local user multiplies the occupancy.
 *
 *   => one open+tcsetattr+close cycle with the evil device costs
 *      1x1000 + 3x1000 + 1x1000 ms + 2 core pauses (hz/10 each,
 *      usb_serial.c:777 and 1636) ~= 5.2 s of tq occupancy; two queued
 *      tcsetattrs block a closer ~8.3 s.
 *
 * PHASE A (baseline): queue the open(2) chain (cfg_open: 1 x 1000 ms +
 *   hz/10 pause) then ONE param-chain task (3 x lksleep("WXFER", 1000 ms)
 *   -- the exact production per-request cost of a request the device
 *   stretches past its timeout -- + hz/10 pause), then what ucom_close()
 *   does: queue cfg_close (1 x 1000 ms, no core pause) + usb_proc_mwait().
 *   Measure the closer's block time.  EXPECT ~5100 ms.
 *
 * PHASE A2 (amplification): open + queue the param task TWICE (two
 *   tcsetattr equivalent, production double-buffer shape) then the closer.
 *   EXPECT ~8200 ms.
 *
 * PHASE B (fix.diff mechanism #1: abort each cfg chain on its first
 *   failed request, timeout unchanged): silent evil device -> request #1
 *   of each chain times out at 1000 ms -> error propagated -> chain
 *   aborts.  EXPECT ~3100 ms.
 *
 * PHASE C (fix.diff mechanism #1+#2: abort + bound the cfg-path control
 *   timeout 1000 -> 250 ms): covers the slow-but-successful evil device
 *   too.  EXPECT ~850 ms.
 */
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/lock.h>
#include <sys/condvar.h>
#include <sys/time.h>
#include <sys/types.h>

#include <sys/bus.h>
#include <bus/u4b/usb.h>
#include <bus/u4b/usbdi.h>
#include <bus/u4b/usb_process.h>

#define	SL_PARAM_REQS	3	/* uslcom.c:520,557,579 per-tcsetattr chain */
#define	SL_OPEN_REQS	1	/* uslcom.c:418 first-open chain */
#define	SL_CLOSE_REQS	1	/* uslcom.c:442 per-close chain */
#define	SL_REQ_MS	1000	/* uslcom.c production cfg timeout */

static struct lock sl_lock;		/* stands in for sc->sc_lock */
static struct usb_process sl_tq;	/* sc_super_ucom.sc_tq (usb_serial.c:322) */

struct sl_msg {
	struct usb_proc_msg hdr;
};
static struct sl_msg sl_param[2];	/* ucom sc_param_task[2] double buffer */
static struct sl_msg sl_open_msg[2];	/* ucom sc_open_task[2] */
static struct sl_msg sl_close[2];	/* ucom sc_close_task[2] */

static int sl_fixed;			/* fix mechanism #1: abort on error */
static int sl_req_ms = SL_REQ_MS;	/* fix mechanism #2: bounded timeout */
static int sl_aborted;

static int64_t
sl_ms(struct timeval *t0, struct timeval *t1)
{
	return ((int64_t)(t1->tv_sec - t0->tv_sec) * 1000 +
	    (t1->tv_usec - t0->tv_usec) / 1000);
}

/*
 * One uslcom vendor request against the silent evil CP210x: the device
 * never answers the data/status stage, so the requester sleeps in the
 * "WXFER" loop until the timeout fires (usb_request.c:642-644, 699-704)
 * and the request returns USB_ERR_TIMEOUT -- which production uslcom
 * discards at every call site (uslcom.c:420, 522, 559, 581, 444).
 */
static int
sl_one_request(void)
{
	lksleep(&sl_lock, &sl_lock, 0, "WXFER", USB_MS_TO_TICKS(sl_req_ms));
	return (0);	/* production: even a timeout is ignored */
}

/* uslcom_param()'s 3-request chain (uslcom.c:505-583), runs on sc_tq */
static void
sl_cfg_param(struct usb_proc_msg *pm)
{
	int i;

	kprintf("SL5425: cfg-param task: enter, %d x %d ms sync EP0 requests%s\n",
	    SL_PARAM_REQS, sl_req_ms,
	    sl_fixed ? " (fix: abort on first error)" : "");
	sl_aborted = 0;
	for (i = 0; i < SL_PARAM_REQS; ++i) {
		sl_one_request();	/* the request that times out */
		if (sl_fixed) {
			/*
			 * fix.diff mechanism: requests propagate the error,
			 * cfg chain bails on first failure.  With the silent
			 * device request #1 fails after its timeout.
			 */
			kprintf("SL5425: cfg-param: request %d/%d failed "
			    "(USB_ERR_TIMEOUT) -> abort chain (fixed)\n",
			    i + 1, SL_PARAM_REQS);
			sl_aborted = 1;
			break;
		}
	}
	/* ucom_cfg_param()'s trailing pause, usb_serial.c:1636 */
	usb_pause_mtx(&sl_lock, hz / 10);
	kprintf("SL5425: cfg-param task: leave (%d/%d requests issued)\n",
	    sl_aborted ? 1 : SL_PARAM_REQS, SL_PARAM_REQS);
}

/* uslcom_open()'s 1-request chain (uslcom.c:406-425), runs on sc_tq */
static void
sl_cfg_open(struct usb_proc_msg *pm)
{
	int i;

	kprintf("SL5425: cfg-open task: enter, %d x %d ms sync EP0 requests%s\n",
	    SL_OPEN_REQS, sl_req_ms,
	    sl_fixed ? " (fix: abort on first error)" : "");
	for (i = 0; i < SL_OPEN_REQS; ++i) {
		sl_one_request();	/* the request that times out */
		if (sl_fixed) {
			kprintf("SL5425: cfg-open: request %d/%d failed "
			    "(USB_ERR_TIMEOUT) -> abort chain (fixed)\n",
			    i + 1, SL_OPEN_REQS);
			break;
		}
	}
	/* ucom_cfg_open()'s trailing pause, usb_serial.c:777 */
	usb_pause_mtx(&sl_lock, hz / 10);
	kprintf("SL5425: cfg-open task: leave\n");
}

/* uslcom_close()'s 1-request chain (uslcom.c:427-446), runs on sc_tq */
static void
sl_cfg_close(struct usb_proc_msg *pm)
{
	int i;

	kprintf("SL5425: cfg-close task: enter, %d x %d ms sync EP0 request "
	    "(uslcom HAS a ucom_cfg_close callback)\n",
	    SL_CLOSE_REQS, sl_req_ms);
	for (i = 0; i < SL_CLOSE_REQS; ++i) {
		sl_one_request();	/* UART disable that times out */
		if (sl_fixed) {
			kprintf("SL5425: cfg-close: request %d/%d failed "
			    "(USB_ERR_TIMEOUT) -> abort chain (fixed)\n",
			    i + 1, SL_CLOSE_REQS);
			break;
		}
	}
	kprintf("SL5425: cfg-close task: leave (no core pause after "
	    "ucom_cfg_close)\n");
}

/*
 * Exactly ucom_close()'s synchronous close (usb_serial.c:671-672 + 955-957):
 * queue cfg_close behind the running/queued cfg_param task(s) and mwait
 * uninterruptibly (usb_process.c:385-393).
 */
static int64_t
sl_closer_thread(int phase, int nparam)
{
	struct timeval t0, t1;
	char tag = phase == 2 ? 'B' : (phase == 3 ? 'C' : (nparam > 1 ? '2' : 'A'));

	/* the tcsetattr(s) queued cfg_param; the first starts running */
	tsleep(&sl_tq, 0, "sllead", hz / 10);	/* 100 ms into the chain */

	kprintf("SL5425[%c]: closer: queue cfg_close + usb_proc_mwait() "
	    "(uninterruptible, usb_serial.c:671-672) behind %d param task(s)\n",
	    tag, nparam);
	microtime(&t0);
	lockmgr(&sl_lock, LK_EXCLUSIVE);
	if (!usb_proc_is_gone(&sl_tq)) {
		struct sl_msg *m;

		m = usb_proc_msignal(&sl_tq, &sl_close[0], &sl_close[1]);
		m->hdr.pm_callback = &sl_cfg_close;
		/* usb_serial.c:671-672 */
		usb_proc_mwait(&sl_tq, &sl_close[0], &sl_close[1]);
	}
	lockmgr(&sl_lock, LK_RELEASE);
	microtime(&t1);
	kprintf("SL5425[%c]: closer: unblocked  ELAPSED=%jd ms\n",
	    tag, (intmax_t)sl_ms(&t0, &t1));
	return (sl_ms(&t0, &t1));
}

/*
 * The open(2) path: ucom_open() queues ucom_cfg_open first
 * (usb_serial.c:865-867) -- 1 x 1000 ms for the evil device.
 */
static void
sl_open(void)
{
	struct sl_msg *m;

	lockmgr(&sl_lock, LK_EXCLUSIVE);
	m = usb_proc_msignal(&sl_tq, &sl_open_msg[0], &sl_open_msg[1]);
	m->hdr.pm_callback = &sl_cfg_open;
	lockmgr(&sl_lock, LK_RELEASE);
}

/*
 * The local user's tcsetattr(2): ucom_param() -> ucom_queue_command(
 * ucom_cfg_param, ..., &sc->sc_param_task[0].hdr, &sc->sc_param_task[1].hdr)
 * (usb_serial.c:1691-1693) -- double-buffered, so two tsetattrs leave two
 * outstanding tasks.
 */
static void
sl_tcsetattr(int n)
{
	struct sl_msg *m;

	lockmgr(&sl_lock, LK_EXCLUSIVE);
	while (n--) {
		m = usb_proc_msignal(&sl_tq, &sl_param[0], &sl_param[1]);
		m->hdr.pm_callback = &sl_cfg_param;
	}
	lockmgr(&sl_lock, LK_RELEASE);
}

static int
sl_phase(int phase, int nparam)
{
	int error;

	memset(&sl_tq, 0, sizeof(sl_tq));
	error = usb_proc_create(&sl_tq, &sl_lock,
	    phase == 2 ? "sl5425b" : (phase == 3 ? "sl5425c" : "sl5425a"),
	    USB_PRI_MED);
	if (error) {
		kprintf("SL5425: usb_proc_create failed: %d\n", error);
		return (error);
	}
	sl_fixed = (phase >= 2);
	sl_req_ms = (phase == 3) ? 250 : SL_REQ_MS;

	/* the user opens the tty (queues cfg_open) and tcsetattr's nparam times */
	sl_open();
	sl_tcsetattr(nparam);

	/* the closing thread (or the hub event thread on unplug) */
	sl_closer_thread(phase, nparam);

	usb_proc_free(&sl_tq);
	return (0);
}

static int
sl_modevent(module_t mod, int type, void *data)
{
	int error = 0;

	switch (type) {
	case MOD_LOAD:
		lockinit(&sl_lock, "sl5425", 0, LK_CANRECURSE);

		kprintf("SL5425: DF-5425 uslcom cfg-chain stall harness, "
		    "hz=%d, %d reqs/param-task, %d reqs/open-task, "
		    "%d reqs/close-task, %d ms/request (production timeout)\n",
		    hz, SL_PARAM_REQS, SL_OPEN_REQS, SL_CLOSE_REQS, SL_REQ_MS);

		sl_phase(0, 1);		/* A: open+1 tcsetattr+close ~5.1 s */
		sl_phase(1, 2);		/* A2: open+2 tcsetattr+close ~8.2 s */
		sl_phase(2, 1);		/* B: fix #1 (abort) ~3.1 s */
		sl_phase(3, 1);		/* C: fix #1+#2 (abort+250ms) ~0.9 s */

		lockuninit(&sl_lock);
		kprintf("SL5425: harness complete\n");
		break;
	case MOD_UNLOAD:
		break;
	default:
		error = EOPNOTSUPP;
		break;
	}
	return (error);
}

static moduledata_t sl_mod = { "uslcomcfgstall", sl_modevent, 0 };
DECLARE_MODULE(uslcomcfgstall, sl_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_DEPEND(uslcomcfgstall, usb, 1, 1, 1);
