/*
 * df5582_rig.c -- DF-5582 rig: bring the REAL smbacpi.ko GSBUS address
 * space handler (sys/bus/smbus/smbacpi/smbacpi.c) up on the QEMU guest
 * under a fake "ig4iic" parent, drive it through the real ACPICA field
 * machinery with a runtime-loaded SSDT, and demonstrate the
 * detach/no-quiesce use-after-free.
 *
 * Finding under test:
 *   smbus_acpi_detach() (smbacpi.c:268-276) only calls
 *   AcpiRemoveAddressSpaceHandler(); nothing waits for in-flight
 *   smbus_acpi_space_handler() dispatches.  ACPICA dispatches non-default
 *   address-space handlers with the interpreter EXITED
 *   (AcpiEvAddressSpaceDispatch, evregion.c:399-401), so a GSBUS field
 *   access can be (and on real hardware routinely is) sleeping inside
 *   the SMBus transfer when the ig4 stack is torn down:
 *
 *     - AcpiRemoveAddressSpaceHandler (evxfregn.c:290) takes only
 *       ACPI_MTX_NAMESPACE, unlinks the HandlerObj and drops its last
 *       reference -> the handler object AND its ContextMutex (which the
 *       in-flight dispatch holds across the handler call, evregion.c:429-
 *       447) are freed underneath the running handler;
 *     - the smbacpi softc (the handler's Context) and the parent ig4iic
 *       device (the handler's data->dev) are freed by newbus right after
 *       detach returns;
 *     - if smbacpi is a kld (it is: /boot/kernel/smbacpi.ko), the module
 *       text the in-flight handler will resume into is freed/unmapped by
 *       kldunload -> freed-text execution (the DF-5521/DF-5563/DF-5518
 *       mechanism).
 *
 * Rig structure:
 *
 *   nexus -> df5582rig0 (this module, bus_read_ivar answers
 *                        ACPI_IVAR_HANDLE with \_SB.DFS)
 *              -> ig4iic0 (this module: fake ig4iic bus device that
 *                          implements the smbus_if methods with a
 *                          controllable park + success -- what
 *                          ig4_acpi-attached ig4 hardware provides on
 *                          real systems)
 *                 -> smbacpi0 (REAL smbacpi.ko, code under test)
 *
 *   SSDT loaded at rig attach via AcpiLoadTable():
 *     \_SB.DFS: OpRegion(SMB0, GenericSerialBus) + FLD0 (AttribBytes 16)
 *     + FLD1 (AttribBlock) + TST0/TST1 methods.
 *
 * Test protocol (run.sh):
 *   control : hw.df5582.blocking=0, go=1  -> TST0 read runs the REAL
 *             handler end-to-end (count>0, AML sees 0xA0.. data).
 *   race    : blocking=1, go=1 (eval thread parks inside SMBUS_TRANS,
 *             in the real smbacpi handler, interpreter exited),
 *             detach=1 (smbacpi_detach runs underneath it),
 *             kldunload smbacpi (text freed), release=1
 *             -> handler resumes into freed module text -> fatal trap.
 *   bonus   : go=2 -> TST1 BLOCK-write with len byte 0xFF -> the KNOWN
 *             DF-1112/DF-2332 buf[32] stack overflow (confirmation of
 *             the pass-1 finding, not a new report).
 *
 * The fake ig4iic's park TIMES OUT (DF5582_PARK_MAXS) so the fixed
 * kernel's drain in smbus_acpi_detach can always make progress.
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/errno.h>
#include <sys/lock.h>
#include <sys/bus.h>
#include <sys/malloc.h>
#include <sys/types.h>
#include <sys/conf.h>
#include <sys/device.h>
#include <sys/ioccom.h>
#include <sys/sysctl.h>
#include <sys/thread.h>
#include <sys/thread2.h>
#include <sys/kthread.h>
#include <machine/atomic.h>

#include "opt_acpi.h"
#include "acpi.h"
#include <dev/acpica/acpivar.h>

#include "smbus_if.h"

#include "df5582_aml.h"		/* built from df5582.dsl by build.sh */

#define DF5582_PARK_MAXS	8	/* rig park (sec); fix drain cap is 30 s */

