/*
 * hp_sim.c -- verbatim-interposition harness for
 *             sys/dev/acpica/acpi_hp/acpi_hp.c  (DF-2383 / DF-2384)
 *
 * Strategy:
 *   - The REAL acpi_hp.c is compiled VERBATIM into the kernel
 *     (sys/conf/files: optional acpi_hp acpi; sha256-gated by build.sh).
 *   - This file provides the surrounding ACPI/WMI world as a FAKE newbus
 *     bus driver literally named "acpi_wmi" attached to the real acpi0.
 *     added to that device are probed against the devclass "acpi_wmi",
 *     which is where the verbatim DRIVER_MODULE(acpi_hp, acpi_wmi, ...)
 *     registered the real acpi_hp driver at boot -> the REAL
 *     acpi_hp_probe/attach/detach/notify/sysctl code runs unmodified.
 *   - The 7 acpi_wmi_if KBI methods below interpose every WMI external
 *     the verbatim TU calls, mirroring the lock protocol of the REAL
 *     sys/dev/acpica/acpi_wmi/acpi_wmi.c:
 *       install_event_handler  (acpi_wmi.c:408-431)
 *       remove_event_handler   (acpi_wmi.c:441-460)  [no drain -- the bug]
 *       get_event_data         (acpi_wmi.c:466-495)  [+ DF-2383 stall hook]
 *       evaluate_call          -> controllable replies [DF-2384]
 *   - Event injection mirrors acpi_wmi_notify_handler (acpi_wmi.c:627-653):
 *     handler+ctx copied under the WMI lock, lock RELEASED, then handler
 *     invoked -- exactly the dispatch-after-unlock of the real driver.
 *   - HP_SIM_FIX_DRAIN (0 baseline / 1 fixed build) mirrors the fix.diff
 *     protocol for acpi_wmi.c: in-flight dispatch refcount + remove waits.
 */

#include "opt_acpi.h"
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/conf.h>
#include <sys/sbuf.h>
#include <sys/bus.h>
#include <sys/module.h>
#include <sys/sysctl.h>
#include <sys/thread.h>
#include <sys/thread2.h>
#include <sys/lock.h>
#include <sys/types.h>
#include <sys/kthread.h>

#include "acpi.h"
#include "accommon.h"
#include <dev/acpica/acpivar.h>
#include "bus_if.h"
#include "acpi_wmi_if.h"

MALLOC_DECLARE(M_BUS);

/* Mirrors fix.diff (drain protocol in acpi_wmi.c); toggled to 1 by the
 * fixed build. */
#ifndef HP_SIM_FIX_DRAIN
#define HP_SIM_FIX_DRAIN	0
#endif

#define HP_SIM_WMI_EVENT_GUID	"95F24279-4D7B-4334-9387-ACCDC67EF61C"
#define HP_SIM_WMI_BIOS_GUID	"5FB7F034-2C63-45E9-BE91-3D44E2C707E4"

#define HPSIM "HPSIM: "

/* WMI command ids used by acpi_hp_exec_wmi_command callers */
#define HP_CMD_DISPLAY	1
#define HP_CMD_HDDTEMP	2
#define HP_CMD_ALS	3
#define HP_CMD_DOCK	4
#define HP_CMD_WIRELESS	5

/*
 * Mirror of struct acpi_hp_softc (sys/dev/acpica/acpi_hp/acpi_hp.c:117-149).
 * Used ONLY to compute field offsets for the victim-poison/diff reporting;
 * never used to drive the driver under test.
 */
struct hp_mirror {
	device_t	dev;			/*   0 */
	device_t	wmi_dev;		/*   8 */
	int		has_notify;		/*  16 */
	int		has_cmi;		/*  20 */
	int		cmi_detail;		/*  24 */
	int		verbose;		/*  28 */
	int		wlan_enable_if_radio_on;	/*  32 */
	int		wlan_disable_if_radio_off;	/*  36 */
	int		bluetooth_enable_if_radio_on;	/*  40 */
	int		bluetooth_disable_if_radio_off;	/*  44 */
	int		wwan_enable_if_radio_on;	/*  48 */
	int		wwan_disable_if_radio_off;	/*  52 */
	int		was_wlan_on_air;		/*  56 */
	int		was_bluetooth_on_air;		/*  60 */
	int		was_wwan_on_air;		/*  64 */
	struct sysctl_ctx_list sysctl_ctx;	/*  68 */
	struct sysctl_oid *sysctl_tree;
	struct cdev *hpcmi_dev_t;
	struct sbuf hpcmi_sbuf;
	pid_t		hpcmi_open_pid;
	int		hpcmi_bufptr;
	int		cmi_order_size;
	struct { UINT32 sequence; UINT8 instance; } cmi_order[128];
};

static const struct {
	int off;
	const char *name;
} hp_mirror_fields[] = {
	{ offsetof(struct hp_mirror, dev), "dev" },
	{ offsetof(struct hp_mirror, wmi_dev), "wmi_dev" },
	{ offsetof(struct hp_mirror, has_notify), "has_notify" },
	{ offsetof(struct hp_mirror, verbose), "verbose" },
	{ offsetof(struct hp_mirror, wlan_enable_if_radio_on), "wlan_en_if_on" },
	{ offsetof(struct hp_mirror, wlan_disable_if_radio_off), "wlan_dis_if_off" },
	{ offsetof(struct hp_mirror, bluetooth_enable_if_radio_on), "bt_en_if_on" },
	{ offsetof(struct hp_mirror, bluetooth_disable_if_radio_off), "bt_dis_if_off" },
	{ offsetof(struct hp_mirror, wwan_enable_if_radio_on), "wwan_en_if_on" },
	{ offsetof(struct hp_mirror, wwan_disable_if_radio_off), "wwan_dis_if_off" },
	{ offsetof(struct hp_mirror, was_wlan_on_air), "was_wlan_on_air" },
	{ offsetof(struct hp_mirror, was_bluetooth_on_air), "was_bt_on_air" },
	{ offsetof(struct hp_mirror, was_wwan_on_air), "was_wwan_on_air" },
	{ -1, NULL },
};

