/*
 * usb_template_fuzz.c - DFly pass-2 negative-evidence KLD for
 * sys/bus/u4b/template/usb_template.c
 *
 * Drives the real USB gadget template machinery (usb_template.ko +
 * usb.ko) with the full space of host-crafted control requests:
 *
 *   (a) template dispatch: wValue[1] (descriptor type) 0..255,
 *       wValue[0] (index / string id) 0..255, wIndex (langid) sweep,
 *       bmRequestType/bRequest sweep
 *   (b) descriptor serving: every served descriptor is validated for
 *       internal consistency (bLength==served len, descriptor walk
 *       ends exactly at wTotalLength, sum of wTotalLengths equals the
 *       independently recomputed pass-1 sizing)
 *   (c) template registry: usb_temp_setup_by_index_p over valid and
 *       invalid indices
 *   (d) speeds: FULL / HIGH / SUPER for every template
 *
 * Success criterion: no panic, no validation failure, "FUZZ: PASS".
 */
#include <sys/stdint.h>
#include <sys/param.h>
#include <sys/queue.h>
#include <sys/types.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/bus.h>
#include <sys/module.h>
#include <sys/lock.h>
#include <sys/condvar.h>
#include <sys/sysctl.h>
#include <sys/unistd.h>
#include <sys/callout.h>
#include <sys/malloc.h>
#include <sys/caps.h>

#include <bus/u4b/usb.h>
#include <bus/u4b/usb_ioctl.h>
#include <bus/u4b/usbdi.h>
#include <bus/u4b/usbdi_util.h>

#include <bus/u4b/usb_cdc.h>
#include <bus/u4b/usb_core.h>
#include <bus/u4b/usb_dynamic.h>
#include <bus/u4b/usb_busdma.h>
#include <bus/u4b/usb_process.h>
#include <bus/u4b/usb_device.h>

#include <bus/u4b/usb_controller.h>
#include <bus/u4b/usb_bus.h>
#include <bus/u4b/usb_request.h>
#include <bus/u4b/template/usb_template.h>

/* ------------------------------------------------------------------ */
/* fake bus / device plumbing                                         */

static void	fuzz_get_hw_ep_profile(struct usb_device *,
		    const struct usb_hw_ep_profile **, uint8_t);
struct usb_device_descriptor *usb_temp_get_device_desc_public(
		    struct usb_device *);

static const struct usb_hw_ep_profile pf_ctrl = {
	.max_in_frame_size = 64,
	.max_out_frame_size = 64,
	.support_control = 1,
	.support_in = 1,
	.support_out = 1,
};

static const struct usb_hw_ep_profile pf_bulk = {
	.max_in_frame_size = 1024,
	.max_out_frame_size = 1024,
	.support_bulk = 1,
	.support_interrupt = 1,
	.support_isochronous = 1,
	.support_in = 1,
	.support_out = 1,
};

static void
fuzz_get_hw_ep_profile(struct usb_device *udev,
    const struct usb_hw_ep_profile **ppf, uint8_t ep_addr)
{
	if (ep_addr == 0)
		*ppf = &pf_ctrl;
	else if (ep_addr < 16)
		*ppf = &pf_bulk;
	else
		*ppf = NULL;
}

static const struct usb_bus_methods fuzz_bus_methods = {
	.get_hw_ep_profile = &fuzz_get_hw_ep_profile,
};

static struct usb_bus fuzz_bus;
static struct usb_device fuzz_udev;
static int fuzz_locks_inited;

static void
fuzz_prep(enum usb_dev_speed spd)
{
	if (fuzz_udev.usb_template_ptr != NULL)
		usb_temp_unsetup_p(&fuzz_udev);
	fuzz_udev.bus = &fuzz_bus;
	fuzz_udev.speed = spd;
	fuzz_udev.flags.usb_mode = USB_MODE_DEVICE;
	fuzz_udev.flags.self_powered = 1;
	fuzz_bus.methods = &fuzz_bus_methods;
}

/* ------------------------------------------------------------------ */
/* independent re-computation of the pass-1 sizing walk               */

