/*
 * DF-5042 / DF-5043 PoC wrapper — replay the stock adv_pci_attach()
 * (verbatim sys/dev/disk/advansys/adv_pci.c, #included below) against a
 * scripted AdvanSys PCI chip, and observe what the attach error unwinds
 * destroy / release.
 *
 * Two scenarios (kenv-selected):
 *
 *   default            : REAL adv_init() is used.  The fake chip window has
 *                        the AdvanSys signature planted and CHIP_STATUS
 *                        (offset 0x0E) stays 0, so adv_init() deterministi-
 *                        cally fails at its first halt check
 *                        ("Unable to halt adapter", advansys.c:794-798)
 *                        ->  adv_pci.c:260-264 unwind runs.
 *
 *   kenv df5042.irqfail: fk_adv_init() scripts success so attach proceeds
 *                        to the IRQ stage; fk_bus_setup_intr() fails
 *                        ->  adv_pci.c:289-294 unwind runs.
 *
 * Everything else is the unmodified driver code: the real adv_find_signature
 * reads the planted signature, the real bus_dma_tag_create/bus_dmamem_alloc/
 * bus_dmamap_load build the real parent_dmat and the shared overrun buffer,
 * and the real adv_free() executes the init_level switch.
 *
 * Expected baseline output (the findings):
 *   mode 1: bus_dma_tag_destroy calls for parent_dmat == 0   *** LEAK (DF-5042)
 *   mode 2: parent_dmat destroy == 0 AND bus_release_resource for the IRQ
 *           resource == 0                                    *** LEAKS (DF-5042+5043)
 * With fix.diff applied to adv_pci.c the same replay shows
 *   parent_dmat destroy == 1 and IRQ release == 1.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/rman.h>

#include <sys/bus.h>
#include <machine/bus_dma.h>
#include <machine/pmap.h>
#include <vm/pmap.h>
#include <vm/vm_extern.h>
#include <vm/vm_kern.h>

#include "fkdecl.h"

/*
 * Pull in <bus/pci/pcivar.h> BEFORE the renames below: pcivar.h defines
 * pci_read_config()/pci_write_config() as static __inline wrappers, which
 * would collide with our shim definitions if first seen under the renames.
 */
#include <bus/pci/pcireg.h>
#include <bus/pci/pcivar.h>


#define	FAKE_BAR_PHYS	0x60000000UL	/* real RAM on this guest (4GB) */
#define	WIN_SIZE	0x1000

static struct resource	fake_iores;
static struct resource	fake_irqres;
static struct adv_softc	fake_softc;
static bus_space_tag_t	ftag;
static bus_space_handle_t fwin;

static bus_dma_tag_t	t_parent;	/* captured from fk tag_create #1 */
static bus_dma_tag_t	t_overrun;	/* captured from fk tag_create #2 */
static int		cnt_create;
static int		cnt_destroy_parent;
static int		cnt_destroy_overrun;
static int		cnt_rel_io;
static int		cnt_rel_irq;
static int		cnt_pci_write;
static int		cnt_setup_intr;
static int		mode_irqfail;

/* ---------------- observation shims (all forward to the real thing) ----- */

u_int32_t
fk_pci_read_config(device_t dev, int reg, int width)
{
	switch (reg) {
	case PCIR_DEVVENDOR:
		return (0x110010CD);	/* PCI_DEVICE_ID_ADVANSYS_1200A */
	case PCIR_COMMAND:
		return (0);		/* force the PORTEN/BUSMASTEREN write */
	default:
		return (0);
	}
}

void
fk_pci_write_config(device_t dev, int reg, uint32_t val, int width)
{
	cnt_pci_write++;
}

struct resource *
fk_bus_alloc_resource(device_t dev, int type, int *rid, u_long start,
    u_long end, u_long count, u_int flags)
{
	if (type == SYS_RES_IOPORT) {
		fake_iores.r_bustag = ftag;
		fake_iores.r_bushandle = fwin;
		kprintf("FK: bus_alloc_resource(IOPORT) -> fake io resource "
		    "(tag=%p h=%p)\n", (void *)(uintptr_t)ftag, (void *)fwin);
		return (&fake_iores);
	}
	if (type == SYS_RES_IRQ) {
		kprintf("FK: bus_alloc_resource(IRQ) -> fake irq resource %p\n",
		    &fake_irqres);
		return (&fake_irqres);
	}
	return (NULL);
}

int
fk_bus_release_resource(device_t dev, int type, int rid, struct resource *res)
{
	if (res == &fake_iores) {
		cnt_rel_io++;
		kprintf("FK: bus_release_resource(IOPORT)\n");
	} else if (res == &fake_irqres) {
		cnt_rel_irq++;
		kprintf("FK: bus_release_resource(IRQ)\n");
	}
	return (0);
}