/* /dev/df5582 ioctls (the teardown MUST NOT be triggered from a sysctl
 * handler: DF's SYSCTL_XLOCK takes every per-CPU gd_sysctllock, and
 * device_delete_child -> device_sysctl_fini takes the same locks --
 * racing the sysctl writer wedges the whole sysctl tree). */
#define DF5582_IOC_DETACH	_IOW('d', 1, int)

static device_t	df5582_dev;		/* df5582rig0 on nexus */
static device_t	df5582_ig;		/* fake ig4iic0 */
static int	df5582_have_dfs;
static ACPI_HANDLE df5582_handle;	/* \_SB.DFS */

static int df5582_block;	/* park SMBUS_* calls */
static int df5582_parked;	/* a call is parked now */
static int df5582_rel;		/* release the parked call */
static int df5582_count;	/* SMBUS_* dispatches seen */
static int df5582_go;		/* 1=TST0 2=TST1 */
static int df5582_done;	/* eval finished */
static int df5582_status;	/* eval status */
static int df5582_detached;
static int df5582_parked_at_detach;	/* parked sampled at teardown end */
static int df5582_parked_at_start;	/* parked sampled at teardown start */
static struct thread *df5582_thr;
static struct thread *df5582_ctl_thr;
static volatile int df5582_shutdown;
static volatile int df5582_thr_gone;
static volatile int df5582_ctl_gone;
static cdev_t df5582_cdev;
static struct dev_ops df5582_ops;

SYSCTL_NODE(_hw, OID_AUTO, df5582, CTLFLAG_RW, 0, "DF-5582 rig");
SYSCTL_INT(_hw_df5582, OID_AUTO, blocking, CTLFLAG_RW, &df5582_block, 0,
    "park SMBUS_* calls inside the fake ig4iic");
SYSCTL_INT(_hw_df5582, OID_AUTO, parked, CTLFLAG_RD, &df5582_parked, 0,
    "a SMBUS_* call is parked (real handler in-flight)");
SYSCTL_INT(_hw_df5582, OID_AUTO, count, CTLFLAG_RD, &df5582_count, 0,
    "SMBUS_* dispatches seen by the fake ig4iic");
SYSCTL_INT(_hw_df5582, OID_AUTO, done, CTLFLAG_RD, &df5582_done, 0,
    "eval thread finished");
SYSCTL_INT(_hw_df5582, OID_AUTO, status, CTLFLAG_RD, &df5582_status, 0,
    "last AcpiEvaluateObject status (AE_OK == 0)");
SYSCTL_INT(_hw_df5582, OID_AUTO, detached, CTLFLAG_RD, &df5582_detached, 0,
    "fake ig4iic (+ smbacpi child) torn down");
SYSCTL_INT(_hw_df5582, OID_AUTO, parked_at_start, CTLFLAG_RD,
    &df5582_parked_at_start, 0, "parked sampled at teardown start");
SYSCTL_INT(_hw_df5582, OID_AUTO, parked_at_detach, CTLFLAG_RD,
    &df5582_parked_at_detach, 0,
    "parked sampled at teardown completion (1=still in-flight=stock, 0=drained=fixed)");

/*
 * Park helper: called from the fake ig4iic's smbus_if methods while the
 * REAL smbus_acpi_space_handler() is on the stack.
 */
static void
df5582_park(const char *what)
{

	df5582_parked = 1;
	wakeup(&df5582_parked);
	kprintf("[df5582] %s parked: smbacpi handler in-flight, "
	    "interpreter exited\n", what);

	while (!df5582_rel) {
		int t;

		for (t = 0; t < DF5582_PARK_MAXS && !df5582_rel; t++)
			tsleep(&df5582_rel, 0, "df55pk", hz);
		if (!df5582_rel) {
			kprintf("[df5582] %s park timeout, proceeding "
			    "(rig safety)\n", what);
			break;
		}
	}
	df5582_parked = 0;
	kprintf("[df5582] %s released\n", what);
}

/* ------------------------------------------------------------------ */
/* smbus_if methods (the fake ig4iic "hardware")                       */
/* ------------------------------------------------------------------ */