static uint32_t
tpl_expected_size(const struct usb_temp_device_desc *tdd)
{
	const struct usb_temp_config_desc *const *tcd;
	const struct usb_temp_interface_desc *const *tid;
	const struct usb_temp_endpoint_desc *const *ted;
	const void **rd;
	uint32_t size = sizeof(struct usb_temp_data);

	for (tcd = tdd->ppConfigDesc; (tcd != NULL) && (*tcd != NULL); tcd++) {
		size += sizeof(struct usb_config_descriptor);
		for (tid = (*tcd)->ppIfaceDesc;
		    (tid != NULL) && (*tid != NULL); tid++) {
			size += sizeof(struct usb_interface_descriptor);
			for (rd = (*tid)->ppRawDesc;
			    (rd != NULL) && (*rd != NULL); rd++)
				size += ((const uint8_t *)*rd)[0];
			for (ted = (*tid)->ppEndpoints;
			    (ted != NULL) && (*ted != NULL); ted++) {
				size += sizeof(struct usb_endpoint_descriptor);
				if (((*ted)->bmAttributes & UE_XFERTYPE) ==
				    UE_ISOCHRONOUS)
					size += 2;
				for (rd = (*ted)->ppRawDesc;
				    (rd != NULL) && (*rd != NULL); rd++)
					size += ((const uint8_t *)*rd)[0];
			}
		}
	}
	return (size);
}

/* ------------------------------------------------------------------ */
/* validation helpers                                                 */

static uint64_t fuzz_calls, fuzz_nonnull, fuzz_stalls, fuzz_anom;
static int fuzz_prints;

static void
fuzz_fail(const char *tag, uint8_t rt, uint8_t rq, uint8_t dt, uint8_t di,
    uint16_t lang, uint32_t v1, uint32_t v2)
{
	if (fuzz_prints++ < 64)
		kprintf("FUZZ ANOMALY: %s rt=%02x rq=%02x dt=%02x di=%02x "
		    "lang=%04x (%u vs %u)\n",
		    tag, rt, rq, dt, di, lang, v1, v2);
	fuzz_anom++;
}

/*
 * Walk the inner descriptors of a served config blob; the bLength
 * chain must end exactly at "len".
 */
static int
walk_config(const uint8_t *buf, uint32_t len)
{
	uint32_t off = 0;

	while (off < len) {
		uint8_t bl = buf[off];
		if (bl < 3)
			return (0);
		if (off + bl > len)
			return (0);
		off += bl;
	}
	return (off == len);
}

static void
check_response(uint8_t rt, uint8_t rq, uint8_t dt, uint8_t di,
    uint16_t lang, const void *ptr, uint16_t len)
{
	const uint8_t *buf = ptr;
	uint32_t sum;
	uint16_t i;

	fuzz_calls++;
	if (ptr == NULL) {
		if (len != 0)
			fuzz_fail("null-ptr-with-len", rt, rq, dt, di, lang,
			    len, 0);
		fuzz_stalls++;
		return;
	}
	fuzz_nonnull++;
	if (len == 0) {
		fuzz_fail("nonnull-len0", rt, rq, dt, di, lang,
		    0, 0);
		return;
	}
	/* touch every served byte */
	sum = 0;
	for (i = 0; i != len; i++)
		sum += buf[i];
	if (sum == 0xDEADBEEF)
		kprintf("FUZZ: (checksum coincidence ignored)\n");

	switch (dt) {
	case UDESC_DEVICE:
		if ((len != sizeof(struct usb_device_descriptor)) ||
		    (buf[1] != UDESC_DEVICE))
			fuzz_fail("bad-device-desc", rt, rq, dt, di, lang,
			    len, sizeof(struct usb_device_descriptor));
		break;
	case UDESC_DEVICE_QUALIFIER:
		if ((len != sizeof(struct usb_device_qualifier)) ||
		    (buf[1] != UDESC_DEVICE_QUALIFIER))
			fuzz_fail("bad-qualifier", rt, rq, dt, di,
			    lang, len, sizeof(struct usb_device_qualifier));
		break;
	case UDESC_STRING:
		if ((buf[1] != UDESC_STRING) || (len & 1) || (len > 255))
			fuzz_fail("bad-string", rt, rq, dt, di, lang,
			    len, 0);
		break;
	case UDESC_CONFIG:
	case UDESC_OTHER_SPEED_CONFIGURATION:
		if ((len < sizeof(struct usb_config_descriptor)) ||
		    (buf[1] != UDESC_CONFIG))
			fuzz_fail("bad-config", rt, rq, dt, di, lang,
			    len, 0);
		else if (!walk_config(buf, len))
			fuzz_fail("config-walk", rt, rq, dt, di,
			    lang, len, 0);
		break;
	default:
		/* vendor / other: only the generic constraints above */
		break;
	}
}