int
fk_bus_setup_intr(device_t dev, struct resource *r, int flags,
    driver_intr_t handler, void *arg, void **cookiep,
    lwkt_serialize_t serializer)
{
	cnt_setup_intr++;
	kprintf("FK: bus_setup_intr -> scripted %s\n",
	    mode_irqfail ? "FAILURE" : "success");
	return (mode_irqfail ? 1 : 0);
}

struct adv_softc *
fk_adv_alloc(device_t dev, bus_space_tag_t tag, bus_space_handle_t bsh)
{
	/*
	 * adv_alloc() verbatim except the softc is our storage instead of
	 * device_get_softc(dev) (dev is the real pci0 device, whose softc
	 * belongs to the PCI bridge).
	 */
	bzero(&fake_softc, sizeof(fake_softc));
	LIST_INIT(&fake_softc.pending_ccbs);
	SLIST_INIT(&fake_softc.free_ccb_infos);
	fake_softc.dev = dev;
	fake_softc.unit = 0;
	fake_softc.tag = tag;
	fake_softc.bsh = bsh;
	kprintf("FK: adv_alloc(dev=%p, tag=%p, h=%p) -> softc %p\n",
	    dev, (void *)(uintptr_t)tag, (void *)bsh, &fake_softc);
	return (&fake_softc);
}

int
fk_adv_init(struct adv_softc *adv)
{
	if (mode_irqfail) {
		kprintf("FK: adv_init -> scripted SUCCESS (reaching IRQ "
		    "stage)\n");
		return (0);
	}
	kprintf("FK: adv_init -> REAL adv_init() against scripted chip\n");
	return (adv_init(adv));	/* the real thing, advansys.c */
}

int
fk_find_signature(bus_space_tag_t tag, bus_space_handle_t bsh)
{
	/*
	 * Forced pass.  The real adv_find_signature() reads byte 0x01
	 * (== 0x25) AND word 0x00 (== 0x04C1/0x00C1) — two reads of the
	 * same two bytes that cannot both be satisfied by a passive RAM
	 * window; real silicon latches the signature registers differently.
	 * The signature check is not what this PoC tests, so it is forced
	 * here and the code under test (dma setup + failure unwinds) runs
	 * exactly as on a real card.
	 */
	kprintf("FK: adv_find_signature -> forced pass (RAM window cannot "
	    "emulate the dual-width signature latch)\n");
	return (1);
}

int
fk_bus_dma_tag_create(bus_dma_tag_t parent, bus_size_t alignment,
    bus_size_t boundary, bus_addr_t lowaddr, bus_addr_t highaddr,
    bus_size_t maxsize, int nsegments, bus_size_t maxsegsz, int flags,
    bus_dma_tag_t *dmat)
{
	int error = bus_dma_tag_create(parent, alignment, boundary, lowaddr,
	    highaddr, maxsize, nsegments, maxsegsz, flags, dmat);

	if (error == 0) {
		cnt_create++;
		if (t_parent == NULL)
			t_parent = *dmat;
		else if (t_overrun == NULL)
			t_overrun = *dmat;
		kprintf("FK: bus_dma_tag_create #%d -> tag %p\n", cnt_create,
		    *dmat);
	}
	return (error);
}

int
fk_bus_dma_tag_destroy(bus_dma_tag_t dmat)
{
	if (dmat != NULL && dmat == t_parent) {
		cnt_destroy_parent++;
		kprintf("FK: bus_dma_tag_destroy(PARENT dmat %p) #%d\n",
		    dmat, cnt_destroy_parent);
	} else if (dmat != NULL && dmat == t_overrun) {
		cnt_destroy_overrun++;
		kprintf("FK: bus_dma_tag_destroy(OVERRUN dmat %p) #%d\n",
		    dmat, cnt_destroy_overrun);
	}
	return (bus_dma_tag_destroy(dmat));	/* the real thing */
}


/* ---- the code under test ---------------------------------------------- */
/*
 * adv_pci.c is included VERBATIM.  The renames below (the equivalent of -D
 * command-line flags) sit AFTER our shim definitions above, so the shims
 * themselves call the real kernel functions while every call adv_pci.c
 * makes to these externals lands in the observation shims.  pcivar.h was
 * already included above, so its static __inline pci_* wrappers are not
 * re-parsed under the renames.  DRIVER_MODULE is neutered (the running
 * kernel already contains an identical pci/adv module from `device adv0`,
 * so re-registering would collide); the driver-table statics at the bottom
 * of adv_pci.c are kept referenced by the global pointers below.
 */