/*
 * Softc of the fake acpi_wmi bus device.  MUST start with an embedded
 * struct acpi_softc: verbatim acpi_hp_attach():495 calls
 * acpi_device_get_parent_softc(sc->wmi_dev) which returns
 * device_get_softc(parent) cast to struct acpi_softc *; it only uses
 * acpi_sc->acpi_sysctl_tree (acpi_hp.c:533-534).
 */
struct hp_sim_softc {
	struct acpi_softc	acpi;		/* fake acpi_softc */
	struct lock		lock;		/* mirrors ACPI_SERIAL(acpi_wmi) */
	ACPI_NOTIFY_HANDLER	evh;		/* registered handler */
	void			*evh_ctx;	/* handler context (= hp dev) */
	int			dispatching;	/* fix protocol refcount */
};

static device_t		hpsim_wmi_dev;		/* fake "acpi_wmi0" */
static device_t		hpsim_hp_dev;		/* real acpi_hp child */
static struct hp_sim_softc *hpsim_sc;
static int		hpsim_created;

/* DF-2383 scenario state */
static void		*hpsim_old_sc;
static size_t		hpsim_sc_size;
static void		*hpsim_victim;
static void		*hpsim_victim_before;
static int		hpsim_victim_hit;
static int		hpsim_stall_arm, hpsim_stall_release, hpsim_stall_state;
static int		hpsim_stall_autorelease_ticks;
static int		hpsim_delay_ticks;
static int		hpsim_poison_hostile;
static int		hpsim_inj_started, hpsim_inj_done;
static int		hpsim_release_watchdog_done;
static int		hpsim_uaf_writes;
static int		hpsim_control_done_marker;
static int		hpsim_delayed_marker;

/* DF-2384 knobs */
static int		hpsim_reply_mode;	/* 0=12B ok,1=len4+canary,
						 * 2=len4 real, 3=len0+NULL */
static int		hpsim_reply_val = 0x2ab;
static int		hpsim_wireless_val = 0x100;

/* persistent log ring for the status sysctl */
static char		hpsim_log[8192];
static size_t		hpsim_log_len;
static struct lock	hpsim_log_lock;

static void
hpsim_vlog(const char *fmt, ...)
{
	__va_list ap;

	kprintf(HPSIM);
	__va_start(ap, fmt);
	kvprintf(fmt, ap);
	__va_end(ap);
	kprintf("\n");

	lockmgr(&hpsim_log_lock, LK_EXCLUSIVE);
	if (hpsim_log_len < sizeof(hpsim_log) - 1) {
		int n;
		__va_start(ap, fmt);
		n = kvsnprintf(hpsim_log + hpsim_log_len,
		    sizeof(hpsim_log) - hpsim_log_len - 2, fmt, ap);
		__va_end(ap);
		if (n > 0) {
			if ((size_t)n >= sizeof(hpsim_log) - hpsim_log_len - 2)
				n = sizeof(hpsim_log) - hpsim_log_len - 2;
			hpsim_log_len += n;
			hpsim_log[hpsim_log_len++] = '\n';
			hpsim_log[hpsim_log_len] = 0;
		}
	}
	lockmgr(&hpsim_log_lock, LK_RELEASE);
}

static int
hpsim_wait_for(volatile int *var, int val, int timeout_ticks)
{
	int t = 0;

	while (*var != val && t < timeout_ticks) {
		tsleep(var, 0, "hpsimw", 1);
		t += 1;
	}
	return (*var == val);
}

/* ------------------------------------------------------------------ */
/* interposed acpi_wmi_if KBI methods                                  */
/* ------------------------------------------------------------------ */

#define HP_SIM_WMI_CMI_GUID	"2D114B49-2DFB-4130-B8FE-4A3C09E75133"

static int
hp_sim_provides_guid_string(device_t dev, const char *guid_string)
{
	/*
	 * Real: return max_instance+1 or 0.  We claim all three HP GUIDs
	 * exactly like an HP 8510p WMI BIOS: BIOS GUID (acpi_hp.c:497),
	 * EVENT GUID (installs the notify handler, acpi_hp.c:503-515) and
	 * CMI GUID (has_cmi=1 -> make_dev(/dev/hpcmi), hpcmi_bufptr=-1 so
	 * the verbatim detach skips sbuf_delete -- NOTE: with has_cmi=0
	 * the verbatim attach NEVER initializes hpcmi_bufptr, and
	 * acpi_hp_detach():595-598 then runs sbuf_delete() on the zeroed
	 * sbuf, which the INVARIANTS kernel catches as
	 * "sbuf_delete called with uninitialized or corrupt sbuf" -- an
	 * independent detach-path bug observed live during this run).
	 */
	if (guid_string == NULL)
		return (0);
	if (strcmp(guid_string, HP_SIM_WMI_BIOS_GUID) == 0)
		return (1);
	if (strcmp(guid_string, HP_SIM_WMI_EVENT_GUID) == 0)
		return (1);
	if (strcmp(guid_string, HP_SIM_WMI_CMI_GUID) == 0)
		return (1);
	return (0);
}

