/*
 * DF-5061 -- PoC harness: ida_v4_done() empty-FIFO sentinel is dead on LP64.
 *
 * File under audit : sys/dev/raid/ida/ida_pci.c (pass 2)
 * Bug              : ida_v4_done() at ida_pci.c:107 tests
 *                         if (completed == -1)
 *                     with `bus_addr_t completed' (uint64_t on x86_64,
 *                     sys/cpu/x86_64/include/bus_dma.h:36) assigned from
 *                     ida_inl() == bus_space_read_4() which returns
 *                     u_int32_t (sys/cpu/x86_64/include/bus_dma.h:130).
 *                     The empty-reply-FIFO value 0xFFFFFFFF zero-extends to
 *                     0x00000000FFFFFFFF which can never equal
 *                     0xFFFFFFFFFFFFFFFF: the "fifo is empty" test is dead
 *                     code on every 64-bit DragonFly.
 *
 * Consequence      : ida_wait() (ida.c:457) exits its poll loop immediately
 *                     on an empty FIFO with completed=0xFFFFFFFF, and
 *                     ida_intr() (ida.c:482) enters its drain loop for the
 *                     same reason; both feed the value to
 *                     idahwqcbptov() (ida.c:140-148):
 *                         hwqcb = hwqcbs + (0xFFFFFFFC - hwqcb_busaddr)
 *                     and DEREFERENCE hwqcb->qcb.  hwqcbs is DMAP memory
 *                     and hwqcb_busaddr is its (32-bit) physical address,
 *                     so the arithmetic cancels to exactly
 *                         DMAP + 0xFFFFFFFC   (physical 4GB-4)
 *                     -- on any host with < 4GB RAM this is an unmapped
 *                     DMAP page => kernel page fault panic, deterministically,
 *                     on the first ida_command() of every Compaq 42xx-class
 *                     board attach (and in every ida_intr() drain).
 *
 * What this module does
 * ---------------------
 * It runs VERBATIM copies of ida_v4_done() and idahwqcbptov() against a mock
 * 32-bit register file whose R_42XX_REPLY slot holds 0xFFFFFFFF -- exactly
 * what a 42xx board presents on an empty reply FIFO (the sole condition the
 * `completed == -1' check exists to catch; it worked on 32-bit FreeBSD where
 * bus_addr_t == uint32_t).
 *
 *   hw.df5061.mode = 1   fixed variant: (uint32_t)completed == 0xffffffff
 *                       -> done() returns 0 -> ida_wait keeps polling /
 *                          ida_intr drains and exits: CORRECT, no fault.
 *   hw.df5061.mode = 2   verbatim upstream variant -> done() returns
 *                       0xFFFFFFFF as a "completion" -> idahwqcbptov()
 *                       computes the wild pointer -> we perform the driver's
 *                       very next instruction (read of qcb->state,
 *                       ida.c:485) => page fault panic.
 *
 *   sysctl hw.df5061.fire=1   runs the demo for the current mode.
 *
 * Only the board's register file is mocked; every byte of kernel-side logic
 * (types, macros, arithmetic, deref) is the shipping driver's code.
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/types.h>
#include <sys/malloc.h>
#include <sys/module.h>
#include <sys/sysctl.h>
#include <sys/proc.h>
#include <sys/queue.h>
#include <sys/buf.h>
#include <sys/bus.h>
#include <sys/devicestat.h>
#include <sys/disk.h>
#include <sys/rman.h>
#include <sys/thread2.h>

#include <machine/bus_dma.h>		/* X86_64_BUS_SPACE_MEM, bus_space_* */

/* sys/vm/pmap.h:233 (declared locally to avoid header tangles) */
extern vm_paddr_t pmap_kextract(vm_offset_t va);

#include "idareg.h"
#include "idavar.h"

/* =====================================================================
 * VERBATIM from sys/dev/raid/ida/ida_pci.c:101-111  (the bug)
 * ===================================================================== */
static bus_addr_t
ida_v4_done(struct ida_softc *ida)
{
	bus_addr_t completed;

	completed = ida_inl(ida, R_42XX_REPLY);
	if (completed == -1)
		return (0);			/* fifo is empty */
	ida_outl(ida, R_42XX_REPLY, 0);		/* confirm read */
	return (completed);
}

/* =====================================================================
 * VERBATIM from sys/dev/raid/ida/ida.c:140-148  (the consumer)
 * ===================================================================== */
static struct ida_qcb *
idahwqcbptov(struct ida_softc *ida, bus_addr_t hwqcb_addr)
{
	struct ida_hardware_qcb *hwqcb;

	hwqcb = (struct ida_hardware_qcb *)
	    ((bus_addr_t)ida->hwqcbs + (hwqcb_addr - ida->hwqcb_busaddr));
	return (hwqcb->qcb);
}

