/*
 * DF-4364 — mps_config.c polled-timeout: mps_command freed while the IOC
 * still owns the request -> late completion operates on freed/reallocated
 * object (UAF / cross-request completion confusion / reply-queue corruption).
 *
 * Kernel code mirrored (all citations from this tree):
 *
 *  sys/dev/raid/mps/mps.c:2140-2161  mps_request_polled():
 *        while ((cm->cm_flags & MPS_CM_FLAGS_COMPLETE) == 0) {
 *                mps_intr_locked(sc); DELAY(50*1000);
 *                if (timeout++ > 1000) { error = ETIMEDOUT; break; }   <-- returns
 *        }                                                              with the
 *                                                                        request still
 *                                                                        posted to the IOC
 *  sys/dev/raid/mps/mps_config.c  every getter, e.g. :143-151 + :165-168
 *        error = mps_request_polled(sc, cm);
 *        if (error || reply == NULL) { error = ENXIO; goto out; }
 *        ...
 *    out:
 *        kfree(page, M_MPT2);
 *        if (cm) mps_free_command(sc, cm);        <-- frees an in-flight cm
 *    (same pattern at 167, 311, 637, 762, 884, 1001, 1120, 1241, 1386)
 *
 *  sys/dev/raid/mps/mpsvar.h:514-540 mps_free_command():
 *        zeroes cm_reply/cm_flags/cm_complete/cm_data/... and puts the cm
 *        back on sc->req_list (FREE state). The SMID the IOC still owns is
 *        untouched — the request frame and reply slot stay outstanding.
 *
 *  sys/dev/raid/mps/mps.c:1581-1600 mps_intr_locked(), when the IOC finally
 *  completes the stale request (address reply):
 *        cm = &sc->commands[desc->AddressReply.SMID];   <-- same index, now
 *                                                          FREE or reallocated
 *        cm->cm_reply = reply;                          <-- write into a
 *                                                          foreign object
 *        cm->cm_reply_data = desc->AddressReply.ReplyFrameAddress;
 *        if (cm != NULL) mps_complete_command(cm);      <-- completes a command
 *                                                          that was never
 *                                                          submitted for it
 *  sys/dev/raid/mps/mps.c:1405-1431 mps_complete_command():
 *        POLLED -> sets COMPLETE; calls cm->cm_complete() of the *new* owner;
 *        wakeup(); io_cmds_active--.
 *  sys/dev/raid/mps/mpsvar.h:518-519 mps_free_command() when the confused
 *  owner later completes:
 *        if (cm->cm_reply != NULL) mps_free_reply(sc, cm->cm_reply_data);
 *        -> frees the STALE reply slot (double free / foreign free), or leaks
 *           it (SCSI_IO_SUCCESS path NULLs cm_reply at mps.c:1516 first).
 *
 * The newer mpr driver fixed exactly this lifecycle: mpr_wait_command()
 * (sys/dev/raid/mpr/mpr.c:3557-3622) takes struct mpr_command **cmp, and on
 * timeout calls mpr_reinit(sc) (diag reset -> the IOC can no longer complete
 * the stale request) and sets *cmp = NULL when the reinit freed the command,
 * so the caller never frees an in-flight command.
 *
 * The audit guest has no LSI mps HBA, so — as with DF-1556/DF-1557 — this
 * harness reproduces the kernel's exact object lifecycle and field semantics
 * in an unprivileged userland process and asserts the kernel invariants that
 * the code violates. Scenario A: the freed cm is reallocated as a SCSI I/O
 * before the late completion (cross-request completion + stale-reply double
 * free). Scenario B: the cm is still FREE at late completion (silent field
 * write onto a FREE object + reply-slot leak).
 *
 * FIXED mode models fix.diff (mps_request_polled performs mps_reinit() on
 * timeout, mirroring mpr_wait_command, so the chip is quiesced before the
 * caller frees): all invariants must hold.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ---- trimmed struct mps_command (mpsvar.h:320-360), relevant fields ---- */
struct mps_command {
	int		 cm_state;
#define MPS_CM_STATE_FREE	0
#define MPS_CM_STATE_BUSY	1
	u_int		 cm_flags;
#define MPS_CM_FLAGS_POLLED	(1 << 2)
#define MPS_CM_FLAGS_COMPLETE	(1 << 4)
#define MPS_CM_FLAGS_WAKEUP	(1 << 5)
	void		(*cm_complete)(void *sc, struct mps_command *);
	void		*cm_complete_data;
	void		*cm_reply;	/* MPI2 reply frame */
	uintptr_t	 cm_reply_data;	/* reply frame address */
	uint64_t	 owner_canary;	/* stands in for SCSI IO state (ccb...) */
	int		 io_function;	/* MPI2 function of THIS request */
};