static ACPI_STATUS
hp_sim_evaluate_call(device_t dev, const char *guid_string, UINT8 instance,
    UINT32 method_id, const ACPI_BUFFER *in, ACPI_BUFFER *out)
{
	ACPI_OBJECT	*o;
	char		*d;
	UINT32		*params;
	size_t		allocsz, datalen;
	int		cmd = -1, is_write = 0;

	if (in && in->Pointer && in->Length >= 5 * sizeof(UINT32)) {
		params = in->Pointer;
		is_write = (params[1] == 2);
		cmd = params[2];
	}

	switch (hpsim_reply_mode) {
	case 1:				/* len 4 declared, canary tail */
		datalen = 4;
		allocsz = sizeof(ACPI_OBJECT) + 16;
		break;
	case 2:				/* len 4, tight allocation */
		datalen = 4;
		allocsz = sizeof(ACPI_OBJECT) + 4;
		break;
	case 3:				/* zero-length, NULL pointer */
		datalen = 0;
		allocsz = sizeof(ACPI_OBJECT);
		break;
	default:			/* conformant 12-byte reply */
		datalen = 12;
		allocsz = sizeof(ACPI_OBJECT) + 12;
		break;
	}

	o = AcpiOsAllocate(allocsz);
	if (o == NULL)
		return (AE_NO_MEMORY);
	memset(o, 0, allocsz);
	o->Type = ACPI_TYPE_BUFFER;
	o->Buffer.Length = datalen;
	if (datalen) {
		d = (char *)o + sizeof(ACPI_OBJECT);
		o->Buffer.Pointer = d;
		memset(d, 0xcc, datalen);
		*(UINT32 *)d = 0x55434553;		/* result[0] magic */
		if (hpsim_reply_mode == 1) {
			*(UINT32 *)(d + 4) = 0x41414141;	/* result[1] canary A */
			*(UINT32 *)(d + 8) = 0x42424242;	/* result[2] canary B */
		} else if (hpsim_reply_mode == 0) {
			UINT32 v;

			v = hpsim_reply_val;
			if (cmd == HP_CMD_WIRELESS && !is_write)
				v = hpsim_wireless_val;
			*(UINT32 *)(d + 4) = 0;		/* result[1] = 0 */
			*(UINT32 *)(d + 8) = v;		/* result[2] = v */
		}
		/* mode 2: only 4 payload bytes initialized; the OOB reads
		 * (result[1]/result[2]) hit the uninitialized tail of the
		 * allocation.  Attribution via debug.use_malloc_pattern=1
		 * (INVARIANTS fill = 0xff...) or leftover heap data. */
	} else {
		o->Buffer.Pointer = NULL;		/* mode 3 */
	}
	out->Pointer = o;
	return (AE_OK);
}

static ACPI_STATUS
hp_sim_get_block(device_t dev, const char *guid_string, UINT8 instance,
    ACPI_BUFFER *out)
{
	return (AE_NOT_EXIST);		/* CMI not claimed */
}

static ACPI_STATUS
hp_sim_set_block(device_t dev, const char *guid_string, UINT8 instance,
    const ACPI_BUFFER *in)
{
	return (AE_NOT_EXIST);
}

static ACPI_STATUS
hp_sim_install_event_handler(device_t dev, const char *guid_string,
    ACPI_NOTIFY_HANDLER event_handler, void *data)
{
	struct hp_sim_softc *sc = device_get_softc(dev);

	/* mirrors acpi_wmi.c:408-431 */
	lockmgr(&sc->lock, LK_EXCLUSIVE);
	if (guid_string == NULL || event_handler == NULL) {
		lockmgr(&sc->lock, LK_RELEASE);
		return (AE_BAD_PARAMETER);
	}
	sc->evh = event_handler;
	sc->evh_ctx = data;
	lockmgr(&sc->lock, LK_RELEASE);
	hpsim_vlog("install_event_handler: handler=%p ctx=%p (dev=%s)",
	    event_handler, data, device_get_nameunit((device_t)data));
	return (AE_OK);
}

static ACPI_STATUS
hp_sim_remove_event_handler(device_t dev, const char *guid_string)
{
	struct hp_sim_softc *sc = device_get_softc(dev);

	/* mirrors acpi_wmi.c:441-460 (+ fix.diff drain protocol) */
	lockmgr(&sc->lock, LK_EXCLUSIVE);
	sc->evh = NULL;
	sc->evh_ctx = NULL;
#if HP_SIM_FIX_DRAIN
	while (sc->dispatching > 0)
		lksleep(sc, &sc->lock, 0, "hpwmidrn", 0);
#endif
	lockmgr(&sc->lock, LK_RELEASE);
	hpsim_vlog("remove_event_handler: removed (drain=%d)",
	    HP_SIM_FIX_DRAIN);
	return (AE_OK);
}

static ACPI_STATUS
hp_sim_get_event_data(device_t dev, UINT32 event_id, ACPI_BUFFER *out)
{
	ACPI_OBJECT	*o;
	char		*d;

	/* mirrors acpi_wmi.c:466-495: canned _WED reply -- HP hotkey event
	 * payload: 8-byte buffer with leading 0x5 (acpi_hp.c:820-823) */
	o = AcpiOsAllocate(sizeof(ACPI_OBJECT) + 8);
	if (o == NULL)
		return (AE_NO_MEMORY);
	memset(o, 0, sizeof(ACPI_OBJECT) + 8);
	o->Type = ACPI_TYPE_BUFFER;
	o->Buffer.Length = 8;
	d = (char *)o + sizeof(ACPI_OBJECT);
	o->Buffer.Pointer = d;
	d[0] = 0x5;
	out->Pointer = o;

	/*
	 * DF-2383 STALL POINT: this runs INSIDE the real acpi_hp_notify
	 * (invoked at acpi_hp.c:818).  The handler has already captured
	 * `dev` (context) and `sc = device_get_softc(dev)` (acpi_hp.c:812,
	 * 815) and has already read sc->wmi_dev to pass it here.
	 */
	if (hpsim_stall_arm) {
		hpsim_stall_state = 2;		/* INSIDE */
		wakeup(&hpsim_stall_state);
		hpsim_vlog("stall: handler parked mid-acpi_hp_notify");
		while (!hpsim_stall_release)
			tsleep(&hpsim_stall_release, 0, "hpstall", 1);
		hpsim_stall_state = 3;		/* RELEASED */
	}
	return (AE_OK);
}

/* ------------------------------------------------------------------ */
/* fake acpi_wmi bus driver (newbus)                                   */
/* ------------------------------------------------------------------ */

static int
hp_sim_wmi_probe(device_t dev)
{
	/* children are only created by us, named "acpi_wmi" */
	device_set_desc(dev, "HP WMI simulation bus");
	return (BUS_PROBE_SPECIFIC);
}

