/*
 * DF-5436 KLD harness -- ulpt(4) cross-cdev open/close TOCTOU on sc_fflags.
 *
 * Production code under test (sys/bus/u4b/serial/ulpt.c, copied VERBATIM
 * below with only the usbd_xfer and usb_fifo calls stubbed at equivalent
 * cost):
 *
 *   ulpt_open()    ulpt.c:406-420   "we assume that open is a serial process"
 *   unlpt_open()   ulpt.c:422-458   racy EBUSY gate on sc->sc_fflags (:427),
 *                                    unlocked sc_fflags |=  (:456),
 *                                    sc_fifo_open[dir] = fifo (:441/:454)
 *   ulpt_close()   ulpt.c:460-470   unlocked sc_fflags &= ~ (:465)
 *   ulpt_watchdog()ulpt.c:719-735   polls device status iff sc_fflags == 0
 *
 * Framework reality modelled here (sys/bus/u4b/usb_dev.c):
 *   - per-cdev (per-vnode) serialization of open/close  -> per-cdev lock
 *     (usb_open/usb_cdevpriv_dtor run under the devfs vnode lock)
 *   - NO serialization ACROSS the two cdevs of one ulpt softc (ulpt + unlpt
 *     are two independent usb_fifo_attach() pairs, ulpt.c:625-638) -> the
 *     only cross-guard is the racy sc_fflags check.
 *
 * The window between the sc_fflags gate check (ulpt.c:427) and the
 * bookkeeping (ulpt.c:441/454/456) contains real production work:
 * usbd_xfer_set_stall() plus usb_fifo_alloc_buffer() -> usb_alloc_mbufs()
 * kmalloc of 2 x 32KB (ULPT_BSIZE=1<<15, ULPT_IFQ_MAXLEN=2).  The harness
 * performs an equivalent kmalloc(2 * ULPT_BSIZE) in that window -- no
 * artificial sleeps.
 *
 * Counters (sysctl hw.hx5436.*):
 *   opens_while_open   successful open of a direction that is ALREADY open
 *                      (exclusive-open invariant broken; must stay 0)
 *   double_open_simul  ...and another open of that direction is still inside
 *                      its open window (true simultaneous race proof)
 *   polls_while_open   watchdog ticks that would start the 1 Hz status poll
 *                      while a session is actually open (ulpt.c:730 guard
 *                      defeated -- the firmware-bug condition it avoids)
 *   resets_while_open  ulpt_open() SOFT_RESET decisions taken while a
 *                      session is actually open (ulpt.c:413 gate defeated)
 *   a_opens/b_opens    total successful opens per cdev
 *
 * sysctl hw.hx5436.fixed_gate=1 switches gate+bookkeeping to the fix.diff
 * mechanism (sc_lock held across check..commit in open and close); all
 * anomaly counters must then stay 0.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/lock.h>
#include <sys/malloc.h>
#include <sys/conf.h>
#include <sys/device.h>
#include <sys/callout.h>
#include <sys/sysctl.h>
#include <sys/fcntl.h>

#define	ULPT_BSIZE	(1<<15)		/* ulpt.c:75 */
#define	ULPT_IFQ_MAXLEN	2		/* ulpt.c:76 */

struct ulpt_softc {			/* production softc fields, verbatim */
	int	sc_fflags;		/* current open flags, FREAD/FWRITE */
	uint8_t	sc_iface_no;
	uint8_t	sc_last_status;
	uint8_t	sc_zlps;
};

struct hx_fifo {			/* stand-in for a struct usb_fifo slot */
	int	dir;
	void	*qbuf;			/* usb_fifo_alloc_buffer() mirror */
};

/* persistent softc state, as in production */
static struct ulpt_softc hx_sc;
static struct hx_fifo *hx_sc_fifo_open[2];	/* sc->sc_fifo_open[2] */
static struct hx_fifo	hx_slot[2][2];	/* [cdev 0=a/1=b][dir TX=0,RX=1] */
static struct lock	hx_cdev_lock[2];	/* devfs per-vnode serialization */
static struct lock	hx_sc_lock;		/* sc->sc_lock (fix gate) */

