/*
 * DF-5044 / DF-5045 PoC trigger — drive the stock kernel's own sili OS glue
 * (sys/dev/disk/sili/sili_dragonfly.c, compiled into X86_64_GENERIC) to
 * demonstrate:
 *
 *  DF-5044: sili_os_stop_port() waits for the per-port helper thread with NO
 *           bound.  We park a REAL helper thread (started by the REAL
 *           sili_os_start_port via the REAL sili_port_alloc) with the port
 *           lock held by us, so the thread blocks inside
 *           sili_port_thread_core()->sili_os_lock_port().  A stopper thread
 *           then calls the REAL sili_os_stop_port(), which signals STOP and
 *           spins in "while (ap->ap_thread) sili_os_sleep(100)" FOREVER —
 *           proven by a watchdog that observes >=3 x 5s of continuous
 *           spinning.  After the proof we release the wedge so the run
 *           unwinds cleanly (the guest is left healthy; on real hardware /
 *           real kldunload there is nobody to release the wedge, which is
 *           the finding: unload/shutdown hangs permanently).
 *
 *  DF-5045: sili_os_stop_port() unconditionally lockuninit()s ap->ap_lock
 *           even when sili_os_start_port() never ran (the sili_port_alloc
 *           error unwinds at sili.c:186/221 goto sili_port_free ->
 *           sili_os_stop_port with ap_thread == NULL).  lockuninit() on the
 *           never-lockinit'd, M_ZERO-zeroed lock executes here (benign only
 *           because lk_count == 0 satisfies the KKASSERT); also note
 *           ap_sim_lock / ap_sig_lock / ap_ccb_lock are never uninited at
 *           all on ANY path.
 *
 * Everything the helper thread does runs against a pmap_mapdev'd fake
 * SiI port-register window (same technique as the DF-4964 pack).
 */
#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/kthread.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 "sili.h"

#define	FAKE_BAR_PHYS	0x60000000UL	/* real RAM on this guest (4GB) */
#define	MAP_SIZE	0x10000		/* 64KB real mapping (like DF-4964) */

static bus_space_handle_t mapbase;
static struct sili_softc *sc;
static int norelease;

static struct sili_port	bare_port;	/* DF-5045: zeroed, never started */
static struct sili_port *ap_wedge;	/* DF-5044: port 1 */
static volatile int stopper_done;
static volatile int hold_forever;

/*
 * norelease mode: a sacrificial kthread holds ap_lock forever (emulating a
 * port helper thread wedged behind a lock held by stuck code), so the
 * stopper's sili_os_stop_port() can never see ap_thread == NULL.  On the
 * baseline kernel the stopper spins forever; with fix.diff (bounded wait)
 * it returns after the bound.
 */
static void
wedge_holder_thread(void *arg)
{
	struct sili_port *ap = arg;

	sili_os_lock_port(ap);
	kprintf("DF5044: WEDGE: ap_lock held forever (norelease mode)\n");
	while (hold_forever)
		sili_os_sleep(1000);
	sili_os_unlock_port(ap);
	kthread_exit();
}


static void
stopper_thread(void *arg)
{
	struct sili_port *ap = arg;

	kprintf("DF5044: STOPPER: calling real sili_os_stop_port(%p) with the "
	    "helper thread wedged on ap_lock\n", ap);
	sili_os_stop_port(ap);			/* code under test */
	stopper_done = 1;
	kprintf("DF5044: STOPPER: sili_os_stop_port RETURNED\n");
	kthread_exit();
}

static void
watchdog_thread(void *arg)
{
	int i;

	for (i = 1; i <= 3; i++) {
		sili_os_sleep(5000);
		if (stopper_done)
			break;
		kprintf("DF5044: WATCHDOG %d/3: stopper STILL spinning in "
		    "sili_os_stop_port wait loop after %d seconds "
		    "(ap_thread=%p != NULL, no bound, no force-kill)\n",
		    i, i * 5, ap_wedge->ap_thread);
	}
	kthread_exit();
}

static void
wait_init_clear(struct sili_port *ap)
{
	int n = 0;

	while ((ap->ap_signal & AP_SIGF_INIT) && n++ < 600)
		sili_os_sleep(100);
	if (ap->ap_signal & AP_SIGF_INIT)
		kprintf("DF5044: WARN: port %d INIT never cleared\n",
		    ap->ap_num);
	else
		kprintf("DF5044: port %d helper thread up and parked "
		    "(thread=%p)\n", ap->ap_num, ap->ap_thread);
}