static int
hp_sim_wmi_attach(device_t dev)
{
	struct hp_sim_softc *sc = device_get_softc(dev);

	sc->evh = NULL;
	sc->evh_ctx = NULL;
	sc->dispatching = 0;
	lockinit(&sc->lock, "hpwmi_sim", 0, 0);

	/*
	 * NOTE: with the fake bus parented to the real acpi0, the
	 * verbatim driver resolves the parent softc to the REAL
	 * acpi_softc (see hpsim_create).  The embedded struct below is
	 * only a defensive fallback for direct consumers.
	 */
	sysctl_ctx_init(&sc->acpi.acpi_sysctl_ctx);
	sc->acpi.acpi_sysctl_tree = NULL;
	{
		devclass_t adc = devclass_find("acpi");

		if (adc != NULL) {
			device_t acpi0 = devclass_get_device(adc, 0);

			if (acpi0 != NULL) {
				struct acpi_softc *real = device_get_softc(acpi0);

				sc->acpi.acpi_sysctl_tree =
				    real->acpi_sysctl_tree;
			}
		}
	}
	if (sc->acpi.acpi_sysctl_tree == NULL) {
		sc->acpi.acpi_sysctl_tree = SYSCTL_ADD_NODE(
		    &sc->acpi.acpi_sysctl_ctx, SYSCTL_STATIC_CHILDREN(_hw),
		    OID_AUTO, "hp_sim_bus", CTLFLAG_RD, 0,
		    "simulated ACPI parent tree");
		if (sc->acpi.acpi_sysctl_tree == NULL)
			return (ENXIO);
	}

	hpsim_sc = sc;
	hpsim_wmi_dev = dev;
	hpsim_vlog("fake acpi_wmi bus attached: %s",
	    device_get_nameunit(dev));
	return (0);
}

static int
hp_sim_wmi_detach(device_t dev)
{
	struct hp_sim_softc *sc = device_get_softc(dev);

	if (sc->evh)
		return (EBUSY);
	sysctl_ctx_free(&sc->acpi.acpi_sysctl_ctx);
	hpsim_wmi_dev = NULL;
	hpsim_sc = NULL;
	return (0);
}

static device_method_t hp_sim_wmi_methods[] = {
	/* device interface */
	DEVMETHOD(device_probe,		hp_sim_wmi_probe),
	DEVMETHOD(device_attach,	hp_sim_wmi_attach),
	DEVMETHOD(device_detach,	hp_sim_wmi_detach),

	/* acpi_wmi interface (the interposition) */
	DEVMETHOD(acpi_wmi_provides_guid_string,
				    hp_sim_provides_guid_string),
	DEVMETHOD(acpi_wmi_evaluate_call, hp_sim_evaluate_call),
	DEVMETHOD(acpi_wmi_get_block,	hp_sim_get_block),
	DEVMETHOD(acpi_wmi_set_block,	hp_sim_set_block),
	DEVMETHOD(acpi_wmi_install_event_handler,
				    hp_sim_install_event_handler),
	DEVMETHOD(acpi_wmi_remove_event_handler,
				    hp_sim_remove_event_handler),
	DEVMETHOD(acpi_wmi_get_event_data, hp_sim_get_event_data),

	DEVMETHOD_END
};

static driver_t hp_sim_wmi_driver = {
	"acpi_wmi",			/* devclass name the hp driver sits on */
	hp_sim_wmi_methods,
	sizeof(struct hp_sim_softc),
};

static devclass_t hp_sim_wmi_devclass;

DRIVER_MODULE(acpi_wmi, acpi, hp_sim_wmi_driver, hp_sim_wmi_devclass,
	    NULL, NULL);
MODULE_VERSION(acpi_wmi, 1);

/* ------------------------------------------------------------------ */
/* injection (mirrors acpi_wmi_notify_handler, acpi_wmi.c:627-653)     */
/* ------------------------------------------------------------------ */

static void
hp_sim_inject_thread(void *arg)
{
	struct hp_sim_softc	*sc = hpsim_sc;
	ACPI_NOTIFY_HANDLER	 h = NULL;
	void			*ctx = NULL;

	lockmgr(&sc->lock, LK_EXCLUSIVE);
	if (sc->evh) {
		h = sc->evh;
		ctx = sc->evh_ctx;
#if HP_SIM_FIX_DRAIN
		sc->dispatching++;
#endif
	}
	lockmgr(&sc->lock, LK_RELEASE);	/* <-- released BEFORE dispatch,
					 * exactly like acpi_wmi.c:649 */

	hpsim_inj_started = 1;
	wakeup(&hpsim_inj_started);

	if (h) {
		if (hpsim_delay_ticks)
			tsleep(hp_sim_inject_thread, 0, "hpidelay",
			    hpsim_delay_ticks);
		hpsim_vlog("dispatch: calling handler %p(ctx=%p) "
		    "AFTER lock release", h, ctx);
		h(ACPI_ROOT_OBJECT, (UINT32)(uintptr_t)arg, ctx);
#if HP_SIM_FIX_DRAIN
		lockmgr(&sc->lock, LK_EXCLUSIVE);
		sc->dispatching--;
		if (sc->dispatching == 0)
			wakeup(sc);
		lockmgr(&sc->lock, LK_RELEASE);
#endif
	} else {
		hpsim_vlog("dispatch: no handler registered");
	}
	hpsim_inj_done = 1;
	wakeup(&hpsim_inj_done);
	/* lwkt thread exits when body returns */
}

/* auto-release watchdog: plays the role of the preemption ending */
static void
hp_sim_release_thread(void *arg)
{
	int t = 0;

	while (!hpsim_stall_release && t < hpsim_stall_autorelease_ticks) {
		tsleep(&hpsim_release_watchdog_done, 0, "hpwdog", 1);
		t += 1;
	}
	if (!hpsim_stall_release) {
		hpsim_stall_release = 1;
		wakeup(&hpsim_stall_release);
		hpsim_vlog("watchdog: stalled handler auto-released into "
		    "(possibly freed) softc");
	}
	hpsim_release_watchdog_done = 1;
	wakeup(&hpsim_release_watchdog_done);
}

