/*
 * DF-4560 harness — proof of the fixed-offset event-member reads that run
 * without any validation that EventDataLength covers the structure, in
 * sys/dev/raid/mps/mps_sas_lsi.c mpssas_fw_work().
 *
 * Allocation replicated verbatim from mpssas_evt_handler
 * (mps_sas_lsi.c:140-148):
 *     u16 sz = le16toh(event->EventDataLength) * 4;   (u16 truncation = DF-1696)
 *     fw_event->event_data = kmalloc(sz, ...);
 *     bcopy(event->EventData, fw_event->event_data, sz);
 * For each EventDataLength EDL the accessible buffer is exactly EDL*4 bytes.
 *
 * Sinks replicated (offsets from the MPI2 headers):
 *   IR CONFIG CHANGE LIST  (struct at mpi2_ioc.h:699-710, element :672-680):
 *     event_data->Flags              u32 @ 0x04   (mps_sas_lsi.c:273)
 *     element->VolDevHandle          u16 @ 0x0A   (mps_sas_lsi.c:276-279 —
 *         read happens BEFORE the NumElements loop and regardless of
 *         NumElements, so DF-1697's loop fix does not cover it)
 *   LOG ENTRY ADDED        (struct at mpi2_ioc.h:516-528):
 *     logEntry->LogEntryQualifier    u16 @ 0x0E   (mps_sas_lsi.c:515)
 *     logEntry->LogData[0]           u8  @ 0x14   (mps_sas_lsi.c:518)
 *
 * Each case is fork()ed with the kmalloc'd buffer placed so its END abuts a
 * PROT_NONE guard page: an OOB member read faults (SIGSEGV) exactly as the
 * kernel would take a page fault / slab-boundary violation.
 *
 * Also prints (no deref, pure arithmetic):
 *   - the EDL table: allocation size vs. bytes each sink needs;
 *   - the DF-4561 (ir_shutdown off-by-one) IR-range coverage count:
 *         end_idx = start_idx + max_volumes - 1   (mps_sas_lsi.c:902)
 *         for (id = start_idx; id < end_idx; id++) (:904)
 *     -> visits max_volumes-1 slots, never id == end_idx: the LAST volume
 *        slot is skipped when looking for volumes to notify at shutdown.
 *
 * Build: cc -O2 -o df4560 df4560_harness.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>

/* struct member offsets (u8-accurate from the MPI2 headers) */
#define IR_FLAGS_OFF        0x04	/* u32 */
#define IR_VOLDEVHANDLE_OFF 0x0A	/* u16, ConfigElement[0]+2 */
#define LOG_QUAL_OFF        0x0E	/* u16 */
#define LOG_DATA0_OFF       0x14	/* u8  */

static char *
guarded_buf(size_t sz)
{
	size_t ps = 4096;
	size_t pages = (sz + ps - 1) / ps + 1;
	char *m = mmap(NULL, (pages + 1) * ps, PROT_READ | PROT_WRITE,
	    MAP_ANON | MAP_PRIVATE, -1, 0);
	if (m == MAP_FAILED) { perror("mmap"); exit(2); }
	if (mprotect(m + pages * ps, ps, PROT_NONE) != 0) { perror("mprotect"); exit(2); }
	return m + pages * ps - sz;	/* buffer end == guard start */
}

static int cases_run, cases_oob;

static void
case_ir(int edl_words)
{
	/* mpssas_fw_work IR CONFIG CHANGE LIST, before/inside the loop */
	char *event_data = guarded_buf((size_t)edl_words * 4);
	volatile uint32_t flags;
	volatile uint16_t volh;

	flags = *(volatile uint32_t *)(event_data + IR_FLAGS_OFF);      /* :273 */
	/* :276-279 — unconditional, happens even when NumElements == 0 */
	volh = *(volatile uint16_t *)(event_data + IR_VOLDEVHANDLE_OFF);
	(void)flags; (void)volh;
	printf("    EDL=%d (%d B): read completed in-bounds\n",
	    edl_words, edl_words * 4);
}