static int
df5582_smb_quick(device_t dev, u_char slave, int how)
{
	df5582_count++;
	if (df5582_block)
		df5582_park("quick");
	return (0);
}

static int
df5582_smb_sendb(device_t dev, u_char slave, char byte)
{
	df5582_count++;
	if (df5582_block)
		df5582_park("sendb");
	return (0);
}

static int
df5582_smb_recvb(device_t dev, u_char slave, char *byte)
{
	df5582_count++;
	if (df5582_block)
		df5582_park("recvb");
	if (byte)
		*byte = (char)0xA5;
	return (0);
}

static int
df5582_smb_writeb(device_t dev, u_char slave, char cmd, char byte)
{
	df5582_count++;
	if (df5582_block)
		df5582_park("writeb");
	return (0);
}

static int
df5582_smb_readb(device_t dev, u_char slave, char cmd, char *byte)
{
	df5582_count++;
	if (df5582_block)
		df5582_park("readb");
	if (byte)
		*byte = (char)0x5A;
	return (0);
}

static int
df5582_smb_writew(device_t dev, u_char slave, char cmd, short word)
{
	df5582_count++;
	if (df5582_block)
		df5582_park("writew");
	return (0);
}

static int
df5582_smb_readw(device_t dev, u_char slave, char cmd, short *word)
{
	df5582_count++;
	if (df5582_block)
		df5582_park("readw");
	if (word)
		*word = 0x1234;
	return (0);
}

static int
df5582_smb_pcall(device_t dev, u_char slave, char cmd, short sdata,
    short *rdata)
{
	df5582_count++;
	if (df5582_block)
		df5582_park("pcall");
	if (rdata)
		*rdata = 0x5678;
	return (0);
}

static int
df5582_smb_bwrite(device_t dev, u_char slave, char cmd, u_char count,
    char *buf)
{
	df5582_count++;
	kprintf("[df5582] BWRITE slave=%#x cmd=%#x count=%u\n",
	    slave, (u_char)cmd, count);
	if (df5582_block)
		df5582_park("bwrite");
	return (0);
}

static int
df5582_smb_bread(device_t dev, u_char slave, char cmd, u_char *count,
    char *buf)
{
	u_char i, n;

	df5582_count++;
	if (df5582_block)
		df5582_park("bread");
	n = *count;
	for (i = 0; i < n; i++)
		buf[i] = (char)(0xB0 + i);
	*count = n;
	return (0);
}

static int
df5582_smb_trans(device_t dev, int slave, char cmd, int op, char *wbuf,
    int wcount, char *rbuf, int rcount, int *actualp)
{
	int i;

	df5582_count++;
	kprintf("[df5582] TRANS slave=%#x cmd=%#x op=%#x wcount=%d "
	    "rcount=%d (#%d)\n", slave, (u_char)cmd, op, wcount, rcount,
	    df5582_count);
	if (df5582_block)
		df5582_park("trans");
	if (actualp)
		*actualp = 0;
	if (rcount > 0 && rbuf != NULL) {
		for (i = 0; i < rcount; i++)
			rbuf[i] = (char)(0xA0 + (i & 0xF));
		if (actualp)
			*actualp = rcount;
	}
	return (0);
}

static int
df5582_smb_callback(device_t dev, int index, void *data)
{

	return (0);
}

static void
df5582_smb_intr(device_t dev, u_char devaddr, char low, char high, int error)
{
}

/* ------------------------------------------------------------------ */
/* fake "ig4iic" bus device (children devclass "ig4iic")               */
/* ------------------------------------------------------------------ */

static int
ig4iic_fake_probe(device_t dev)
{

	device_set_desc(dev, "DF5582 fake ig4iic (ACPI I2cSerialBus ctrl)");
	return (0);
}

static int
ig4iic_fake_attach(device_t dev)
{
	device_t child;

	df5582_ig = dev;
	child = device_add_child(dev, "smbacpi", -1);
	if (child == NULL) {
		kprintf("[df5582] smbacpi child add failed "
		    "(is smbacpi.ko loaded?)\n");
		return (ENXIO);
	}
	bus_generic_attach(dev);
	kprintf("[df5582] fake ig4iic up, smbacpi child=%p (%s)\n", child,
	    device_is_attached(child) ? "ATTACHED" : "not-attached");
	if (!device_is_attached(child))
		return (ENXIO);
	return (0);
}