/* ------------------------------------------------------------------ */
/* create / teardown (real newbus paths, kldunload-equivalent)         */
/* ------------------------------------------------------------------ */

/*
 * The fake acpi_wmi bus device is parented to the REAL acpi0, matching
 * the real topology (acpi_wmi is a child of acpi).  This matters:
 * verbatim acpi_hp_attach():495 does acpi_device_get_parent_softc(
 * sc->wmi_dev), which returns device_get_softc(device_get_parent(
 * wmi_dev)) cast to struct acpi_softc * -- with acpi0 as the parent
 * that is the REAL acpi_softc with its real hw.acpi sysctl tree.
 */
static device_t
hpsim_parent_bus(void)
{
	devclass_t	dc;
	device_t	acpi0;

	dc = devclass_find("acpi");
	if (dc == NULL)
		return (NULL);
	acpi0 = devclass_get_device(dc, 0);
	return (acpi0);
}

static int
hpsim_create(void)
{
	device_t	acpi0;

	if (hpsim_created)
		return (0);

	acpi0 = hpsim_parent_bus();
	if (acpi0 == NULL)
		return (ENXIO);

	/* go through the REAL acpi bus add_child so the child gets ACPI
	 * ivars (bus_child_pnpinfo_str in devadded() derefs them) */
	hpsim_wmi_dev = BUS_ADD_CHILD(acpi0, acpi0, 0, "acpi_wmi", -1);
	if (hpsim_wmi_dev == NULL)
		return (ENXIO);
	device_quiet(hpsim_wmi_dev);	/* skip print_child */
	if (device_probe_and_attach(hpsim_wmi_dev) != 0) {
		hpsim_wmi_dev = NULL;
		return (ENXIO);
	}
	/* the verbatim acpi_hp driver sits in devclass "acpi_wmi" */
	hpsim_hp_dev = device_add_child(hpsim_wmi_dev, "acpi_hp", -1);
	if (hpsim_hp_dev == NULL)
		return (ENXIO);
	device_quiet(hpsim_hp_dev);
	if (device_probe_and_attach(hpsim_hp_dev) != 0) {
		hpsim_hp_dev = NULL;
		return (ENXIO);
	}
	hpsim_created = 1;
	hpsim_vlog("created: %s + %s (sc=%p)",
	    device_get_nameunit(hpsim_wmi_dev),
	    device_get_nameunit(hpsim_hp_dev),
	    device_get_softc(hpsim_hp_dev));
	return (0);
}

static int
hpsim_destroy(void)
{
	if (!hpsim_created)
		return (0);

	hpsim_old_sc = device_get_softc(hpsim_hp_dev);
	hpsim_sc_size = device_get_driver(hpsim_hp_dev)->size;

	hpsim_vlog("teardown: device_delete_child -> real acpi_hp_detach "
	    "+ newbus kfree(softc, M_BUS) [kldunload path] sc=%p size=%zu",
	    hpsim_old_sc, hpsim_sc_size);
	if (device_delete_child(hpsim_wmi_dev, hpsim_hp_dev) != 0) {
		hpsim_vlog("teardown: delete_child FAILED");
		return (EBUSY);
	}
	hpsim_hp_dev = NULL;
	return (0);
}

static void
hpsim_victim_fill(void)
{
	uint8_t	*p = hpsim_victim;
	device_t fake_wmi = hpsim_wmi_dev;
	size_t	sz = hpsim_sc_size;

	memset(p, 0xA5, sz);

	/* benign poison: keep wmi_dev valid so the resumed handler flows
	 * through the interposed KBI (attribution of the WRITES) */
	*(device_t *)(p + offsetof(struct hp_mirror, wmi_dev)) =
	    hpsim_poison_hostile ?
	    (device_t)0xdeadbeefdeadbeefUL : fake_wmi;
	*(int *)(p + offsetof(struct hp_mirror, verbose)) = 1; /* printf read */
	*(int *)(p + offsetof(struct hp_mirror, was_wlan_on_air)) = 0;
	*(int *)(p + offsetof(struct hp_mirror, was_bluetooth_on_air)) = 0;
	*(int *)(p + offsetof(struct hp_mirror, was_wwan_on_air)) = 0;

	/* baseline snapshot AFTER our own poison fill: any later change
	 * is an external (UAF) write */
	memcpy(hpsim_victim_before, p, sz);
}

static void
hpsim_victim_report(const char *tag)
{
	uint8_t	*p = hpsim_victim;
	size_t	sz = hpsim_sc_size;
	size_t	i;
	int	nchg = 0, first = 1;

	hpsim_uaf_writes = 0;
	for (i = 0; i < sz; i++) {
		if (p[i] != ((uint8_t *)hpsim_victim_before)[i]) {
			int k, known = 0;
			for (k = 0; hp_mirror_fields[k].name; k++) {
				if (i >= (size_t)hp_mirror_fields[k].off &&
				    i < (size_t)hp_mirror_fields[k].off + 4) {
					hpsim_vlog("%s victim WRITE at "
					    "softc+%zu (field %s): "
					    "0x%02x -> 0x%02x", tag, i,
					    hp_mirror_fields[k].name,
					    ((uint8_t *)hpsim_victim_before)[i],
					    p[i]);
					known = 1;
					hpsim_uaf_writes++;
				}
			}
			if (!known && (nchg - hpsim_uaf_writes) < 8)
				hpsim_vlog("%s victim byte changed at "
				    "softc+%zu: 0x%02x -> 0x%02x", tag, i,
				    ((uint8_t *)hpsim_victim_before)[i], p[i]);
			nchg++;
			first = 0;
		}
	}
	hpsim_vlog("%s victim diff: %d changed byte(s), %d at known "
	    "softc field(s); victim==old_sc %s", tag, nchg, hpsim_uaf_writes,
	    hpsim_victim_hit ? "YES" : "no");
	/* dump the interesting window: wmi_dev(+8) .. was_*(+56..68) */
	if (sz >= 72) {
		uint64_t w = *(uint64_t *)(p + 8);
		hpsim_vlog("%s victim window: +8 wmi_dev=%016llx "
		    "+28 verbose=%08x +56 was_wlan=%08x +60 was_bt=%08x "
		    "+64 was_wwan=%08x", tag, (unsigned long long)w,
		    *(int *)(p + 28), *(int *)(p + 56), *(int *)(p + 60),
		    *(int *)(p + 64));
	}
}