#define pci_read_config fk_pci_read_config
#define pci_write_config fk_pci_write_config
#define bus_alloc_resource fk_bus_alloc_resource
#define bus_release_resource fk_bus_release_resource
#define bus_setup_intr fk_bus_setup_intr
#define adv_alloc fk_adv_alloc
#define adv_init fk_adv_init
#define adv_find_signature fk_find_signature
#define bus_dma_tag_create fk_bus_dma_tag_create
#define bus_dma_tag_destroy fk_bus_dma_tag_destroy
#undef DRIVER_MODULE
#define DRIVER_MODULE(a, b, c, d, e, f)	/* neutered: kernel has pci/adv */

#include "adv_pci.c"		/* VERBATIM sys/dev/disk/advansys/adv_pci.c */

/* keep adv_pci.c's neutered statics referenced (no -Wunused-variable) */
driver_t *df_ref_driver = &adv_pci_driver;
devclass_t *df_ref_devclass = &adv_pci_devclass;

/* ------------------------------ driver --------------------------------- */

static int
df5042_modevent(module_t mod, int type, void *arg)
{
	device_t dev;
	int attach_ret;
	int leaked_parent, leaked_irq;

	switch (type) {
	case MOD_LOAD:
		mode_irqfail = kgetenv("df5042.irqfail") != NULL;

		fwin = (bus_space_handle_t)pmap_mapdev_uncacheable(
		    FAKE_BAR_PHYS, WIN_SIZE);
		ftag = (bus_space_tag_t)X86_64_BUS_SPACE_MEM;

		/*
		 * Plant the AdvanSys signature (adv_find_signature reads
		 * byte 0x01 == 0x25 and word 0x00 == 0x04C1) and leave
		 * CHIP_STATUS (0x0E) at 0 so the real adv_init() fails its
		 * halt check deterministically.
		 */
		((volatile u_int8_t *)fwin)[0] = 0xC1;
		((volatile u_int8_t *)fwin)[1] = 0x25;

		dev = devclass_get_device(devclass_find("pci"), 0);
		if (dev == NULL)
			dev = devclass_get_device(devclass_find("nexus"), 0);
		kprintf("DF5042: mode=%s, chip window=%p, dev=%p\n",
		    mode_irqfail ? "IRQFAIL" : "INITFAIL", (void *)fwin, dev);

		attach_ret = adv_pci_attach(dev);	/* code under test */

		kprintf("DF5042: adv_pci_attach returned %d\n", attach_ret);
		kprintf("DF5042: pci_write_config=%d dma tags created=%d "
		    "(parent=%p overrun=%p)\n", cnt_pci_write, cnt_create,
		    t_parent, t_overrun);
		kprintf("DF5042: bus_dma_tag_destroy: parent=%d overrun=%d\n",
		    cnt_destroy_parent, cnt_destroy_overrun);
		kprintf("DF5042: bus_release_resource: IOPORT=%d IRQ=%d; "
		    "bus_setup_intr calls=%d\n", cnt_rel_io, cnt_rel_irq,
		    cnt_setup_intr);

		leaked_parent = (t_parent != NULL && cnt_destroy_parent == 0);
		leaked_irq = (mode_irqfail && cnt_rel_irq == 0);
		if (leaked_parent)
			kprintf("DF5042: *** parent_dmat NEVER DESTROYED on "
			    "the failure unwind (adv_free case 1 tears down "
			    "from the ccb_infos tier) -- DF-5042 LEAK "
			    "PROVEN ***\n");
		if (leaked_irq)
			kprintf("DF5042: *** IRQ resource NEVER RELEASED on "
			    "the bus_setup_intr failure unwind -- DF-5043 "
			    "LEAK PROVEN ***\n");
		if (!leaked_parent && !leaked_irq)
			kprintf("DF5042: unwind clean (fix effective?)\n");

		/* keep the guest clean: manually undo everything leaked */
		if (leaked_parent)
			bus_dma_tag_destroy(t_parent);
		if (t_overrun != NULL && overrun_buf != NULL) {
			bus_dmamap_unload(t_overrun, overrun_dmamap);
			bus_dmamem_free(t_overrun, overrun_buf,
			    overrun_dmamap);
			bus_dma_tag_destroy(t_overrun);
			overrun_buf = NULL;
			overrun_dmat = NULL;
			overrun_physbase = 0;
		} else if (t_overrun != NULL && cnt_destroy_overrun == 0) {
			bus_dma_tag_destroy(t_overrun);
		}
		kprintf("DF5042: cleanup done, guest still healthy\n");
		return (0);
	case MOD_UNLOAD:
		kprintf("DF5042: unloaded\n");
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t df5042_mod = { "df5042", df5042_modevent, NULL };
DECLARE_MODULE(df5042, df5042_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