static struct callout	hx_watchdog;

/* ground truth maintained by the framework shim (NOT by the driver copy) */
static int	actually_open[2];	/* per direction */
static int	opening[2];		/* per direction, f_open in flight */

/* anomaly counters */
static int	counter_opens_while_open;
static int	counter_double_simul;
static int	counter_polls_while_open;
static int	counter_resets_while_open;
static int	counter_a_opens, counter_b_opens;
static int	counter_ebusy;

static int	hx_fixed_gate;		/* fix.diff mechanism switch */

/* instrumentation */
static int64_t	window_ns_sum;
static int64_t	window_n;
static int64_t	window_ns_max;
static int	counter_window_crossed;

SYSCTL_NODE(_hw, OID_AUTO, hx5436, CTLFLAG_RW, 0, "DF-5436 harness");
SYSCTL_INT(_hw_hx5436, OID_AUTO, opens_while_open, CTLFLAG_RD,
    &counter_opens_while_open, 0, "opens that broke exclusive-open");
SYSCTL_INT(_hw_hx5436, OID_AUTO, double_open_simul, CTLFLAG_RD,
    &counter_double_simul, 0, "simultaneous same-direction race wins");
SYSCTL_INT(_hw_hx5436, OID_AUTO, polls_while_open, CTLFLAG_RD,
    &counter_polls_while_open, 0, "watchdog polls during open session");
SYSCTL_INT(_hw_hx5436, OID_AUTO, resets_while_open, CTLFLAG_RD,
    &counter_resets_while_open, 0, "SOFT_RESET decisions during open session");
SYSCTL_INT(_hw_hx5436, OID_AUTO, a_opens, CTLFLAG_RD, &counter_a_opens, 0, "");
SYSCTL_INT(_hw_hx5436, OID_AUTO, b_opens, CTLFLAG_RD, &counter_b_opens, 0, "");
SYSCTL_INT(_hw_hx5436, OID_AUTO, ebusy, CTLFLAG_RD, &counter_ebusy, 0, "");
SYSCTL_INT(_hw_hx5436, OID_AUTO, fixed_gate, CTLFLAG_RW,
    &hx_fixed_gate, 0, "1 = fix.diff mechanism (locked gate+commit)");
SYSCTL_QUAD(_hw_hx5436, OID_AUTO, window_ns_sum, CTLFLAG_RD,
    &window_ns_sum, 0, "sum gate->commit latency");
SYSCTL_QUAD(_hw_hx5436, OID_AUTO, window_n, CTLFLAG_RD,
    &window_n, 0, "successful opens");
SYSCTL_QUAD(_hw_hx5436, OID_AUTO, window_ns_max, CTLFLAG_RD,
    &window_ns_max, 0, "max gate->commit latency");
SYSCTL_INT(_hw_hx5436, OID_AUTO, window_crossed, CTLFLAG_RD,
    &counter_window_crossed, 0, "peer committed during our gate window");

/*---------------- production code copies (ulpt.c) ----------------*/

static void
hx_usbd_xfer_set_stall(void *x __unused)
{
	/* production: lockmgr x2 + flag store (ulpt.c:432-434) -- no-op */
}

static int
hx_usb_fifo_alloc_buffer(struct hx_fifo *fifo)
{
	/* production: usb_fifo_alloc_buffer(fifo, usbd_xfer_max_len, 2)
	 * -> usb_alloc_mbufs(M_USBDEV, ..., ULPT_BSIZE, ULPT_IFQ_MAXLEN):
	 * one contiguous ~2x32KB kmalloc (usb_mbuf.c:55-100) */
	if (fifo->qbuf != NULL)
		kfree(fifo->qbuf, M_TEMP);
	fifo->qbuf = kmalloc(ULPT_BSIZE * ULPT_IFQ_MAXLEN, M_TEMP,
	    M_WAITOK | M_ZERO);
	return (fifo->qbuf != NULL ? 0 : ENOMEM);
}