static void
hpsim_victim_alloc(void)
{
	int	i;

	hpsim_victim_hit = 0;
	for (i = 0; i < 32; i++) {
		/* same size class, same malloc type as newbus softc
		 * (subr_bus.c:1953 kmalloc(size, M_BUS, ...)); the
		 * per-CPU free list is LIFO so the first try usually
		 * lands exactly on the just-freed chunk */
		hpsim_victim = kmalloc(hpsim_sc_size, M_BUS,
		    M_INTWAIT | M_ZERO);
		if (hpsim_victim == hpsim_old_sc) {
			hpsim_victim_hit = 1;
			break;
		}
	}
	hpsim_vlog("victim spray: victim=%p old_sc=%p hit=%d",
	    hpsim_victim, hpsim_old_sc, hpsim_victim_hit);
	hpsim_victim_fill();
}

/* ------------------------------------------------------------------ */
/* scenarios                                                           */
/* ------------------------------------------------------------------ */

static void
hpsim_reset_state(void)
{
	hpsim_stall_arm = 0;
	hpsim_stall_release = 0;
	hpsim_stall_state = 0;
	hpsim_stall_autorelease_ticks = 0;
	hpsim_delay_ticks = 0;
	hpsim_inj_started = 0;
	hpsim_inj_done = 0;
	hpsim_release_watchdog_done = 0;
	hpsim_uaf_writes = 0;

	hpsim_reply_mode = 0;
	hpsim_wireless_val = 0x100;
}

/*
 * scenario 1 "uaf": inject an event; the handler stalls inside the real
 * acpi_hp_notify (via the interposed get_event_data); teardown runs the
 * REAL detach + newbus free; the freed chunk is reallocated as a victim;
 * a watchdog releases the handler -> it continues on the freed softc.
 * Baseline: UAF write captured.  Fixed (drain): teardown blocks until
 * the handler finished on the LIVE softc -> no victim writes.
 */
static int
hpsim_scenario_uaf(void)
{
	if (hpsim_created) {
		hpsim_destroy();
		device_delete_child(hpsim_parent_bus(), hpsim_wmi_dev);
		hpsim_wmi_dev = NULL;
		hpsim_created = 0;
	}
	hpsim_reset_state();
	if (hpsim_create() != 0) {
		hpsim_vlog("scenario uaf: create FAILED");
		return (ENXIO);
	}
	hpsim_victim = NULL;

	hpsim_stall_arm = 1;
	hpsim_stall_autorelease_ticks = 2 * hz;	/* handler resumes at +2s */

	/* watch the previous victim go away */
	if (hpsim_victim_before == NULL)
		hpsim_victim_before = kmalloc(4096, M_TEMP, M_INTWAIT);

	kthread_create(hp_sim_release_thread, NULL, NULL, "hpwdog");
	kthread_create(hp_sim_inject_thread, (void *)(uintptr_t)0x80, NULL,
	    "hpsiminj");

	if (!hpsim_wait_for(&hpsim_inj_started, 1, 10 * hz) ||
	    !hpsim_wait_for(&hpsim_stall_state, 2, 10 * hz)) {
		hpsim_vlog("scenario uaf: inject/stall FAILED to arm");
		hpsim_stall_release = 1;
		wakeup(&hpsim_stall_release);
		return (ENXIO);
	}
	hpsim_vlog("scenario uaf: handler confirmed stalled inside "
	    "acpi_hp_notify; running detach+free underneath it");

	hpsim_destroy();		/* real detach + kfree(softc) */
	hpsim_created = 0;		/* wmi_dev torn down below */

	hpsim_victim_alloc();		/* reclaim + poison the freed softc */

	/* wait for the handler to finish (baseline: on the FREED softc) */
	hpsim_wait_for(&hpsim_inj_done, 1, 30 * hz);
	hpsim_wait_for(&hpsim_release_watchdog_done, 1, 30 * hz);

	hpsim_victim_report("uaf");
	if (hpsim_uaf_writes > 0)
		hpsim_vlog("RESULT: UAF WRITE CONFIRMED on freed softc "
		    "(%d field write(s))", hpsim_uaf_writes);
	else if (hpsim_inj_done)
		hpsim_vlog("RESULT: no post-free writes (handler drained "
		    "before free?)");

	/* tear the fake wmi bus down too */
	device_delete_child(hpsim_parent_bus(), hpsim_wmi_dev);
	hpsim_wmi_dev = NULL;
	hpsim_sc = NULL;
	return (0);
}

/*
 * scenario 2 "control": identical teardown+victim, but no in-flight
 * notify.  The victim must come back untouched.
 */
static int
hpsim_scenario_control(void)
{
	if (hpsim_created) {
		hpsim_destroy();
		device_delete_child(hpsim_parent_bus(), hpsim_wmi_dev);
		hpsim_wmi_dev = NULL;
		hpsim_created = 0;
	}
	hpsim_reset_state();
	if (hpsim_create() != 0)
		return (ENXIO);
	if (hpsim_victim_before == NULL)
		hpsim_victim_before = kmalloc(4096, M_TEMP, M_INTWAIT);

	hpsim_destroy();
	hpsim_created = 0;
	hpsim_victim_alloc();

	/* quiet period: nothing is in flight */
	tsleep(&hpsim_control_done_marker, 0, "hpctl", hz);

	hpsim_victim_report("control");

	device_delete_child(hpsim_parent_bus(), hpsim_wmi_dev);
	hpsim_wmi_dev = NULL;
	hpsim_sc = NULL;
	return (0);
}