/* =====================================================================
 * DF-5061 fixed variant (identical except the LP64-safe compare)
 * ===================================================================== */
static bus_addr_t
ida_v4_done_fixed(struct ida_softc *ida)
{
	bus_addr_t completed;

	completed = ida_inl(ida, R_42XX_REPLY);
	if ((uint32_t)completed == 0xffffffffU)	/* DF-5061 FIX */
		return (0);			/* fifo is empty */
	ida_outl(ida, R_42XX_REPLY, 0);		/* confirm read */
	return (completed);
}

/* ================== mock board / demo state ========================= */

static struct ida_softc ida_state;		/* static: zeroed */
static uint8_t *regfile;			/* mock 32-bit register file */
static int df5061_mode = 1;			/* 1=fixed 2=buggy(verbatim) */
static int df5061_fire;

static void
df5061_setup(void)
{
	int i;

	/* mock MMIO register file: everything reads 0 except the reply FIFO */
	regfile = kmalloc(0x100, M_DEVBUF, M_WAITOK | M_ZERO);
	*(volatile uint32_t *)(regfile + R_42XX_REPLY) = 0xffffffffU;

	ida_state.tag = X86_64_BUS_SPACE_MEM;
	ida_state.bsh = (bus_space_handle_t)(uintptr_t)regfile;

	/* hardware QCB array + its bus address, as ida_init() sets them up */
	ida_state.hwqcbs = kmalloc(IDA_QCB_MAX *
	    sizeof(struct ida_hardware_qcb), M_DEVBUF, M_WAITOK | M_ZERO);
	ida_state.hwqcb_busaddr =
	    (bus_addr_t)pmap_kextract((vm_offset_t)ida_state.hwqcbs);

	ida_state.qcbs = kmalloc(IDA_QCB_MAX * sizeof(struct ida_qcb),
	    M_DEVBUF, M_WAITOK | M_ZERO);

	/* identity of qcb #0, exactly as ida_alloc_qcb() wires it */
	ida_state.qcbs[0].hwqcb = &ida_state.hwqcbs[0];
	ida_state.qcbs[0].hwqcb_busaddr = ida_state.hwqcb_busaddr;
	ida_state.hwqcbs[0].qcb = &ida_state.qcbs[0];
	ida_state.qcbs[0].state = QCB_FREE;
	ida_state.num_qcbs = 1;

	kprintf("DF-5061: mock board up: hwqcbs=%p phys=%08jx "
	    "(VA-phys delta=%08jx) regfile=%p R_42XX_REPLY@%p=0x%08x\n",
	    ida_state.hwqcbs,
	    (uintmax_t)ida_state.hwqcb_busaddr,
	    (uintmax_t)((vm_offset_t)ida_state.hwqcbs -
	    (vm_offset_t)ida_state.hwqcb_busaddr),
	    regfile, regfile + R_42XX_REPLY,
	    *(volatile uint32_t *)(regfile + R_42XX_REPLY));

	(void)i;
}

static void
df5061_demo_fixed(void)
{
	bus_addr_t r;

	kprintf("DF-5061 [fixed ] (uint32_t)completed==0xffffffff test:\n");
	r = ida_v4_done_fixed(&ida_state);
	kprintf("DF-5061 [fixed ] ida_v4_done_fixed() = 0x%016jx "
	    "(0 = empty FIFO detected: ida_wait() keeps polling, "
	    "ida_intr() drain loop exits -- correct, no wild deref)\n",
	    (uintmax_t)r);
	kprintf("DF-5061 [fixed ] R_42XX_REPLY untouched: 0x%08x "
	    "(no 'confirm read' write on an empty FIFO)\n",
	    *(volatile uint32_t *)(regfile + R_42XX_REPLY));
}