/* one control request through the real demux */
static void
do_req(uint8_t rt, uint8_t rq, uint8_t dt, uint8_t di, uint16_t lang)
{
	struct usb_device_request req;
	const void *ptr;
	uint16_t len;
	usb_error_t err;

	req.bmRequestType = rt;
	req.bRequest = rq;
	req.wValue[0] = di;
	req.wValue[1] = dt;
	USETW(req.wIndex, lang);
	USETW(req.wLength, 0xFFFF);

	ptr = (void *)1;	/* detect missing writes */
	len = 0xBEEF;
	err = (usb_temp_get_desc_p) (&fuzz_udev, &req, &ptr, &len);
	if (err != 0) {
		fuzz_fail("demux-error", rt, rq, dt, di, lang,
		    err, 0);
		return;
	}
	if (ptr == (void *)1)
		fuzz_fail("ptr-untouched", rt, rq, dt, di, lang, 0, 0);
	else
		check_response(rt, rq, dt, di, lang, ptr, len);
}

/* ------------------------------------------------------------------ */
/* per template+speed sweep                                           */

static const uint8_t rt_tab[] = {
	UT_READ_DEVICE, UT_READ_CLASS_DEVICE, UT_READ_VENDOR_DEVICE,
	0x00, 0x81, 0x21, 0x8A, 0xFF
};
static const uint8_t rq_tab[] = { 0x06, 0x00, 0x01, 0x07, 0xFF };
static const uint16_t lang_tab[] = { 0x0000, 0x0409, 0xFFFF, 0x0904 };

static int
sweep_template(const struct usb_temp_device_desc *tdd, const char *name,
    enum usb_dev_speed spd)
{
	struct usb_device_descriptor *dd;
	const uint8_t *buf;
	uint32_t expect, wtot = 0;
	uint8_t cfg, ncfg;
	uint8_t rt, rq, dt, di;
	u_int li;
	usb_error_t err;

	fuzz_prep(spd);
	err = usb_temp_setup(&fuzz_udev, tdd);
	if (err != 0) {
		if ((spd == USB_SPEED_SUPER) && (err == USB_ERR_INVAL)) {
			/* template mps[] tables define FULL/HIGH only:
			 * usb_template.c:221-225 rejects mps==0 */
			kprintf("FUZZ: template %s speed SUPER: setup "
			    "err=INVAL (expected - no SUPER mps)\n", name);
			return (0);
		}
		kprintf("FUZZ: template %s speed %d: setup err=%d "
		    "(unexpected - profiles support everything)\n",
		    name, (int)spd, err);
		return (1);
	}
	if (fuzz_udev.usb_template_ptr == NULL) {
		kprintf("FUZZ: template %s speed %d: NULL template_ptr "
		    "after successful setup\n", name, (int)spd);
		return (1);
	}