static int
df5044_modevent(module_t mod, int type, void *arg)
{
	device_t dev;
	int error;
	int n;

	switch (type) {
	case MOD_LOAD:
		hold_forever = 1;
		norelease = kgetenv("df5044.norelease") != NULL;
		if (norelease) {
			/*
			 * Wedge-forever mode: build ONE port, park its helper
			 * thread behind a permanently-held ap_lock, start the
			 * stopper, and return from MOD_LOAD after proving the
			 * spin (the stopper + wedge threads are intentionally
			 * left running until reboot — that is the finding).
			 */
			int spin;

			mapbase = (bus_space_handle_t)pmap_mapdev_uncacheable(
			    FAKE_BAR_PHYS, MAP_SIZE);
			dev = devclass_get_device(devclass_find("pci"), 0);
			sc = kmalloc(sizeof(*sc), M_DEVBUF, M_INTWAIT | M_ZERO);
			sc->sc_dev = dev;
			sc->sc_piot = (bus_space_tag_t)X86_64_BUS_SPACE_MEM;
			sc->sc_pioh = mapbase;
			sc->sc_iot = (bus_space_tag_t)X86_64_BUS_SPACE_MEM;
			sc->sc_ioh = (bus_space_handle_t)(mapbase + 0x8000);
			sc->sc_ncmds = SILI_MAX_CMDS;
			error = bus_dma_tag_create(NULL, 256, 65536,
			    BUS_SPACE_MAXADDR, BUS_SPACE_MAXADDR,
			    sizeof(struct sili_prb) * SILI_MAX_CMDS, 1,
			    sizeof(struct sili_prb) * SILI_MAX_CMDS, 0,
			    &sc->sc_tag_prbs);
			error += bus_dma_tag_create(NULL, 4, 0,
			    BUS_SPACE_MAXADDR, BUS_SPACE_MAXADDR,
			    4096 * 1024, SILI_MAX_SGET, 65536, 0,
			    &sc->sc_tag_data);
			if (error)
				return (ENOMEM);
			if (sili_port_alloc(sc, 1) != 0)
				return (EIO);
			wait_init_clear(sc->sc_ports[1]);

			ap_wedge = sc->sc_ports[1];
			kthread_create(wedge_holder_thread, ap_wedge, NULL,
			    "df5044wedge");
			sili_os_sleep(200);	/* let it take the lock */
			sili_os_signal_port_thread(ap_wedge, AP_SIGF_TIMEOUT);
			sili_os_sleep(200);	/* helper blocks on ap_lock */

			kprintf("DF5044: NORELEASE: starting stopper against a "
			    "permanently wedged helper thread (baseline kernel: "
			    "expect spin-forever; fixed kernel: expect return "
			    "after the bound)\n");
			kthread_create(stopper_thread, ap_wedge, NULL,
			    "df5044stop");
			for (spin = 1; spin <= 18; spin++) {
				sili_os_sleep(5000);
				kprintf("DF5044: NORELEASE: %2d x 5s, stopper "
				    "%s (ap_thread=%p)\n", spin,
				    stopper_done ? "RETURNED" : "STILL "
				    "SPINNING", ap_wedge->ap_thread);
			}
			kprintf("DF5044: NORELEASE: observation window closed "
			    "after 90s; module unloads, wedge/stopper threads "
			    "stay (leaked by design until reboot)\n");
			return (0);
		}
		mapbase = (bus_space_handle_t)pmap_mapdev_uncacheable(
		    FAKE_BAR_PHYS, MAP_SIZE);
		dev = devclass_get_device(devclass_find("pci"), 0);
		if (dev == NULL)
			dev = devclass_get_device(devclass_find("nexus"), 0);

		sc = kmalloc(sizeof(*sc), M_DEVBUF, M_INTWAIT | M_ZERO);
		sc->sc_dev = dev;
		sc->sc_piot = (bus_space_tag_t)X86_64_BUS_SPACE_MEM;
		sc->sc_pioh = mapbase;			/* BAR(2) fake window */
		sc->sc_iot = (bus_space_tag_t)X86_64_BUS_SPACE_MEM;
		sc->sc_ioh = (bus_space_handle_t)(mapbase + 0x8000);
		sc->sc_ncmds = SILI_MAX_CMDS;
		/*
		 * Real-driver tag constraints (sili_attach.c:210-238): the
		 * 4th arg is lowaddr and doubles as the contigmalloc upper
		 * bound / kmalloc-eligibility check (must be >= ptoa(Maxmem)),
		 * so use BUS_SPACE_MAXADDR like the driver, not 0.
		 */
		error = bus_dma_tag_create(NULL, 256, 65536,
		    BUS_SPACE_MAXADDR, BUS_SPACE_MAXADDR,
		    sizeof(struct sili_prb) * SILI_MAX_CMDS, 1,
		    sizeof(struct sili_prb) * SILI_MAX_CMDS, 0,
		    &sc->sc_tag_prbs);
		error += bus_dma_tag_create(NULL, 4, 0,
		    BUS_SPACE_MAXADDR, BUS_SPACE_MAXADDR,
		    4096 * 1024, SILI_MAX_SGET, 65536, 0, &sc->sc_tag_data);
		if (error)
			return (ENOMEM);

		/*
		 * Build two REAL ports (the same construction DF-4964 used;
		 * ports 0/1 sit at offsets 0x0000/0x2000 of the window, so
		 * nothing here is out of bounds).
		 */
		error = sili_port_alloc(sc, 0);
		error += sili_port_alloc(sc, 1);
		if (error) {
			kprintf("DF5044: sili_port_alloc error %d\n", error);
			return (EIO);
		}
		wait_init_clear(sc->sc_ports[0]);
		wait_init_clear(sc->sc_ports[1]);

		/* ---- E1: normal stop returns promptly (control) --------- */
		kprintf("DF5044: E1: normal sili_os_stop_port(port0)...\n");
		sili_os_stop_port(sc->sc_ports[0]);
		kprintf("DF5044: E1: returned (thread=%p)\n",
		    sc->sc_ports[0]->ap_thread);

		/* ---- E2: wedged helper thread => unbounded stop wait ----- */
		ap_wedge = sc->sc_ports[1];
		kprintf("DF5044: E2: wedging helper thread: holding ap_lock, "
		    "then signalling AP_SIGF_TIMEOUT so the thread blocks in "
		    "sili_port_thread_core()->sili_os_lock_port()\n");
		sili_os_lock_port(ap_wedge);
		sili_os_signal_port_thread(ap_wedge, AP_SIGF_TIMEOUT);
		sili_os_sleep(200);		/* let the thread block */

		stopper_done = 0;
		kthread_create(stopper_thread, ap_wedge, NULL,
		    "df5044stop");
		kthread_create(watchdog_thread, NULL, NULL,
		    "df5044wd");
		/* >=15s of watchdog proof while the wedge is held */
		for (n = 0; n < 160 && !stopper_done; n++)
			sili_os_sleep(100);
		/*
		 * Release the wedge: on a real kldunload/shutdown there is
		 * nobody to do this — that is the finding (permanent hang).
		 * Here we release it only so the run unwinds cleanly.
		 */
		kprintf("DF5044: E2: releasing the wedge after >= 15s of "
		    "proven unbounded spinning\n");
		sili_os_unlock_port(ap_wedge);
		for (n = 0; n < 600 && !stopper_done; n++)
			sili_os_sleep(100);
		if (!stopper_done) {
			kprintf("DF5044: ERROR: stopper never returned even "
			    "after wedge release\n");
			return (EIO);
		}
		kprintf("DF5044: E2: PROVEN: sili_os_stop_port spun >= 15s "
		    "purely because the helper thread was blocked; on a real "
		    "unload/shutdown nobody releases the wedge => permanent "
		    "hang (DF-5044)\n");

		/* ---- E3: lockuninit on a NEVER-lockinit'd lock ----------- */
		bzero(&bare_port, sizeof(bare_port));
		bare_port.ap_sc = sc;		/* sili.c:165 sets this before */
						/* any goto freeport path */
		bare_port.ap_thread = NULL;	/* thread never started */
		ksnprintf(bare_port.ap_name, sizeof(bare_port.ap_name), "bare");
		kprintf("DF5045: E3: calling sili_os_stop_port() on a port "
		    "whose ap_lock was NEVER lockinit'ed (the sili_port_alloc "
		    "error-unwind shape)\n");
		sili_os_stop_port(&bare_port);
		kprintf("DF5045: E3: returned — lockuninit(&ap_lock) executed "
		    "on the uninitialized zeroed lock (benign only because "
		    "M_ZERO makes lk_count==0); ap_sim_lock/ap_sig_lock/"
		    "ap_ccb_lock are never uninited on any path (DF-5045)\n");

		/* ---- cleanup: leave the guest clean --------------------- */
		sili_port_free(sc, 0);
		sili_port_free(sc, 1);
		bus_dma_tag_destroy(sc->sc_tag_prbs);
		bus_dma_tag_destroy(sc->sc_tag_data);
		sc->sc_tag_prbs = NULL;
		sc->sc_tag_data = NULL;
		kfree(sc, M_DEVBUF);
		sc = NULL;
		kprintf("DF5044: cleanup done, guest still healthy\n");
		return (0);
	case MOD_UNLOAD:
		kprintf("DF5044: unloaded\n");
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t df5044_mod = { "df5044", df5044_modevent, NULL };
DECLARE_MODULE(df5044, df5044_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
/* no MODULE_DEPEND: stock kernel has sili compiled into X86_64_GENERIC */