static int
ig4iic_fake_detach(device_t dev)
{

	kprintf("[df5582] fake ig4iic detach (smbacpi child detaches "
	    "first: smbus_acpi_detach under test)\n");
	bus_generic_detach(dev);
	return (0);
}

static device_method_t ig4iic_fake_methods[] = {
	/* Device interface */
	DEVMETHOD(device_probe,		ig4iic_fake_probe),
	DEVMETHOD(device_attach,	ig4iic_fake_attach),
	DEVMETHOD(device_detach,	ig4iic_fake_detach),

	/* SMBUS interface */
	DEVMETHOD(smbus_quick,		df5582_smb_quick),
	DEVMETHOD(smbus_sendb,		df5582_smb_sendb),
	DEVMETHOD(smbus_recvb,		df5582_smb_recvb),
	DEVMETHOD(smbus_writeb,		df5582_smb_writeb),
	DEVMETHOD(smbus_readb,		df5582_smb_readb),
	DEVMETHOD(smbus_writew,		df5582_smb_writew),
	DEVMETHOD(smbus_readw,		df5582_smb_readw),
	DEVMETHOD(smbus_pcall,		df5582_smb_pcall),
	DEVMETHOD(smbus_bwrite,		df5582_smb_bwrite),
	DEVMETHOD(smbus_bread,		df5582_smb_bread),
	DEVMETHOD(smbus_trans,		df5582_smb_trans),
	DEVMETHOD(smbus_callback,	df5582_smb_callback),
	DEVMETHOD(smbus_intr,		df5582_smb_intr),

	DEVMETHOD_END
};

static driver_t ig4iic_fake_driver = {
	"ig4iic",
	ig4iic_fake_methods,
	1,			/* no softc needed */
};

static devclass_t ig4iic_fake_devclass;

/* ------------------------------------------------------------------ */
/* df5582 bus device on nexus (answers ACPI_IVAR_HANDLE)               */
/* ------------------------------------------------------------------ */

static void
df5582_identify(driver_t *driver, device_t parent)
{
	device_t child;

	child = device_find_child(parent, "df5582", 0);
	if (child == NULL)
		BUS_ADD_CHILD(parent, parent, 0, "df5582", 0);
}

static int
df5582_probe(device_t dev)
{

	/*
	 * Only useful once ACPI is alive (we need a namespace node for the
	 * GSBUS handler).  acpi0 probing precedes late nexus identifies.
	 */
	if (devclass_find("acpi") == NULL)
		return (ENXIO);
	device_set_desc(dev, "DF5582 smbacpi rig bus");
	return (0);
}

static int
df5582_read_ivar(device_t dev, device_t child, int index, uintptr_t *result)
{

	switch (index) {
	case ACPI_IVAR_HANDLE:
		/*
		 * Exactly what an ig4_acpi-attached ig4iic device provides
		 * on real hardware (acpivar.h __ACPI_BUS_ACCESSOR(acpi,
		 * handle,...) -> BUS_READ_IVAR(parent, ...)): the ACPI node
		 * of the controller, so smbus_acpi_probe() passes and
		 * smbus_acpi_install_address_space_handler() installs the
		 * production GSBUS handler on that node.
		 */
		*result = (uintptr_t)df5582_handle;
		return (0);
	}
	return (ENOENT);
}