	/* (b) whole-buffer consistency: sum of wTotalLengths must equal
	 *     the independently computed pass-1 size. */
	expect = tpl_expected_size(tdd);
	dd = usb_temp_get_device_desc_public(&fuzz_udev);
	if (dd == NULL) {
		kprintf("FUZZ: %s: no device desc\n", name);
		return (1);
	}
	ncfg = dd->bNumConfigurations;
	for (cfg = 0; cfg != ncfg; cfg++) {
		const void *ptr;
		uint16_t l2;

		ptr = NULL;
		l2 = 0;
		{
			struct usb_device_request req;
			req.bmRequestType = UT_READ_DEVICE;
			req.bRequest = UR_GET_DESCRIPTOR;
			req.wValue[0] = cfg;
			req.wValue[1] = UDESC_CONFIG;
			USETW(req.wIndex, 0);
			USETW(req.wLength, 0xFFFF);
			err = (usb_temp_get_desc_p) (&fuzz_udev, &req,
			    &ptr, &l2);
		}
		if ((err != 0) || (ptr == NULL)) {
			kprintf("FUZZ: %s: config %u missing (err=%d)\n",
			    name, cfg, err);
			return (1);
		}
		buf = ptr;
		if (!walk_config(buf, l2)) {
			kprintf("FUZZ: %s: config %u walk mismatch "
			    "len=%u\n", name, cfg, l2);
			return (1);
		}
		wtot += l2;
	}
	if (wtot + sizeof(struct usb_temp_data) != expect) {
		kprintf("FUZZ: %s speed %d: SIZE MISMATCH served=%lu "
		    "expected=%lu\n", name, (int)spd,
		    (u_long)(wtot + sizeof(struct usb_temp_data)),
		    (u_long)expect);
		return (1);
	}
	/* index == bNumConfigurations must stall (no OOB walk) */
	do_req(UT_READ_DEVICE, UR_GET_DESCRIPTOR, UDESC_CONFIG, ncfg, 0);

	/* (a) full dispatch sweep */
	for (li = 0; li != sizeof(lang_tab)/sizeof(lang_tab[0]); li++)
		for (rt = 0; rt != sizeof(rt_tab); rt++)
			for (rq = 0; rq != sizeof(rq_tab); rq++)
				for (dt = 0; dt != 255; dt++)
					for (di = 0; di != 255; di++)
						do_req(rt_tab[rt],
						    rq_tab[rq], dt, di,
						    lang_tab[li]);
	return (0);
}

/* ------------------------------------------------------------------ */

static void
fuzz_run(void)
{
	const struct {
		const struct usb_temp_device_desc *tdd;
		const char *name;
	} tabs[] = {
		{ &usb_template_msc,		"msc" },
		{ &usb_template_cdce,		"cdce" },
		{ &usb_template_mtp,		"mtp" },
		{ &usb_template_modem,		"modem" },
		{ &usb_template_audio,		"audio" },
		{ &usb_template_kbd,		"kbd" },
		{ &usb_template_mouse,		"mouse" },
		{ &usb_template_serialnet,	"serialnet" },
		/* phone: unreachable via usb_temp_setup_by_index() (missing
		 * case) but buildable directly - proves the template itself
		 * is functional and only the dispatcher drops it. */
		{ &usb_template_phone,		"phone" },
	};
	const enum usb_dev_speed speeds[] = {
		USB_SPEED_FULL, USB_SPEED_HIGH, USB_SPEED_SUPER
	};
	enum usb_dev_speed spd;
	usb_error_t err;
	uint32_t bad = 0;
	uint16_t i;
	uint8_t badidx = 0;
	int t, s;

	fuzz_calls = fuzz_nonnull = fuzz_stalls = fuzz_anom = 0;
	fuzz_prints = 0;

	kprintf("FUZZ: usb_template dispatch sweep starting\n");

	/* (c) registry: valid indices build, invalid indices are rejected.
	 * NOTE: USB_TEMP_PHONE (7) is advertised by the enum in
	 * usb_ioctl.h and compiled into usb_template.ko, but
	 * usb_temp_setup_by_index() has no case for it: it returns
	 * USB_ERR_INVAL. Expected-dead (recorded as INFO finding). */
	for (i = 0; i != USB_TEMP_MAX; i++) {
		fuzz_prep(USB_SPEED_HIGH);
		err = (usb_temp_setup_by_index_p) (&fuzz_udev, i);
		if (i == USB_TEMP_PHONE) {
			kprintf("FUZZ: setup_by_index(PHONE=7) err=%d "
			    "(template compiled but unreachable - INFO)\n",
			    err);
			continue;
		}
		if (err != 0) {
			kprintf("FUZZ: setup_by_index(%u) err=%d "
			    "(unexpected)\n", i, err);
			badidx = 1;
		}
	}
	for (i = USB_TEMP_MAX; i != 1024; i++) {
		fuzz_prep(USB_SPEED_HIGH);
		err = (usb_temp_setup_by_index_p) (&fuzz_udev, i);
		if ((err != USB_ERR_INVAL) ||
		    (fuzz_udev.usb_template_ptr != NULL)) {
			kprintf("FUZZ: setup_by_index(%u) accepted invalid "
			    "index (err=%d)\n", i, err);
			badidx = 1;
			if (i > 40)
				break;
		}
	}
	{
		const uint16_t spikes[] = { 0x8000, 0xFF00, 0xFFFF };
		for (i = 0; i != 3; i++) {
			fuzz_prep(USB_SPEED_HIGH);
			err = (usb_temp_setup_by_index_p) (&fuzz_udev,
			    spikes[i]);
			if (err != USB_ERR_INVAL)
				kprintf("FUZZ: setup_by_index(0x%x) err=%d\n",
				    spikes[i], err), badidx = 1;
		}
	}

	/* (a)+(b)+(d): per template, per speed */
	for (t = 0; t != (int)(sizeof(tabs)/sizeof(tabs[0])); t++) {
		for (s = 0; s != (int)(sizeof(speeds)/sizeof(speeds[0])); s++) {
			spd = speeds[s];
			if (sweep_template(tabs[t].tdd, tabs[t].name, spd)) {
				bad++;
				/* keep going: gather more signal */
			}
			kprintf("FUZZ: template %s speed %d swept "
			    "(calls=%ju nonnull=%ju stall=%ju anom=%ju)\n",
			    tabs[t].name, (int)spd,
			    (uintmax_t)fuzz_calls,
			    (uintmax_t)fuzz_nonnull,
			    (uintmax_t)fuzz_stalls,
			    (uintmax_t)fuzz_anom);
		}
	}

	fuzz_prep(USB_SPEED_FULL);	/* leave clean state */

	if (bad || badidx || fuzz_anom) {
		kprintf("FUZZ: FAIL (bad=%u badidx=%u anom=%ju "
		    "calls=%ju)\n", bad, badidx, (uintmax_t)fuzz_anom,
		    (uintmax_t)fuzz_calls);
	} else {
		kprintf("FUZZ: PASS calls=%ju nonnull=%ju stalls=%ju "
		    "anomalies=0\n",
		    (uintmax_t)fuzz_calls, (uintmax_t)fuzz_nonnull,
		    (uintmax_t)fuzz_stalls);
	}
}

