/*
 * DF-4008 userspace replication — mpr diag-buffer registration state machine
 *
 * Reproduces, verbatim modulo bus_dma stubs, the state transitions of
 * mpr_diag_register() / mpr_diag_unregister() / mpr_diag_read_buffer()
 * (sys/dev/raid/mpr/mpr_user.c:1442-1698, 1771-1840) and demonstrates the
 * three failure shapes caused by the shared sc->fw_diag_{dmat,buffer,map}
 * trio being overwritten by a registration of a SECOND buffer type:
 *
 *   1. stale-size OOB read   (read bound uses list[TRACE].size while the
 *                              live allocation is SNAPSHOT's, smaller)
 *   2. cross-type free        (unregister of TRACE frees SNAPSHOT's memory)
 *   3. NULL-dmat deref / UAF  (subsequent SNAPSHOT read derefs freed or
 *                              NULL'd sc->fw_diag_dmat / fw_diag_buffer)
 *
 * Precondition (hardware-gated): the IOC advertises >=2 diag capabilities so
 * that >=2 fw_diag_buffer_list[] entries have enabled=TRUE (mpr.c:588-599)
 * and DIAG_BUFFER_POST succeeds for the first registration.
 */
#include <stdio.h>
#include <string.h>
#include <stdint.h>

#define	MPI2_DIAG_BUF_TYPE_TRACE		0
#define	MPI2_DIAG_BUF_TYPE_SNAPSHOT	1
#define	MPI2_DIAG_BUF_TYPE_COUNT		3
#define	MPR_FW_DIAG_INVALID_UID		0
#define	MPR_FW_DIAGNOSTIC_UID_NOT_FOUND	0xFF
#define	MPR_FW_DIAG_TYPE_REGISTER	1
#define	MPR_FW_DIAG_TYPE_UNREGISTER	2

typedef struct {
	uint32_t unique_id;
	int	enabled, immediate, valid_data, owned_by_firmware;
	int	force_release, extended_type, buffer_type;
	uint32_t size;
	uint32_t product_specific[23];
} fw_diag_buffer_t;

struct fake_sc {
	fw_diag_buffer_t list[MPI2_DIAG_BUF_TYPE_COUNT];
	/* shared trio — mprvar.h:410-413 */
	void	*fw_diag_buffer;
	uint64_t fw_diag_busaddr;
	void	*fw_diag_dmat;
	void	*fw_diag_map;
	/* model of the CURRENT live allocation */
	size_t	 live_alloc_sz;
	int	 live_alloc_freed;
	/* bus_dma op counters */
	int	 n_tag_create, n_tag_destroy, n_mem_alloc, n_mem_free;
	int	 post_succeeds;		/* modeled HW behavior */
};

static uint8_t
get_bufnum(struct fake_sc *sc, uint32_t uid)
{
	for (uint8_t i = 0; i < MPI2_DIAG_BUF_TYPE_COUNT; i++)
		if (sc->list[i].unique_id == uid)
			return (i);
	return (MPR_FW_DIAGNOSTIC_UID_NOT_FOUND);
}

/* stubbed mpr_post_fw_diag_buffer(): models a successful IOC post */
static int
post_ok(struct fake_sc *sc, fw_diag_buffer_t *b)
{
	if (!b->enabled || !sc->post_succeeds)
		return (1);
	b->valid_data = 1;
	b->owned_by_firmware = 1;
	return (0);
}

/* ---- verbatim mpr_diag_register() core (mpr_user.c:1442-1638) ---- */
static int
diag_register(struct fake_sc *sc, uint8_t extended_type, uint8_t buffer_type,
    uint32_t buffer_size, uint32_t unique_id)
{
	fw_diag_buffer_t *pBuffer;
	uint8_t i;
	int status;

	if (buffer_type >= MPI2_DIAG_BUF_TYPE_COUNT)
		return (1);
	i = get_bufnum(sc, unique_id);
	pBuffer = &sc->list[buffer_type];
	if (i != MPR_FW_DIAGNOSTIC_UID_NOT_FOUND)
		return (1);
	if ((pBuffer->unique_id != MPR_FW_DIAG_INVALID_UID) ||
	    (unique_id == MPR_FW_DIAG_INVALID_UID))
		return (1);
	if (pBuffer->immediate && pBuffer->owned_by_firmware &&
	    (pBuffer->unique_id == MPR_FW_DIAG_INVALID_UID)) {
		pBuffer->immediate = 0;
		pBuffer->unique_id = unique_id;
		return (0);
	}
	if (!pBuffer->enabled)
		return (1);
	/* bus_dma_tag_create(&sc->fw_diag_dmat) — mpr_user.c:1508 */
	sc->n_tag_create++;			/* OVERWRITES any prior tag */
	sc->fw_diag_dmat = (void *)0x2000;
	sc->fw_diag_map = (void *)0x3000;
	/* bus_dmamem_alloc(&sc->fw_diag_buffer) — 1523 */
	sc->n_mem_alloc++;			/* OVERWRITES any prior buffer */
	sc->fw_diag_buffer = (void *)0x1000;	/* new live allocation */
	sc->live_alloc_sz = buffer_size;
	sc->live_alloc_freed = 0;
	pBuffer->size = buffer_size;		/* 1596 */
	pBuffer->buffer_type = buffer_type;
	pBuffer->immediate = 0;
	pBuffer->extended_type = extended_type;
	pBuffer->unique_id = unique_id;		/* 1611 */
	status = post_ok(sc, pBuffer);		/* 1612 */
	if (status) {				/* bailout: 1619-1633 */
		sc->fw_diag_busaddr = 0;
		sc->fw_diag_buffer = NULL;
		sc->fw_diag_dmat = NULL;
		sc->live_alloc_freed = 1;
		sc->n_mem_free++; sc->n_tag_destroy++;
	}
	return (status);
}