/* reply free-queue model: each slot has an owner epoch, 0 = free */
#define NREPLY 64
static uintptr_t reply_slot_owner[NREPLY];
static uintptr_t req_epoch;		/* request generation counter */

static int mps_free_reply(uintptr_t addr, uintptr_t epoch, const char *who)
{
	int idx = (int)(addr & (NREPLY - 1));
	if (reply_slot_owner[idx] == 0) {
		printf("    !! REPLY DOUBLE-FREE: %s returns slot %d which is "
		    "already FREE (reply FIFO accounting corrupted)\n", who, idx);
		return -1;
	}
	if (reply_slot_owner[idx] != epoch) {
		printf("    !! REPLY FOREIGN-FREE: %s (request gen %zu) returns"
		    " slot %d owned by request gen %zu (reply FIFO accounting"
		    " corrupted)\n", who, (size_t)epoch, idx,
		    (size_t)reply_slot_owner[idx]);
		return -1;
	}
	reply_slot_owner[idx] = 0;
	return 0;
}
static void mps_alloc_reply(uintptr_t addr, uintptr_t epoch)
{
	reply_slot_owner[addr & (NREPLY - 1)] = epoch;
}

static int bad_double_free, bad_confusion, bad_stale_write;

/* ---- exact field semantics of mpsvar.h:514-540 / 543-555 ---- */
static struct mps_command commands[4];
static int req_list[4], req_n;		/* sc->req_list */
static int io_cmds_active;

static void mps_free_command(struct mps_command *cm, uintptr_t epoch,
    const char *who)
{
	if (cm->cm_reply != NULL) {
		if (mps_free_reply(cm->cm_reply_data, epoch, who) != 0)
			bad_double_free++;
	}
	cm->cm_reply = NULL;
	cm->cm_flags = 0;
	cm->cm_complete = NULL;
	cm->cm_complete_data = NULL;
	cm->cm_state = MPS_CM_STATE_FREE;
	req_list[req_n++] = (int)(cm - commands);
}

static struct mps_command *mps_alloc_command(void)
{
	struct mps_command *cm;
	if (req_n == 0)
		return NULL;
	cm = &commands[req_list[--req_n]];
	cm->cm_state = MPS_CM_STATE_BUSY;	/* mpsvar.h:553 */
	return cm;
}

/* mps.c:1405-1431 */
static void mps_complete_command(struct mps_command *cm)
{
	if (cm->cm_flags & MPS_CM_FLAGS_POLLED)
		cm->cm_flags |= MPS_CM_FLAGS_COMPLETE;
	if (cm->cm_complete != NULL)
		cm->cm_complete(NULL, cm);	/* new owner's callback! */
	if (cm->cm_flags & MPS_CM_FLAGS_WAKEUP)
		/* wakeup(cm) */ ;
	if (io_cmds_active != 0)
		io_cmds_active--;
}

/* the SCSI IO completion callback the NEW owner installed.
 * mps.c:1582 stores the stale CONFIG reply frame on this cm before the
 * callback runs, so the SCSI completion path misparses it. */
static void *g_config_reply_frame;
static void mpssas_scsiio_complete(void *sc, struct mps_command *cm)
{
	(void)sc;
	if (cm->cm_reply == g_config_reply_frame) {
		printf("    !! CROSS-REQUEST COMPLETION: SCSI IO completion"
		    " callback invoked with the stale CONFIG reply frame %p"
		    " (reply is misparsed as MPI2_SCSI_IO_REPLY)\n",
		    cm->cm_reply);
		bad_confusion++;
	}
}

/* the "firmware": model of an IOC that takes >50s and then completes */
static int late_smid;			/* SMID the IOC still owns */
static int late_reply_addr;		/* its reply frame address */
static int ioc_has_late_completion;
static uintptr_t late_reply_slot_epoch;	/* request gen that owns the frame */