/*
 * scenario 3 "delayed": handler copy made, then the dispatch is delayed
 * by `delay_ticks` (mirrors a preempted acpi_wmi_notify_handler between
 * lock release and the handler call).  Teardown starts during the delay.
 * Baseline: handler runs entirely on freed memory.  Fixed: remove/detach
 * blocks until the late handler completed -> clean.
 */
static int
hpsim_scenario_delayed(void)
{
	if (hpsim_created) {
		hpsim_destroy();
		device_delete_child(hpsim_parent_bus(), hpsim_wmi_dev);
		hpsim_wmi_dev = NULL;
		hpsim_created = 0;
	}
	hpsim_reset_state();
	if (hpsim_create() != 0)
		return (ENXIO);
	if (hpsim_victim_before == NULL)
		hpsim_victim_before = kmalloc(4096, M_TEMP, M_INTWAIT);

	hpsim_delay_ticks = 2 * hz;	/* dispatch 2s late */
	kthread_create(hp_sim_inject_thread, (void *)(uintptr_t)0x80, NULL,
	    "hpsiminj");
	hpsim_wait_for(&hpsim_inj_started, 1, 10 * hz);
	hpsim_vlog("scenario delayed: dispatch armed with 2s delay; "
	    "teardown NOW (handler copied but not yet invoked)");
	tsleep(&hpsim_delayed_marker, 0, "hpyld", hz / 10);

	hpsim_destroy();
	hpsim_created = 0;
	hpsim_victim_alloc();

	hpsim_wait_for(&hpsim_inj_done, 1, 30 * hz);
	hpsim_victim_report("delayed");

	device_delete_child(hpsim_parent_bus(), hpsim_wmi_dev);
	hpsim_wmi_dev = NULL;
	hpsim_sc = NULL;
	return (0);
}


/*
 * scenario 4 "postfree": teardown first, then a fresh WMI event.  On the
 * real stack the handler registration SURVIVES detach because
 * acpi_hp_detach():591 invokes the KBI on its own dev (kobj falls back to
 * kobj_error_method -> ENXIO, handler never removed), so the next event
 * dispatches with the freed hp device as context.
 */
static int
hpsim_scenario_postfree(void)
{
	if (hpsim_created) {
		hpsim_destroy();
		device_delete_child(hpsim_parent_bus(), hpsim_wmi_dev);
		hpsim_wmi_dev = NULL;
		hpsim_created = 0;
	}
	hpsim_reset_state();
	if (hpsim_create() != 0)
		return (ENXIO);
	if (hpsim_victim_before == NULL)
		hpsim_victim_before = kmalloc(4096, M_TEMP, M_INTWAIT);

	/* simulate the ENXIO remove: registration survives teardown */
	hpsim_destroy();
	hpsim_created = 0;
	hpsim_victim_alloc();

	hpsim_vlog("scenario postfree: injecting event with freed device "
	    "as context");
	kthread_create(hp_sim_inject_thread, (void *)(uintptr_t)0x80, NULL,
	    "hpsiminj");
	hpsim_wait_for(&hpsim_inj_started, 1, 10 * hz);
	hpsim_wait_for(&hpsim_inj_done, 1, 30 * hz);
	hpsim_victim_report("postfree");

	device_delete_child(hpsim_parent_bus(), hpsim_wmi_dev);
	hpsim_wmi_dev = NULL;
	hpsim_sc = NULL;
	return (0);
}

/* ------------------------------------------------------------------ */
/* command plane: a sysctl handler may NOT create/destroy sysctls
 * (kernel_sysctl holds SYSCTL_SLOCK across the handler while
 * sysctl_register_oid wants SYSCTL_XLOCK -> self-deadlock), so every
 * structural operation runs in this worker kthread, queued via a
 * generation counter and observed through hw.hpsim.cmd_done.
 * ------------------------------------------------------------------ */

static int		hpsim_cmd;	/* 1=create 2=destroy 3=inject
					 * 4=uaf 5=control 6=delayed
					 * 7=postfree */
static volatile int	hpsim_cmdgen;
static int		hpsim_cmd_done;	/* == cmdgen when finished */

static void	hpsim_inspect_tree(void);

static void
hp_sim_worker(void *arg)
{
	int	lastgen = 0;

	for (;;) {
		tsleep(&hpsim_cmdgen, 0, "hpsimwk", 0);
		if (hpsim_cmdgen == lastgen)
			continue;
		lastgen = hpsim_cmdgen;
		hpsim_cmd_done = 0;
		hpsim_vlog("worker: exec cmd=%d", hpsim_cmd);
		switch (hpsim_cmd) {
		case 1:
			hpsim_reset_state();
			hpsim_create();
			break;
		case 2:
			if (hpsim_created) {
				hpsim_destroy();
				device_delete_child(
				    hpsim_parent_bus(), hpsim_wmi_dev);
				hpsim_wmi_dev = NULL;
				hpsim_sc = NULL;
				hpsim_created = 0;
			}
			break;
		case 3:
			if (hpsim_created) {
				hpsim_inj_started = 0;
				hpsim_inj_done = 0;
				kthread_create(hp_sim_inject_thread,
				    (void *)(uintptr_t)0x80, NULL, "hpsiminj");
			}
			break;
		case 4:
			hpsim_scenario_uaf();
			break;
		case 5:
			hpsim_scenario_control();
			break;
		case 6:
			hpsim_scenario_delayed();
			break;
		case 7:
			hpsim_scenario_postfree();
			break;
		case 8:
			hpsim_inspect_tree();
			break;
		default:
			break;
		}
		hpsim_vlog("worker: cmd=%d DONE", hpsim_cmd);
		hpsim_cmd_done = lastgen;
		wakeup(&hpsim_cmd_done);
	}
}