/* ---- verbatim mpr_diag_unregister() core (mpr_user.c:1641-1698) ---- */
static int
diag_unregister(struct fake_sc *sc, uint32_t unique_id)
{
	fw_diag_buffer_t *pBuffer;
	uint8_t i;
	int status;

	i = get_bufnum(sc, unique_id);
	if (i == MPR_FW_DIAGNOSTIC_UID_NOT_FOUND)
		return (1);
	pBuffer = &sc->list[i];
	if (!pBuffer->owned_by_firmware)
		status = 0;
	else {
		/* modeled successful MPI2_FUNCTION_DIAG_RELEASE */
		status = 0;
		pBuffer->owned_by_firmware = 0;
	}
	pBuffer->unique_id = MPR_FW_DIAG_INVALID_UID;	/* 1680 */
	if (status == 0) {				/* 1681-1695 */
		/* frees the SHARED trio — whoever it belongs to now */
		sc->fw_diag_busaddr = 0;
		sc->fw_diag_buffer = NULL;
		sc->fw_diag_dmat = NULL;
		sc->live_alloc_freed = 1;
		sc->n_mem_free++; sc->n_tag_destroy++;
	}
	return (status);
}

/* ---- verbatim mpr_diag_read_buffer() core (mpr_user.c:1771-1815) ---- */
static int
diag_read(struct fake_sc *sc, uint32_t uid, uint32_t off, uint32_t bytes)
{
	fw_diag_buffer_t *pBuffer;
	uint8_t i;

	i = get_bufnum(sc, uid);
	if (i == MPR_FW_DIAGNOSTIC_UID_NOT_FOUND)
		return (1);
	pBuffer = &sc->list[i];
	if (off + bytes > pBuffer->size) {	/* 1798: the bound that lies */
		printf("  [read uid=%#x] bound REJECTED (off=%u bytes=%u > "
		    "size=%u)\n", uid, off, bytes, pBuffer->size);
		return (1);
	}
	/* 1805: bus_dmamap_sync(sc->fw_diag_dmat, ...) — dmat may be NULL */
	if (sc->fw_diag_dmat == NULL) {
		printf("  [read uid=%#x] bus_dmamap_sync(NULL) => KERNEL "
		    "NULL-DEREF PANIC here (mpr_user.c:1805)\n", uid);
		return (-1);
	}
	/* 1812-1814: pData = fw_diag_buffer + off; copyout(pData, ..., bytes) */
	if (sc->live_alloc_freed)
		printf("  [read uid=%#x] copyout from FREED allocation "
		    "(UAF read, mpr_user.c:1812)\n", uid);
	else if (off + bytes > sc->live_alloc_sz)
		printf("  [read uid=%#x] copyout OOB READ: bound check used "
		    "stale size=%u but live allocation is %zu bytes -> %u "
		    "bytes past the end disclosed to user (mpr_user.c:1798 "
		    "vs 1814)\n", uid, pBuffer->size, sc->live_alloc_sz,
		    (off + bytes) - (uint32_t)sc->live_alloc_sz);
	else
		printf("  [read uid=%#x] in-bounds of live alloc (unexpected)\n",
		    uid);
	return (0);
}

int
main(void)
{
	struct fake_sc sc;

	memset(&sc, 0, sizeof(sc));
	sc.post_succeeds = 1;		/* SAS3 FW accepts DIAG_BUFFER_POST */
	/* mpr.c:588-599: IOC advertises TRACE + SNAPSHOT -> both enabled */
	sc.list[MPI2_DIAG_BUF_TYPE_TRACE].enabled = 1;
	sc.list[MPI2_DIAG_BUF_TYPE_SNAPSHOT].enabled = 1;

	printf("== DF-4008 replication: two enabled diag buffer types ==\n");
	printf("1) REGISTER uid=0xA type=TRACE size=0x100000 (1MB)\n");
	if (diag_register(&sc, 0, MPI2_DIAG_BUF_TYPE_TRACE, 0x100000, 0xA)) {
		printf("   register A failed\n"); return (1);
	}
	printf("   TRACE.size=%u live_alloc=%zu\n",
	    sc.list[MPI2_DIAG_BUF_TYPE_TRACE].size, sc.live_alloc_sz);

	printf("2) REGISTER uid=0xB type=SNAPSHOT size=0x1000 (4KB)\n");
	if (diag_register(&sc, 0, MPI2_DIAG_BUF_TYPE_SNAPSHOT, 0x1000, 0xB)) {
		printf("   register B failed\n"); return (1);
	}
	printf("   tag_create=%d mem_alloc=%d (prior tag+buffer LEAKED); "
	    "live_alloc now %zu bytes, but TRACE.size is still %u\n",
	    sc.n_tag_create, sc.n_mem_alloc, sc.live_alloc_sz,
	    sc.list[MPI2_DIAG_BUF_TYPE_TRACE].size);

	printf("3) READ uid=0xA off=0 bytes=0x100000\n");
	diag_read(&sc, 0xA, 0, 0x100000);

	printf("4) UNREGISTER uid=0xA (TRACE)\n");
	diag_unregister(&sc, 0xA);
	printf("   shared trio freed: mem_free=%d tag_destroy=%d — this was "
	    "SNAPSHOT's allocation (cross-type free)\n",
	    sc.n_mem_free, sc.n_tag_destroy);

	printf("5) READ uid=0xB off=0 bytes=0x1000\n");
	diag_read(&sc, 0xB, 0, 0x1000);
	printf("== replication complete ==\n");
	return (0);
}