/* mps.c:2140-2161 */
static int mps_request_polled(struct mps_command *cm, int fw_hangs)
{
	int timeout = 0;
	cm->cm_flags |= MPS_CM_FLAGS_POLLED;
	cm->cm_complete = NULL;
	/* mps_map_command + mps_enqueue_request happen here: the SMID is now
	 * owned by the IOC */
	late_smid = (int)(cm - commands);
	if (!fw_hangs) {
		cm->cm_flags |= MPS_CM_FLAGS_COMPLETE;
		return 0;
	}
	while ((cm->cm_flags & MPS_CM_FLAGS_COMPLETE) == 0) {
		if (timeout++ > 3) {
			/* mps: just breaks out — request still outstanding! */
			return 124; /* ETIMEDOUT stand-in */
		}
	}
	return 0;
}

/* mps.c:1518-1585 (address reply for the stale SMID) */
static void late_ioc_completion(void)
{
	struct mps_command *cm;
	uintptr_t reply_addr = 0x1000 + (late_reply_addr & (NREPLY - 1)) * 16;
	void *reply_frame = g_config_reply_frame;

	if (!ioc_has_late_completion)
		return;
	ioc_has_late_completion = 0;
	cm = &commands[late_smid];		/* mps.c:1581 */
	if (cm->cm_state == MPS_CM_STATE_FREE)
		printf("    [late completion lands on a FREE cm]\n");
	else if (cm->io_function == 0x1A)
		printf("    [late completion lands on the still-owned CONFIG cm"
		    " (safe: same owner)]\n");
	else
		printf("    [late completion lands on a BUSY cm owned by "
		    "another request]\n");
	cm->cm_reply = reply_frame;		/* mps.c:1582 — foreign write */
	cm->cm_reply_data = reply_addr;		/* mps.c:1583 */
	mps_complete_command(cm);		/* mps.c:1600 */
}

/* fix.diff model: mps_request_polled() calls mps_reinit(sc) on timeout;
 * the diag reset reinitializes the queues and the IOC discards all
 * outstanding requests — a late completion becomes impossible. */
#ifdef FIXED
static void mps_reinit_model(void)
{
	ioc_has_late_completion = 0;
}
#endif

static void reset_world(void)
{
	int i;
	memset(commands, 0, sizeof(commands));
	memset(reply_slot_owner, 0, sizeof(reply_slot_owner));
	req_n = 0;
	for (i = 3; i >= 0; i--)
		req_list[req_n++] = i;	/* TAILQ_INSERT_TAIL order */
	req_n = 4;
	io_cmds_active = 0;
	bad_double_free = bad_confusion = bad_stale_write = 0;
	ioc_has_late_completion = 0;
	g_config_reply_frame = NULL;
}