/*
 * cmd 8: inspect the real hw.acpi sysctl list the verbatim attach wants
 * to hook into (diagnostic for the strcmp(NULL) trap at attach).
 */
static void
hpsim_inspect_tree(void)
{
	devclass_t	adc;
	device_t	acpi0;
	struct acpi_softc *asc;
	struct sysctl_oid_list *list;
	struct sysctl_oid *oidp;
	struct sysctl_ctx_list ctx;
	int n = 0;

	adc = devclass_find("acpi");
	kprintf(HPSIM "inspect: acpi devclass=%p\n", adc);
	if (adc == NULL)
		return;
	acpi0 = devclass_get_device(adc, 0);
	kprintf(HPSIM "inspect: acpi0=%p\n", acpi0);
	if (acpi0 == NULL)
		return;
	asc = device_get_softc(acpi0);
	kprintf(HPSIM "inspect: acpi_sc=%p tree=%p\n", asc,
	    asc ? asc->acpi_sysctl_tree : NULL);
	if (asc == NULL || asc->acpi_sysctl_tree == NULL)
		return;
	list = SYSCTL_CHILDREN(asc->acpi_sysctl_tree);
	kprintf(HPSIM "inspect: children list=%p first=%p\n", list,
	    SLIST_FIRST(list));
	SLIST_FOREACH(oidp, list, oid_link) {
		kprintf(HPSIM "inspect: [%02d] oid=%p name=%s kind=%08x\n",
		    n++, oidp,
		    oidp->oid_name ? oidp->oid_name : "** NULL NAME **",
		    oidp->oid_kind);
	}
	kprintf(HPSIM "inspect: walked %d entries; now trying a probe "
	    "SYSCTL_ADD_NODE under the same parent\n", n);
	sysctl_ctx_init(&ctx);
	if (SYSCTL_ADD_NODE(&ctx, list, OID_AUTO, "hpsim_probe",
	    CTLFLAG_RD, 0, "") == NULL)
		kprintf(HPSIM "inspect: probe node FAILED\n");
	else
		kprintf(HPSIM "inspect: probe node OK\n");
	sysctl_ctx_free(&ctx);
}

static int
hpsim_sysctl_cmd(SYSCTL_HANDLER_ARGS)
{
	int v = 0, error;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (v < 1 || v > 8)
		return (EINVAL);
	hpsim_cmd = v;
	hpsim_cmdgen++;
	wakeup(&hpsim_cmdgen);
	return (0);
}

static int
hpsim_sysctl_status(SYSCTL_HANDLER_ARGS)
{
	int len = strlen(hpsim_log);

	return (SYSCTL_OUT(req, hpsim_log, len));
}

SYSCTL_NODE(_hw, OID_AUTO, hpsim, CTLFLAG_RD, 0, "acpi_hp verbatim sim");

SYSCTL_PROC(_hw_hpsim, OID_AUTO, cmd, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, hpsim_sysctl_cmd, "I",
    "queue worker cmd: 1=create 2=destroy 3=inject 4=uaf 5=control "
    "6=delayed 7=postfree");
SYSCTL_PROC(_hw_hpsim, OID_AUTO, log, CTLTYPE_STRING | CTLFLAG_RD,
    0, 0, hpsim_sysctl_status, "A", "scenario log");

SYSCTL_INT(_hw_hpsim, OID_AUTO, reply_mode, CTLFLAG_RW,
    &hpsim_reply_mode, 0,
    "evaluate_call reply: 0=12B 1=len4+canary 2=len4 3=len0+NULL");
SYSCTL_INT(_hw_hpsim, OID_AUTO, reply_val, CTLFLAG_RW,
    &hpsim_reply_val, 0, "conformant result[2] value");
SYSCTL_INT(_hw_hpsim, OID_AUTO, wireless_val, CTLFLAG_RW,
    &hpsim_wireless_val, 0, "WIRELESS command reply bits");
SYSCTL_INT(_hw_hpsim, OID_AUTO, poison_hostile, CTLFLAG_RW,
    &hpsim_poison_hostile, 0, "victim wmi_dev = hostile pointer");
SYSCTL_INT(_hw_hpsim, OID_AUTO, created, CTLFLAG_RD,
    &hpsim_created, 0, "bus+device created");
SYSCTL_INT(_hw_hpsim, OID_AUTO, uaf_writes, CTLFLAG_RD,
    &hpsim_uaf_writes, 0, "last scenario: victim field writes");
SYSCTL_INT(_hw_hpsim, OID_AUTO, cmd_done, CTLFLAG_RD,
    &hpsim_cmd_done, 0, "generation of the last finished worker cmd");
SYSCTL_INT(_hw_hpsim, OID_AUTO, stall_state, CTLFLAG_RD,
    &hpsim_stall_state, 0, "0=off 1=armed 2=inside 3=released");

static void
hpsim_boot(void *arg)
{
	size_t	sz = sizeof(struct hp_mirror);

	lockinit(&hpsim_log_lock, "hplog", 0, 0);
	kthread_create(hp_sim_worker, NULL, NULL, "hpsimwork");
	kprintf(HPSIM "harness up: fix_drain=%d sizeof(softc mirror)=%zu "
	    "(offsets: wmi_dev=%zu verbose=%zu was_wlan=%zu was_bt=%zu "
	    "was_wwan=%zu)\n", HP_SIM_FIX_DRAIN, sz,
	    offsetof(struct hp_mirror, wmi_dev),
	    offsetof(struct hp_mirror, verbose),
	    offsetof(struct hp_mirror, was_wlan_on_air),
	    offsetof(struct hp_mirror, was_bluetooth_on_air),
	    offsetof(struct hp_mirror, was_wwan_on_air));
	hpsim_vlog("harness up (fix_drain=%d)", HP_SIM_FIX_DRAIN);
}

SYSINIT(hpsim, SI_SUB_CONFIGURE, SI_ORDER_MIDDLE, hpsim_boot, NULL);