static void
df5582_ctl_thread(void *arg)
{

	for (;;) {
		tsleep(&df5582_detached, 0, "df5582c", 0);
		if (df5582_shutdown)
			break;
		if (df5582_detached == 1 && df5582_ig != NULL) {
			kprintf("[df5582] teardown: device_delete_child("
			    "df5582rig0, ig4iic0) -> smbus_acpi_detach() "
			    "runs now\n");
			device_delete_child(df5582_dev, df5582_ig);
			df5582_ig = NULL;
			df5582_detached = 2;
			/*
			 * Ordering witness (console prints reorder across
			 * CPUs on DF): 1 = the in-flight handler had
			 * ALREADY completed when the teardown returned
			 * (fixed module: detach drained it); 0 = teardown
			 * finished while the handler was still parked
			 * (stock module: no drain).
			 */
			df5582_parked_at_detach = df5582_parked;
			kprintf("[df5582] teardown complete: smbacpi softc "
			    "freed, GSBUS handler object freed "
			    "(parked=%d)\n", df5582_parked_at_detach);
		}
	}
	df5582_ctl_gone = 1;
	wakeup(&df5582_shutdown);
	kthread_exit();
}

static void
df5582_eval_thread(void *arg)
{
	ACPI_STATUS s;

	for (;;) {
		tsleep(&df5582_go, 0, "df5582w", 0);
		if (df5582_shutdown)
			break;
		switch (df5582_go) {
		case 2:
			kprintf("[df5582] eval TST1 (BLOCK write, known "
			    "buf[32] class)\n");
			s = AcpiEvaluateObject(df5582_handle, "TST1",
			    NULL, NULL);
			break;
		default:
			kprintf("[df5582] eval TST0 (BYTES read)\n");
			s = AcpiEvaluateObject(df5582_handle, "TST0",
			    NULL, NULL);
			break;
		}
		df5582_status = (int)s;
		df5582_done = 1;
		wakeup(&df5582_done);
		kprintf("[df5582] eval done: %s\n", AcpiFormatException(s));
		df5582_go = 0;
	}
	df5582_thr_gone = 1;
	wakeup(&df5582_shutdown);
	kthread_exit();
}

static int
df5582_attach(device_t dev)
{
	ACPI_STATUS s;
	UINT32 tidx = 0;
	device_t child;

	df5582_dev = dev;

	/*
	 * 1+2. Load the rig SSDT (creates \_SB.DFS with the GSBUS opregion
	 * and fields), or reuse it if the rig was re-loaded this boot.
	 * Resolving \_SB.DFS is what ig4_acpi supplies on real hardware.
	 */
	s = AcpiGetHandle(NULL, "\\_SB.DFS", &df5582_handle);
	if (ACPI_FAILURE(s)) {
		s = AcpiLoadTable((ACPI_TABLE_HEADER *)df5582_aml, &tidx);
		if (ACPI_FAILURE(s)) {
			kprintf("[df5582] AcpiLoadTable failed: %s\n",
			    AcpiFormatException(s));
			return (ENXIO);
		}
		kprintf("[df5582] SSDT loaded (table index %u)\n", tidx);
		s = AcpiGetHandle(NULL, "\\_SB.DFS", &df5582_handle);
		if (ACPI_FAILURE(s)) {
			kprintf("[df5582] AcpiGetHandle(\\_SB.DFS) failed: %s\n",
			    AcpiFormatException(s));
			return (ENXIO);
		}
	} else {
		kprintf("[df5582] \\_SB.DFS already present (rig reload)\n");
	}
	df5582_have_dfs = 1;

	/* 3. Fake ig4iic; its smbacpi child is the code under test. */
	child = device_add_child(dev, "ig4iic", -1);
	if (child == NULL)
		return (ENXIO);
	bus_generic_attach(dev);

	/* 4. Eval + control threads. */
	kthread_create(df5582_eval_thread, NULL, &df5582_thr,
	    "df5582ev");
	kthread_create(df5582_ctl_thread, NULL, &df5582_ctl_thr,
	    "df5582ctl");

	df5582_cdev = make_dev(&df5582_ops, 0, UID_ROOT, GID_WHEEL, 0600,
	    "df5582");
	if (df5582_cdev == NULL)
		kprintf("[df5582] make_dev(df5582) failed\n");

	kprintf("[df5582] rig up: DFS handle=%p ig4iic=%p\n",
	    df5582_handle, child);
	return (0);
}