static void
df5061_demo_buggy(void)
{
	bus_addr_t completed;
	struct ida_qcb *qcb;
	qcb_state st;

	kprintf("DF-5061 [buggy ] verbatim ida_pci.c:107 test "
	    "'completed == -1' with bus_addr_t=uint%zu_t and "
	    "bus_space_read_4=uint32_t:\n", sizeof(bus_addr_t) * 8);
	kprintf("DF-5061 [buggy ]   0x00000000FFFFFFFF == "
	    "0xFFFFFFFFFFFFFFFF ? NO -> sentinel DEAD\n");

	completed = ida_v4_done(&ida_state);
	kprintf("DF-5061 [buggy ] ida_v4_done() = 0x%016jx  "
	    "(empty FIFO reported as a completion!)\n", (uintmax_t)completed);
	kprintf("DF-5061 [buggy ] side effect: 'confirm read' write done on "
	    "EMPTY fifo, R_42XX_REPLY now 0x%08x\n",
	    *(volatile uint32_t *)(regfile + R_42XX_REPLY));

	/* ida_wait(), ida.c:457:  while ((completed = done(ida)) == 0) ...
	 * exits instantly with completed = 0xFFFFFFFF; ida_intr(), ida.c:482:
	 * while ((completed = done(ida)) != 0) ... loops forever on it.  Both
	 * then do exactly this: */
	qcb = idahwqcbptov(&ida_state, completed & ~3);
	kprintf("DF-5061 [buggy ] idahwqcbptov(0x%016jx) -> %p "
	    "(= hwqcbs + (0xFFFFFFFC - hwqcb_busaddr); with DMAP-mapped hwqcbs "
	    "this is DMAP+0xFFFFFFFC, physical 4GB-4)\n",
	    (uintmax_t)(completed & ~3), qcb);
	kprintf("DF-5061 [buggy ] physmem=%ju bytes (<4GB => target unmapped). "
	    "Performing the driver's next instruction, "
	    "ida_intr() ida.c:485 'qcb->state != QCB_ACTIVE' ...\n",
	    (uintmax_t)(unsigned long)physmem * PAGE_SIZE);

	/* THE WILD DEREF -- genuine page fault follows on <4GB hosts */
	st = *(volatile qcb_state *)&qcb->state;
	kprintf("DF-5061 [buggy] SURVIVED deref: state=%d "
	    "(mapped garbage: driver compares and spins forever)\n", (int)st);
}

static void
df5061_demo_probe(void)
{
	bus_addr_t completed;
	struct ida_qcb *qcb;
	vm_offset_t qva;
	vm_paddr_t qphys;

	completed = ida_v4_done(&ida_state);
	qcb = idahwqcbptov(&ida_state, completed & ~3);
	qva = (vm_offset_t)qcb;
	qphys = pmap_kextract(qva);

	kprintf("DF-5061 [probe ] ida_v4_done() = 0x%016jx on empty FIFO\n",
	    (uintmax_t)completed);
	kprintf("DF-5061 [probe ] idahwqcbptov() -> qcb=%p "
	    "(hwqcbs=%p + (0xFFFFFFFC - busaddr %08jx))\n",
	    qcb, ida_state.hwqcbs, (uintmax_t)ida_state.hwqcb_busaddr);
	kprintf("DF-5061 [probe ] pmap_kextract(%p) = %08jx  -> %s\n",
	    (void *)qva, (uintmax_t)qphys,
	    qphys == 0 ? "UNMAPPED (deref == page fault)" :
	    "mapped (garbage; driver would compare/spin forever)");
	kprintf("DF-5061 [probe ] NOT dereferencing in probe mode "
	    "(mode=2 performs the deref)\n");
}

static int
df5061_fire_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, t;

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

	if (df5061_mode == 1)
		df5061_demo_fixed();
	else if (df5061_mode == 2)
		df5061_demo_buggy();	/* panics on <4GB hosts */
	else if (df5061_mode == 3)
		df5061_demo_probe();	/* arithmetic only, no deref */
	else
		kprintf("DF-5061: unknown mode %d\n", df5061_mode);

	return (0);
}

static int
df5061_load(module_t mod, int what, void *arg)
{
	switch (what) {
	case MOD_LOAD:
		SYSCTL_ADD_INT(NULL, SYSCTL_STATIC_CHILDREN(_hw),
		    OID_AUTO, "df5061_mode", CTLFLAG_RW,
		    &df5061_mode, 0, "1=fixed variant, 2=verbatim (panics)");
		SYSCTL_ADD_PROC(NULL, SYSCTL_STATIC_CHILDREN(_hw),
		    OID_AUTO, "df5061_fire", CTLTYPE_INT | CTLFLAG_RW,
		    NULL, 0, df5061_fire_sysctl, "I",
		    "write 1 to fire the current mode's demo");
		df5061_setup();
		kprintf("DF-5061: loaded (mode=%d). "
		    "sysctl hw.df5061.fire=1 to run.\n", df5061_mode);
		break;
	case MOD_UNLOAD:
		if (regfile != NULL)
			kfree(regfile, M_DEVBUF);
		if (ida_state.hwqcbs != NULL)
			kfree(ida_state.hwqcbs, M_DEVBUF);
		if (ida_state.qcbs != NULL)
			kfree(ida_state.qcbs, M_DEVBUF);
		kprintf("DF-5061: unloaded\n");
		break;
	default:
		break;
	}
	return (0);
}

static moduledata_t df5061_mod = {
	"df5061",
	df5061_load,
	NULL
};
DECLARE_MODULE(df5061, df5061_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