/* public accessor: usb_temp_get_device_desc() is static in
 * usb_template.c; use the registered demux instead. */
struct usb_device_descriptor *
usb_temp_get_device_desc_public(struct usb_device *udev)
{
	struct usb_device_request req;
	const void *ptr;
	uint16_t len;

	req.bmRequestType = UT_READ_DEVICE;
	req.bRequest = UR_GET_DESCRIPTOR;
	req.wValue[0] = 0;
	req.wValue[1] = UDESC_DEVICE;
	USETW(req.wIndex, 0);
	USETW(req.wLength, 0);

	ptr = NULL;
	len = 0;
	if ((usb_temp_get_desc_p) (udev, &req, &ptr, &len) != 0)
		return (NULL);
	return (__DECONST(struct usb_device_descriptor *, ptr));
}

static int
fuzz_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		lockinit(&fuzz_udev.enum_lock, "utfzenum", 0, LK_CANRECURSE);
		lockinit(&fuzz_udev.sr_lock, "utfzsr", 0, LK_CANRECURSE);
		fuzz_locks_inited = 1;
		fuzz_run();
		return (0);
	case MOD_UNLOAD:
		if (fuzz_udev.usb_template_ptr != NULL)
			usb_temp_unsetup_p(&fuzz_udev);
		if (fuzz_locks_inited) {
			lockuninit(&fuzz_udev.enum_lock);
			lockuninit(&fuzz_udev.sr_lock);
		}
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t fuzz_mod = {
	"usb_template_fuzz",
	fuzz_modevent,
	NULL
};

DECLARE_MODULE(usb_template_fuzz, fuzz_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
MODULE_DEPEND(usb_template_fuzz, usb_template, 1, 1, 1);
MODULE_VERSION(usb_template_fuzz, 1);