static int
df5582_detach(device_t dev)
{

	/*
	 * Rig teardown when NOT racing (kldunload of the rig itself, or a
	 * fresh-rig reload between runs): stop the kthreads first so the
	 * module text is never freed under them, then tear the fake
	 * ig4iic down (smbacpi child detaches first).
	 */
	df5582_shutdown = 1;
	wakeup(&df5582_go);
	wakeup(&df5582_detached);
	while (!df5582_thr_gone || !df5582_ctl_gone)
		tsleep(&df5582_shutdown, 0, "df5582x", 1);
	if (df5582_ig != NULL) {
		device_delete_child(df5582_dev, df5582_ig);
		df5582_ig = NULL;
	}
	if (df5582_cdev != NULL) {
		dev_ops_remove_minor(&df5582_ops, 0);
		df5582_cdev = NULL;
	}
	return (0);
}

static device_method_t df5582_methods[] = {
	/* Device interface */
	DEVMETHOD(device_identify,	df5582_identify),
	DEVMETHOD(device_probe,		df5582_probe),
	DEVMETHOD(device_attach,	df5582_attach),
	DEVMETHOD(device_detach,	df5582_detach),

	/* Bus interface: supply the ACPI handle of "our controller" */
	DEVMETHOD(bus_read_ivar,	df5582_read_ivar),

	DEVMETHOD_END
};

static driver_t df5582_driver = {
	"df5582",
	df5582_methods,
	1,
};

static devclass_t df5582_devclass;

/* ------------------------------------------------------------------ */
/* action sysctls                                                      */
/* ------------------------------------------------------------------ */

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

	t = 0;
	error = sysctl_handle_int(oidp, &t, 0, req);
	if (error != 0 || req->newptr == NULL)
		return (error);
	if (t == 1 || t == 2) {
		df5582_done = 0;
		df5582_go = t;
		wakeup(&df5582_go);
	}
	return (0);
}

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

	t = 0;
	error = sysctl_handle_int(oidp, &t, 0, req);
	if (error != 0 || req->newptr == NULL)
		return (error);
	if (t == 1 && !df5582_detached && df5582_ig != NULL) {
		kprintf("[df5582] teardown: device_delete_child(df5582rig0,"
		    " ig4iic0) -> smbacpi_detach() runs now\n");
		device_delete_child(df5582_dev, df5582_ig);
		df5582_ig = NULL;
		df5582_detached = 1;
		kprintf("[df5582] teardown complete: smbacpi softc freed, "
		    "GSBUS handler object freed\n");
	}
	return (0);
}

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

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

SYSCTL_PROC(_hw_df5582, OID_AUTO, go, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, df5582_sysctl_go, "I", "1=TST0 (BYTES read) 2=TST1 (BLOCK write)");
SYSCTL_PROC(_hw_df5582, OID_AUTO, detach, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, df5582_sysctl_detach, "I",
    "tear down the fake ig4iic (runs smbus_acpi_detach)");
SYSCTL_PROC(_hw_df5582, OID_AUTO, release, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, df5582_sysctl_release, "I", "release a parked SMBUS_* call");

/*
 * /dev/df5582: DETACH ioctl signals the ctl thread.  (GO and RELEASE
 * stay as trivial sysctl writes -- their handlers take no locks.)
 */
static int
df5582_open(struct dev_open_args *ap)
{
	return (0);
}

static int
df5582_close(struct dev_close_args *ap)
{
	return (0);
}

static int
df5582_ioctl(struct dev_ioctl_args *ap)
{

	switch (ap->a_cmd) {
	case DF5582_IOC_DETACH:
		if (df5582_detached == 0 && df5582_ig != NULL) {
			df5582_detached = 1;
			wakeup(&df5582_detached);
		}
		return (0);
	}
	return (ENOIOCTL);
}

static struct dev_ops df5582_ops = {
	{ "df5582", 0, 0 },
	.d_open =	df5582_open,
	.d_close =	df5582_close,
	.d_ioctl =	df5582_ioctl,
};

DRIVER_MODULE(df5582, nexus, df5582_driver, df5582_devclass, NULL, NULL);
DRIVER_MODULE(ig4iic, df5582, ig4iic_fake_driver, ig4iic_fake_devclass,
    NULL, NULL);
MODULE_DEPEND(df5582, smbacpi, 1, 1, 1);
MODULE_VERSION(df5582, 1);