int main(void)
{
	struct mps_command *cmA, *cmB;
	uintptr_t cfg_reply_addr = 0x1000 + 5 * 16;	/* slot 5 */
	uintptr_t cfg_epoch, scsi_epoch;
	int violations = 0;

	printf("== DF-4364 harness: free-while-in-flight after mps_request_polled"
	    " timeout ==\n\n");

	/* -------- Scenario A: cm reallocated as SCSI IO before the IOC
	 * --------            finally completes the stale config request   */
	printf("Scenario A: timed-out config cm freed, then reallocated as SCSI"
	    " IO\n");
	reset_world();
	g_config_reply_frame = (void *)(uintptr_t)0xfeed0000; /* CONFIG reply */
	cfg_epoch = ++req_epoch;

	cmA = mps_alloc_command();
	cmA->io_function = 0x1A /* MPI2_FUNCTION_CONFIG */;
	/* config request takes reply free-queue frame slot 5 */
	mps_alloc_reply(cfg_reply_addr, cfg_epoch);
	io_cmds_active++;
	ioc_has_late_completion = 1;
	late_reply_addr = 5;
	late_reply_slot_epoch = cfg_epoch;

	if (mps_request_polled(cmA, /*fw_hangs*/1) == 124) {
		printf("  mps_request_polled: ETIMEDOUT after ~50s "
		    "(mps.c:2153-2156); request still posted to IOC\n");
#ifdef FIXED
		/* fix.diff: mps_reinit() diag-resets the chip; the stale
		 * request can no longer complete. Caller free is now safe. */
		mps_reinit_model();
		mps_free_command(cmA, cfg_epoch, "config-getter out: (after reinit)");
		printf("  [FIXED] reinit-on-timeout, then free: safe\n");
#else
		/* mps_config.c out: path */
		mps_free_command(cmA, cfg_epoch, "config-getter out:");
		printf("  mps_config.c out: freed the in-flight cm "
		    "(mpsvar.h:514) — SMID %d still owned by IOC\n",
		    late_smid);
#endif
	}

	/* the freed cm is immediately reused for unrelated SCSI IO */
	scsi_epoch = ++req_epoch;
	cmB = mps_alloc_command();
#ifndef FIXED
	if (cmB != cmA) { printf("unexpected free-list order\n"); exit(2); }
#endif
	cmB->io_function = 0x00;	/* SCSI_IO_REQUEST */
	cmB->cm_complete = mpssas_scsiio_complete;
	cmB->cm_flags = MPS_CM_FLAGS_WAKEUP;
	cmB->owner_canary = 0xCAFEBABEDEADBEEFULL;
	io_cmds_active++;

	/* ... 80 seconds later the wedged IOC resets itself and finally
	 * completes the stale CONFIG request: */
	printf("  IOC late-completes the stale request: "
	    "mps_intr_locked mps.c:1581-1600\n");
	late_ioc_completion();

	if (cmB->owner_canary != 0xCAFEBABEDEADBEEFULL) {
		printf("    !! UAF WRITE onto live object state\n");
		bad_stale_write++;
	}
	if (cmB->cm_reply == g_config_reply_frame) {
		printf("    !! FOREIGN FIELDS ON LIVE SCSI IO COMMAND: "
		    "cm_reply=%p cm_reply_data=0x%zx installed by the stale "
		    "CONFIG completion\n", cmB->cm_reply, cmB->cm_reply_data);
		bad_stale_write++;
	}
	/* the SCSI IO's own lifecycle later completes and frees it: the
	 * stale cm_reply/cm_reply_data now cause a foreign reply-slot free */
	mps_free_command(cmB, scsi_epoch, "scsiio free");
	if (bad_confusion || bad_double_free || bad_stale_write) {
		printf("  >>> VIOLATIONS CONFIRMED (cross-request completion:"
		    " %d, reply accounting corruption: %d, foreign/UAF writes:"
		    " %d)\n", bad_confusion, bad_double_free, bad_stale_write);
		violations++;
	} else {
		printf("  invariants hold\n");
	}

	/* -------- Scenario B: cm still FREE when the late completion hits -- */
	printf("\nScenario B: timed-out config cm freed, still FREE at late"
	    " completion\n");
	reset_world();
	cfg_epoch = ++req_epoch;
	g_config_reply_frame = (void *)(uintptr_t)0xfeed0000;

	cmA = mps_alloc_command();
	cmA->io_function = 0x1A;
	mps_alloc_reply(cfg_reply_addr, cfg_epoch);
	io_cmds_active++;
	ioc_has_late_completion = 1;
	late_reply_addr = 5;
	late_reply_slot_epoch = cfg_epoch;

	if (mps_request_polled(cmA, 1) == 124) {
#ifdef FIXED
		mps_reinit_model();
#endif
		mps_free_command(cmA, cfg_epoch, "config-getter out:");
	}
	printf("  IOC late-completes the stale request onto the cm\n");
	late_ioc_completion();
	if (cmA->cm_state == MPS_CM_STATE_FREE && cmA->cm_reply != NULL) {
		printf("    !! STALE FIELDS ON FREE OBJECT: FREE cm now carries"
		    " cm_reply=%p cm_reply_data=0x%zx\n",
		    cmA->cm_reply, cmA->cm_reply_data);
		printf("    !! reply slot %d is now DOUBLE-BOOKED: next"
		    " mps_alloc_command() hands this cm out; if the new request"
		    " finishes via the SCSI_IO_SUCCESS path (cm_reply=NULLed,"
		    " mps.c:1516) the slot is leaked -> reply queue exhaustion;"
		    " if via free_command it is double-freed\n",
		    (int)(cmA->cm_reply_data & (NREPLY - 1)));
		violations++;
	} else {
		printf("  invariants hold\n");
	}

	printf("\n== verdict: %d scenario(s) with kernel-invariant violations"
	    " ==\n", violations);
#ifndef FIXED
	return violations ? 1 : 0;
#else
	if (violations) { printf("FIXED mode must not violate invariants\n"); return 1; }
	printf("FIXED mode: reinit-on-timeout (mpr-style) prevents the UAF\n");
	return 0;
#endif
}