static void
case_log(int edl_words)
{
	/* mpssas_fw_work LOG ENTRY ADDED, WarpDrive branch (qualifier 0x8002) */
	char *event_data = guarded_buf((size_t)edl_words * 4);
	volatile uint16_t qual;
	volatile uint8_t code;

	if (edl_words >= 4)
		*(uint16_t *)(event_data + LOG_QUAL_OFF) = 0x8002; /*
		    MPI2_WD_LOG_ENTRY (mpsvar.h:111) — put the qualifier on-path
		    whenever writing it is itself in bounds */
	qual = *(volatile uint16_t *)(event_data + LOG_QUAL_OFF);       /* :515 */
	if (qual == 0x8002)
		code = *(volatile uint8_t *)(event_data + LOG_DATA0_OFF); /* :518 */
	(void)code;
	printf("    EDL=%d (%d B): read completed in-bounds\n",
	    edl_words, edl_words * 4);
}

static int
run(int (*should_fault)(int), void (*fn)(int), const char *tag, int edl)
{
	pid_t p = fork();
	if (p == 0) { fn(edl); _exit(0); }
	int st; waitpid(p, &st, 0);
	cases_run++;
	if (WIFSIGNALED(st)) {
		cases_oob += should_fault(edl) ? 1 : 0;
		printf("  [%s EDL=%d] SIGSEGV — %s\n", tag, edl,
		    should_fault(edl) ?
		    "OOB read past the EventDataLength-sized allocation (expected)" :
		    "UNEXPECTED fault");
		return 1;
	}
	printf("  [%s EDL=%d] clean — %s\n", tag, edl,
	    should_fault(edl) ? "UNEXPECTED: sink read OOB without fault" :
	    "in bounds (expected)");
	return 0;
}

static int ir_needs(int edl) { return edl * 4 < 12; }  /* VolDevHandle needs 12 */
static int log_needs(int edl) { return edl * 4 < 21; } /* LogData[0] needs 21 */

int
main(void)
{
	printf("== DF-4560: unvalidated fixed-offset event-member reads "
	    "(mps_sas_lsi.c:273,276-279,515,518) ==\n\n");
	printf("allocation: sz = EventDataLength*4 bytes (mps_sas_lsi.c:140-141)\n");
	printf("IR  sink needs >= 12 B (Flags@4..8 + VolDevHandle@10..12)\n");
	printf("LOG sink needs >= 21 B (Qualifier@14..16 + LogData[0]@20..21)\n\n");
	printf("-- IR CONFIG CHANGE LIST: element->VolDevHandle read at +0x0A --\n");
	run(ir_needs, case_ir, "IR ", 2);   /* 8 B alloc, read @10 — OOB */
	run(ir_needs, case_ir, "IR ", 3);   /* 12 B alloc — exact fit, control */
	printf("-- LOG ENTRY ADDED: LogData[0] read at +0x14 --\n");
	run(log_needs, case_log, "LOG", 1); /* 4 B — qualifier itself OOB */
	run(log_needs, case_log, "LOG", 4); /* 16 B — qualifier ok, LogData OOB */
	run(log_needs, case_log, "LOG", 6); /* 24 B — fits, control */

	printf("\n-- arithmetic table (no deref) --\n");
	for (int edl = 0; edl <= 6; edl++) {
		printf("    EDL=%d: alloc=%2d B | IR VolDevHandle@10 %s | "
		    "LOG Qualifier@14 %s | LOG LogData@20 %s\n",
		    edl, edl * 4,
		    edl * 4 < 12 ? "OOB" : "ok ",
		    edl * 4 < 16 ? "OOB" : "ok ",
		    edl * 4 < 21 ? "OOB" : "ok ");
	}

	printf("\n-- DF-4561 arithmetic: mpssas_ir_shutdown volume scan coverage "
	    "(mps_sas_lsi.c:902-911) --\n");
	unsigned max_volumes = 8, start_idx = 256, end_idx;
	unsigned visited = 0, last_visited;
	end_idx = start_idx + max_volumes - 1;
	for (unsigned id = start_idx; id < end_idx; id++) { visited++; last_visited = id; }
	printf("    IR range [%u..%u] (%u slots); loop visits %u, last visited "
	    "%u -> slot %u (the LAST volume slot) NEVER checked for a volume "
	    "before deciding whether to send SYSTEM_SHUTDOWN_INITIATED\n",
	    start_idx, end_idx, max_volumes, visited, last_visited, end_idx);
	printf("== %d cases, %d confirmed OOB ==\n", cases_run, cases_oob);
	return 0;
}