static void
hx_usb_fifo_free_buffer(struct hx_fifo *fifo)
{
	/* production: kfree(queue_data) + queue reset (usb_dev.c:2009-2020) */
	if (fifo->qbuf != NULL) {
		kfree(fifo->qbuf, M_TEMP);
		fifo->qbuf = NULL;
	}
}

/*
 * ulpt.c:422-458 -- VERBATIM logic, stubs for the usb calls.
 * (instrumentation: window_ns_* measure gate-pass -> commit latency,
 * window_crossed counts canary hits where a peer committed during our
 * own gate window -- both are harness-only observations)
 */
static int
unlpt_open_copy(int cdev, int fflags)
{
	struct hx_fifo *fifo;
	int64_t ns0, ns1;
	struct timespec ts;
	int canary;

	if (hx_sc.sc_fflags & fflags) {
		return (EBUSY);			/* ulpt.c:427-429 (racy gate) */
	}
	getnanouptime(&ts);
	ns0 = (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
	canary = hx_sc.sc_fflags;		/* re-read after gate */
	if (fflags & FREAD) {
		/* clear stall first */
		hx_usbd_xfer_set_stall(NULL);
		fifo = &hx_slot[cdev][1];		/* USB_FIFO_RX */
		if (hx_usb_fifo_alloc_buffer(fifo)) {
			return (ENOMEM);
		}
		/* set which FIFO is opened */
		hx_sc_fifo_open[1] = fifo;		/* ulpt.c:441 */
	}
	if (fflags & FWRITE) {
		hx_usbd_xfer_set_stall(NULL);
		fifo = &hx_slot[cdev][0];		/* USB_FIFO_TX */
		if (hx_usb_fifo_alloc_buffer(fifo)) {
			return (ENOMEM);
		}
		hx_sc_fifo_open[0] = fifo;		/* ulpt.c:454 */
	}
	hx_sc.sc_fflags |= fflags & (FREAD | FWRITE);	/* ulpt.c:456 */
	getnanouptime(&ts);
	ns1 = (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
	window_ns_sum += ns1 - ns0;
	window_n++;
	if (ns1 - ns0 > window_ns_max)
		window_ns_max = ns1 - ns0;
	if (canary & fflags)
		counter_window_crossed++;
	return (0);
}

/* ulpt.c:406-420 */
static int
ulpt_open_copy(int cdev, int fflags)
{
	/* we assume that open is a serial process */	/* ulpt.c:411 */
	if (hx_sc.sc_fflags == 0) {
		if (actually_open[0] + actually_open[1] +
		    opening[0] + opening[1] > 0)
			counter_resets_while_open++;
		/* ulpt_reset(sc): 2 synchronous EP0 requests, ulpt.c:177-187 */
	}
	return (unlpt_open_copy(cdev, fflags));
}

/* ulpt.c:460-470 -- VERBATIM logic: production frees the CLOSING fifo's
 * buffer (usb_fifo_free_buffer(fifo)); sc_fifo_open[] is left dangling */
static void
ulpt_close_copy(int cdev, int fflags)
{
	hx_sc.sc_fflags &= ~(fflags & (FREAD | FWRITE));	/* ulpt.c:465 */

	if (fflags & FREAD)
		hx_usb_fifo_free_buffer(&hx_slot[cdev][1]);
	if (fflags & FWRITE)
		hx_usb_fifo_free_buffer(&hx_slot[cdev][0]);
	/* NOTE: production never does sc_fifo_open[dir] = NULL */
}

/* ulpt.c:719-735 -- watchdog condition verbatim */
static void
ulpt_watchdog_copy(void *arg __unused)
{
	int open_now = actually_open[0] + actually_open[1];

	if (hx_sc.sc_fflags == 0) {
		/* usbd_transfer_start(status poll) would happen here */
		if (open_now > 0)
			counter_polls_while_open++;
	}
	callout_reset(&hx_watchdog, hz, ulpt_watchdog_copy, NULL);
}

/*---------------- fixed-gate variant (fix.diff mechanism) ----------------*/

static int
unlpt_open_fixed(int cdev, int fflags)
{
	int rv;

	lockmgr(&hx_sc_lock, LK_EXCLUSIVE);
	rv = unlpt_open_copy(cdev, fflags);
	lockmgr(&hx_sc_lock, LK_RELEASE);
	return (rv);
}

static void
ulpt_close_fixed(int cdev, int fflags)
{
	lockmgr(&hx_sc_lock, LK_EXCLUSIVE);
	ulpt_close_copy(cdev, fflags);
	lockmgr(&hx_sc_lock, LK_RELEASE);
}

/*---------------- framework shim: the two cdevs ----------------*/

static d_open_t	hx_a_open, hx_b_open;
static d_close_t hx_a_close, hx_b_close;

static struct dev_ops hx_a_ops = {
	{ "hx5436a", 0, D_MPSAFE },		/* production: usb_ops (D_MPSAFE) */
	.d_open = hx_a_open,
	.d_close = hx_a_close,
};
static struct dev_ops hx_b_ops = {
	{ "hx5436b", 0, D_MPSAFE },
	.d_open = hx_b_open,
	.d_close = hx_b_close,
};

static cdev_t hx_a_dev, hx_b_dev;

static int
hx_open_common(int cdev, struct dev_open_args *ap, struct lock *cdev_lock)
{
	int fflags = ap->a_oflags & (FREAD | FWRITE);
	int is_rx = (fflags & FREAD) != 0;
	int is_tx = (fflags & FWRITE) != 0;
	int rv;

	/* per-cdev serialization only -- the production cross-cdev race is
	 * exactly the absence of anything wider (devfs per-vnode lock) */
	lockmgr(cdev_lock, LK_EXCLUSIVE);

	if (is_rx)
		atomic_add_int(&opening[1], 1);
	if (is_tx)
		atomic_add_int(&opening[0], 1);

	if (cdev == 0)				/* /dev/ulpt0 role */
		rv = (hx_fixed_gate ?
		    unlpt_open_fixed(0, fflags) : ulpt_open_copy(0, fflags));
	else					/* /dev/unlpt0 role */
		rv = (hx_fixed_gate ?
		    unlpt_open_fixed(1, fflags) : unlpt_open_copy(1, fflags));

	if (rv == 0) {
		/* exclusive-open invariant check (ground truth) */
		if ((is_rx && actually_open[1] > 0) ||
		    (is_tx && actually_open[0] > 0))
			atomic_add_int(&counter_opens_while_open, 1);
		if ((is_rx && opening[1] > 1) || (is_tx && opening[0] > 1))
			atomic_add_int(&counter_double_simul, 1);
		if (cdev == 0)
			atomic_add_int(&counter_a_opens, 1);
		else
			atomic_add_int(&counter_b_opens, 1);
		/* framework shim: open completed */
		if (is_rx)
			atomic_add_int(&actually_open[1], 1);
		if (is_tx)
			atomic_add_int(&actually_open[0], 1);
	} else if (rv == EBUSY) {
		atomic_add_int(&counter_ebusy, 1);
	}

	if (is_rx)
		atomic_add_int(&opening[1], -1);
	if (is_tx)
		atomic_add_int(&opening[0], -1);

	lockmgr(cdev_lock, LK_RELEASE);
	return (rv);
}

static int
hx_close_common(int cdev, struct dev_close_args *ap, struct lock *cdev_lock)
{
	int fflags = ap->a_fflag & (FREAD | FWRITE);

	lockmgr(cdev_lock, LK_EXCLUSIVE);

	/* framework: stop + unref happen BEFORE f_close (usb_dev.c:842-857) */
	if (fflags & FREAD)
		atomic_add_int(&actually_open[1], -1);
	if (fflags & FWRITE)
		atomic_add_int(&actually_open[0], -1);

	if (hx_fixed_gate)
		ulpt_close_fixed(cdev, fflags);
	else
		ulpt_close_copy(cdev, fflags);

	lockmgr(cdev_lock, LK_RELEASE);
	return (0);
}

static int
hx_a_open(struct dev_open_args *ap) { return (hx_open_common(0, ap, &hx_cdev_lock[0])); }
static int
hx_b_open(struct dev_open_args *ap) { return (hx_open_common(1, ap, &hx_cdev_lock[1])); }
static int
hx_a_close(struct dev_close_args *ap) { return (hx_close_common(0, ap, &hx_cdev_lock[0])); }
static int
hx_b_close(struct dev_close_args *ap) { return (hx_close_common(1, ap, &hx_cdev_lock[1])); }

/*---------------- module boilerplate ----------------*/

static int
hx5436_modevent(module_t mod, int type, void *arg __unused)
{
	switch (type) {
	case MOD_LOAD:
		memset(&hx_sc, 0, sizeof(hx_sc));
		memset(hx_slot, 0, sizeof(hx_slot));
		memset(hx_sc_fifo_open, 0, sizeof(hx_sc_fifo_open));
		memset(actually_open, 0, sizeof(actually_open));
		memset(opening, 0, sizeof(opening));
		counter_opens_while_open = 0;
		counter_double_simul = 0;
		counter_polls_while_open = 0;
		counter_resets_while_open = 0;
		counter_a_opens = counter_b_opens = counter_ebusy = 0;
		counter_window_crossed = 0;
		window_ns_sum = window_n = window_ns_max = 0;
		hx_fixed_gate = 0;
		lockinit(&hx_sc_lock, "hx5436lk", 0, LK_CANRECURSE);
		lockinit(&hx_cdev_lock[0], "hx5436a", 0, 0);
		lockinit(&hx_cdev_lock[1], "hx5436b", 0, 0);
		callout_init_lk(&hx_watchdog, &hx_sc_lock);
		callout_reset(&hx_watchdog, hz, ulpt_watchdog_copy, NULL);
		hx_a_dev = make_dev(&hx_a_ops, 0, UID_ROOT, GID_WHEEL,
		    0666, "hx5436a");
		hx_b_dev = make_dev(&hx_b_ops, 0, UID_ROOT, GID_WHEEL,
		    0666, "hx5436b");
		kprintf("HX5436: loaded (production racy gate; "
		    "sysctl hw.hx5436.fixed_gate=1 for fix mechanism)\n");
		return (0);
	case MOD_UNLOAD:
		if (actually_open[0] || actually_open[1]) {
			/* counter drift from the racy gate is itself
			 * evidence; the racer is joined before unload */
			kprintf("HX5436: ground-truth drift (actually_open="
			    "%d,%d) -- the race desyncs shared state\n",
			    actually_open[0], actually_open[1]);
		}
		callout_stop(&hx_watchdog);
		destroy_dev(hx_a_dev);
		destroy_dev(hx_b_dev);
		lockuninit(&hx_cdev_lock[0]);
		lockuninit(&hx_cdev_lock[1]);
		lockuninit(&hx_sc_lock);
		kprintf("HX5436: unloaded: opens_while_open=%d "
		    "double_simul=%d polls_while_open=%d resets_while_open=%d "
		    "a=%d b=%d ebusy=%d\n",
		    counter_opens_while_open, counter_double_simul,
		    counter_polls_while_open, counter_resets_while_open,
		    counter_a_opens, counter_b_opens, counter_ebusy);
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t hx5436_mod = {
	"hx5436openrace",
	hx5436_modevent,
	NULL
};
DECLARE_MODULE(hx5436openrace, hx5436_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
