/*
 * panasim.c -- verbatim-interposition harness for
 *              sys/dev/acpica/acpi_panasonic/acpi_panasonic.c
 *              (DF-2386, DF-2387, DF-2590, DF-2591)
 *
 * Strategy (same shape as the acpi_hp rig, DF-2383/DF-2384):
 *   - The REAL acpi_panasonic.c is compiled VERBATIM into the kernel
 *     via pana_shim.c (build glue that #defines every ACPI external
 *     the driver calls to a panasim_* implementation BEFORE
 *     #including the sha256-gated verbatim source).  The driver source
 *     itself is byte-identical to the repo file
 *     (sha256 23e49741027f53dcedd05423f7c5eacd1be0dada1c46277dde0be6694b8d0912).
 *   - This file provides the fabricated ACPI world:
 *       panasim_AcpiEvaluateObject   runtime-selectable SINF/HINF/SSET
 *                                    replies (succeed / fail with the
 *                                    buffer untouched exactly like real
 *                                    ACPICA nsxfeval.c:536-600 failure
 *                                    paths / short package with canary
 *                                    tail / tight allocation)
 *       panasim_AcpiInstallNotifyHandler  captures the verbatim
 *                                    acpi_panasonic_notify + ctx so a
 *                                    notify 0x80 can be injected the
 *                                    way ACPICA's deferred dispatch
 *                                    thread would
 *   - The fake acpi_panasonic device is added to the REAL acpi0 via
 *     the real newbus BUS_ADD_CHILD, so the REAL verbatim probe /
 *     attach / sysctl / detach / shutdown code runs unmodified, its
 *     sysctls appear under hw.acpi.panasonic.* (CTLFLAG_ANYBODY,
 *     acpi_panasonic.c:171) and are readable by ANY local user.
 *
 * Findings exercised through this rig:
 *   DF-2386 : sinf AcpiEvaluateObject failure -> res->Type NULL deref
 *             (acpi_panasonic.c:277-279), unprivileged sysctl read
 *   DF-2387 : sinf Elements[index] read without index < Count check
 *             (acpi_panasonic.c:280) -> OOB value to userspace
 *   DF-2590 : hkey_event AcpiEvaluateObject failure -> res->Type NULL
 *             deref (acpi_panasonic.c:403-405), ACPI notify 0x80
 *   DF-2591 : acpi_panasonic_shutdown -> hkey_sound_mute without
 *             ACPI_SERIAL_BEGIN (acpi_panasonic.c:231, assert :374)
 *
 * The interposed SINF/HINF "firmware" lives at exactly the boundary
 * the findings describe: what the real AcpiEvaluateObject would have
 * returned to the verbatim driver.
 */

#include "opt_acpi.h"
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.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 <sys/power.h>

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

#include "panasim.h"

#define PANASIM "PANASIM: "

MALLOC_DEFINE(M_PANASIM, "panasim", "PANASIM fabricated ACPI objects");

/* SINF reply modes */
#define	SINF_FAIL	0	/* AE_NO_MEMORY, buffer untouched (DF-2386) */
#define	SINF_CANARY	1	/* Package Count=2, 9-element backing store
				 * with per-index canaries past Count
				 * (DF-2387 attribution) */
#define	SINF_TIGHT	2	/* Package Count=2, 2-element backing store:
				 * reads past the kmalloc = real heap OOB
				 * (DF-2387 leak) */
#define	SINF_TIGHT_SPRAY 3	/* tight + groomed 256-zone neighbors
				 * (marker chunks + freed chunks) */
#define	SINF_SANE	4	/* well-formed 9-element package (control) */

/* HINF reply modes */
#define	HINF_FAIL	0	/* AE_NO_MEMORY, buffer untouched (DF-2590) */
#define	HINF_INT	1	/* integer reply = hinf_val (happy path) */

static int	panasim_sinf_mode = SINF_SANE;
static int	panasim_hinf_mode = HINF_INT;
static int	panasim_hinf_val = 0x81;	/* key 1 pressed */
static int	panasim_power_profile = POWER_PROFILE_PERFORMANCE;
static int	panasim_created;		/* fake device attached */
static int	panasim_notify_seen;		/* acpi_UserNotify reached */
static int	panasim_sinf_calls;
static int	panasim_sset_calls;
static int	panasim_sset_last_index = -1;
static int	panasim_sset_last_val = -1;
static int	panasim_last_sinf_status = -1;

static device_t	panasim_acpi0;
static device_t	panasim_child;

/* captured by the interposed AcpiInstallNotifyHandler */
static ACPI_NOTIFY_HANDLER	panasim_notify_handler;
static void			*panasim_notify_ctx;
static ACPI_HANDLE		panasim_notify_handle;

/* the handle we hand the driver: a real, always-valid ACPICA object
 * (the namespace root) -- stray real-ACPICA use of it (e.g. newbus
 * devd pnpinfo) degrades gracefully to "not found"; the interposed
 * evaluate never looks at it. */
static ACPI_HANDLE	panasim_handle_cookie = ACPI_ROOT_OBJECT;

/* persistent log ring for the status sysctl */
static char		panasim_log[8192];
static size_t		panasim_log_len;
static struct lock	panasim_log_lock;

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

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

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

/* ------------------------------------------------------------------ */
/* interposed ACPI externals (called by the VERBATIM driver TU)        */
/* ------------------------------------------------------------------ */

/*
 * Fabricate a Package ACPI_OBJECT.  Backing allocation is
 * sizeof(ACPI_OBJECT) [the package object itself, with
 * Package.Elements pointing just past it] + nelts * sizeof(ACPI_OBJECT).
 */
static ACPI_OBJECT *
panasim_make_package(int count, int backed)
{
	ACPI_OBJECT *o, *e;
	size_t sz = sizeof(ACPI_OBJECT) + backed * sizeof(ACPI_OBJECT);
	int i;

	o = kmalloc(sz, M_PANASIM, M_INTWAIT | M_ZERO);
	o->Type = ACPI_TYPE_PACKAGE;
	o->Package.Count = count;
	e = (ACPI_OBJECT *)((char *)o + sizeof(ACPI_OBJECT));
	o->Package.Elements = e;
	for (i = 0; i < backed; i++) {
		e[i].Type = ACPI_TYPE_INTEGER;
		if (i < count) {
			/* sane-looking values for the declared elements */
			e[i].Integer.Value = (i == 2) ? 255 :
			    (i == 3) ? 0 : (i == 4) ? 128 : (i == 8) ? 0 : 0xAA;
		} else {
			/* canary: each index past Count gets its own
			 * pattern C<i>C<i>C<i>C<i>... so the value that
			 * reaches userspace identifies the exact OOB
			 * index that was read */
			e[i].Integer.Value = 0xC0C0C0C0C0C0C0C0ULL +
			    ((UINT64)(i & 0xf) * 0x0101010101010101ULL);
		}
	}
	return (o);
}

/*
 * Interposed AcpiEvaluateObject.  Mirrors the REAL ACPICA contract
 * (sys/contrib/dev/acpica/source/components/namespace/nsxfeval.c,
 * AcpiEvaluateObject :525-631): on ANY failure the ReturnBuffer is
 * left exactly as the caller initialized it -- Pointer stays NULL,
 * nothing is written (the `!Info->ReturnObject -> Length=0` path at
 * :541-545 also never touches Pointer).  That is the failure contract
 * acpi_panasonic_sinf()/acpi_panasonic_hkey_event() violate.
 */
ACPI_STATUS
panasim_AcpiEvaluateObject(ACPI_HANDLE h, ACPI_STRING pathname,
    ACPI_OBJECT_LIST *ExternalParameters, ACPI_BUFFER *ReturnBuffer)
{
	char path[8];
	int i;

	for (i = 0; i < 7 && pathname && pathname[i]; i++)
		path[i] = pathname[i];
	path[i] = 0;

	if (pathname == NULL) {
		/* sset-style activation eval (#if 0 block) is not live */
		panasim_last_sinf_status = -2;
		return (AE_OK);
	}
		if (strcmp(path, "SINF") == 0) {
			panasim_sinf_calls++;
			switch (panasim_sinf_mode) {
			case SINF_FAIL:
				panasim_last_sinf_status = SINF_FAIL;
				panasim_vlog("eval SINF -> AE_NO_MEMORY (buffer "
				    "untouched, Pointer stays NULL)");
				return (AE_NO_MEMORY);
			case SINF_CANARY:
				ReturnBuffer->Pointer =
				    panasim_make_package(2, 9);
				ReturnBuffer->Length = sizeof(ACPI_OBJECT) +
				    9 * sizeof(ACPI_OBJECT);
				panasim_last_sinf_status = SINF_CANARY;
				panasim_vlog("eval SINF -> Package Count=2 "
				    "(9-elem backing, canary tail)");
				return (AE_OK);
			case SINF_TIGHT:
				ReturnBuffer->Pointer =
				    panasim_make_package(2, 2);
				ReturnBuffer->Length = sizeof(ACPI_OBJECT) +
				    2 * sizeof(ACPI_OBJECT);
				panasim_last_sinf_status = SINF_TIGHT;
				panasim_vlog("eval SINF -> Package Count=2 "
				    "(tight 2-elem allocation, nothing past it)");
				return (AE_OK);
			case SINF_TIGHT_SPRAY: {
				/*
				 * tight allocation with groomed neighbors in
				 * the SAME malloc zone: the tight package is
				 * 3 * sizeof(ACPI_OBJECT) = 72 bytes
				 * (ACPI_OBJECT external is 24 bytes); spray
				 * same-size chunks AFTER it so they occupy
				 * the slab space directly behind the package
				 * chunk -- reads past the package land in the
				 * marked 0xEE neighbor bodies.  Attributes
				 * the cross-chunk OOB read.
				 */
				void *s[6];
				size_t psz = 3 * sizeof(ACPI_OBJECT);
				int i;

				ReturnBuffer->Pointer =
				    panasim_make_package(2, 2);
				ReturnBuffer->Length = psz;
				for (i = 0; i < 6; i++) {
					s[i] = kmalloc(psz, M_PANASIM,
					    M_INTWAIT);
					memset(s[i], 0xEE, psz);
				}
				kfree(s[2], M_PANASIM);
				kfree(s[4], M_PANASIM);
				panasim_last_sinf_status = SINF_TIGHT_SPRAY;
				panasim_vlog("eval SINF -> Package Count=2 "
				    "(tight %zu-byte alloc + 6 same-zone "
				    "neighbors marked 0xEE, s[2]/s[4] freed)",
				    psz);
				/* remaining chunks are leaked intentionally
				 * (rig teardown is a reboot) */
				return (AE_OK);
			}
			default:
				ReturnBuffer->Pointer =
				    panasim_make_package(9, 9);
				ReturnBuffer->Length = sizeof(ACPI_OBJECT) +
				    9 * sizeof(ACPI_OBJECT);
				panasim_last_sinf_status = SINF_SANE;
				return (AE_OK);
			}
		}
	if (strcmp(path, "HINF") == 0) {
		if (panasim_hinf_mode == HINF_FAIL) {
			panasim_vlog("eval HINF -> AE_NO_MEMORY (buffer "
			    "untouched, Pointer stays NULL)");
			return (AE_NO_MEMORY);
		} else {
			ACPI_OBJECT *o = kmalloc(sizeof(ACPI_OBJECT),
			    M_PANASIM, M_INTWAIT | M_ZERO);
			o->Type = ACPI_TYPE_INTEGER;
			o->Integer.Value = (UINT64)panasim_hinf_val;
			ReturnBuffer->Pointer = o;
			ReturnBuffer->Length = sizeof(ACPI_OBJECT);
			return (AE_OK);
		}
	}
	if (strcmp(path, "SSET") == 0) {
		panasim_sset_calls++;
		if (ExternalParameters != NULL &&
		    ExternalParameters->Count == 2 &&
		    ExternalParameters->Pointer != NULL) {
			panasim_sset_last_index = (int)ExternalParameters->
			    Pointer[0].Integer.Value;
			panasim_sset_last_val = (int)ExternalParameters->
			    Pointer[1].Integer.Value;
		}
		return (AE_OK);
	}
	return (AE_OK);
}

/* the driver frees exactly what panasim_AcpiEvaluateObject fabricated */
void
panasim_AcpiOsFree(void *Memory)
{
	kfree(Memory, M_PANASIM);
}

/* mirrors real AcpiInstallNotifyHandler (just the registration) */
ACPI_STATUS
panasim_AcpiInstallNotifyHandler(ACPI_HANDLE Handle, UINT32 Type,
    ACPI_NOTIFY_HANDLER Handler, void *Context)
{
	panasim_notify_handler = Handler;
	panasim_notify_ctx = Context;
	panasim_notify_handle = Handle;
	panasim_vlog("install_notify_handler: handler=%p ctx=%p handle=%p",
	    Handler, Context, Handle);
	return (AE_OK);
}

ACPI_STATUS
panasim_AcpiRemoveNotifyHandler(ACPI_HANDLE Handle, UINT32 Type,
    ACPI_NOTIFY_HANDLER Handler)
{
	if (panasim_notify_handler == Handler) {
		panasim_notify_handler = NULL;
		panasim_notify_ctx = NULL;
	}
	return (AE_OK);
}

const char *
panasim_AcpiFormatException(ACPI_STATUS Status)
{
	return ("PANASIM-FABRICATED");
}

int
panasim_acpi_disabled(char *subsys)
{
	return (0);	/* pretend "panasonic" ACPI support is enabled */
}

/* interposed ACPI_ID_PROBE (kobj method call in the verbatim probe):
 * claim the MAT0019 HID match ONLY for the fake child this rig creates
 * (a real ACPI_ID_PROBE compares _HID against "MAT0019" and matches
 * nothing on non-Panasonic firmware; blanket-matching here would let
 * the driver hijack unrelated unit-0 ACPI children at boot). */
char *
panasim_id_probe(device_t bus, device_t dev, char **ids)
{
	if (panasim_child != NULL && dev == panasim_child)
		return (ids[0]);
	return (NULL);
}

void
panasim_acpi_UserNotify(const char *subsystem, ACPI_HANDLE h,
    uint8_t notify)
{
	panasim_notify_seen++;
	panasim_vlog("UserNotify(subsystem=%s val=0x%02x)", subsystem,
	    notify);
}

UINT32
panasim_acpi_event_sleep_button_sleep(void *context)
{
	panasim_vlog("sleep_button_sleep(context=%p) [interposed no-op]",
	    context);
	return (0);
}

int
panasim_power_profile_get_state(void)
{
	return (panasim_power_profile);
}

/* ------------------------------------------------------------------ */
/* fake device plumbing: real newbus child of the REAL acpi0           */
/* ------------------------------------------------------------------ */

static void
panasim_create(void)
{
	devclass_t dc;

	if (panasim_created)
		return;

	dc = devclass_find("acpi");
	if (dc == NULL) {
		panasim_vlog("create: no acpi devclass");
		return;
	}
	panasim_acpi0 = devclass_get_device(dc, 0);
	if (panasim_acpi0 == NULL) {
		panasim_vlog("create: no acpi0");
		return;
	}

	/* real acpi bus add_child: gives the child real ACPI ivars
	 * (pnpinfo at add time sees handle==NULL, like the acpi_hp rig) */
	panasim_child = BUS_ADD_CHILD(panasim_acpi0, panasim_acpi0, 0,
	    "acpi_panasonic", -1);
	if (panasim_child == NULL) {
		panasim_vlog("create: BUS_ADD_CHILD failed");
		return;
	}
	device_quiet(panasim_child);
	acpi_set_handle(panasim_child, panasim_handle_cookie);

	/* runs the REAL verbatim acpi_panasonic_probe + attach */
	if (device_probe_and_attach(panasim_child) != 0) {
		panasim_vlog("create: probe_and_attach FAILED");
		panasim_child = NULL;
		return;
	}
	panasim_created = 1;
	panasim_vlog("created: %s on %s (softc=%p) -- REAL verbatim "
	    "probe/attach ran; hw.acpi.panasonic.* sysctls live",
	    device_get_nameunit(panasim_child),
	    device_get_nameunit(panasim_acpi0),
	    device_get_softc(panasim_child));
}

static void
panasim_destroy(void)
{
	if (!panasim_created)
		return;
	panasim_vlog("destroy: device_delete_child -> real verbatim detach");
	device_delete_child(panasim_acpi0, panasim_child);
	panasim_child = NULL;
	panasim_created = 0;
}

/*
 * Notify injection: mirrors ACPICA deferred notification dispatch
 * (acpi_ev_notify_dispatch runs the handler in its own thread context,
 * no interpreter/ACPI lock held).
 */
static void
panasim_notify_thread(void *arg)
{
	ACPI_NOTIFY_HANDLER h = panasim_notify_handler;
	void *ctx = panasim_notify_ctx;
	ACPI_HANDLE hd = panasim_notify_handle;

	if (h == NULL) {
		panasim_vlog("inject: no notify handler registered");
		return;
	}
	panasim_vlog("inject: dispatching notify 0x%02x to captured "
	    "handler %p(ctx=%p) [ACPI_DEVICE_NOTIFY]", (UINT32)(uintptr_t)arg,
	    h, ctx);
	h(hd, (UINT32)(uintptr_t)arg, ctx);
	panasim_vlog("inject: handler returned");
}

/* ------------------------------------------------------------------ */
/* command plane: structural operations run in this worker kthread     */
/* (a sysctl handler must not create/destroy sysctls or newbus         */
/* devices reentrantly)                                                */
/* ------------------------------------------------------------------ */

static int		panasim_cmd;
static volatile int	panasim_cmdgen;
static int		panasim_cmd_done;

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

	for (;;) {
		tsleep(&panasim_cmdgen, 0, "panawkr", 0);
		if (panasim_cmdgen == lastgen)
			continue;
		lastgen = panasim_cmdgen;
		panasim_cmd_done = 0;
		panasim_vlog("worker: exec cmd=%d", panasim_cmd);
		switch (panasim_cmd) {
		case 1:
			panasim_create();
			break;
		case 2:
			panasim_destroy();
			break;
		case 3:		/* inject ACPI notify 0x80 */
			kthread_create(panasim_notify_thread,
			    (void *)(uintptr_t)0x80, NULL, "pananotify");
			break;
		case 9:
			/*
			 * DF-2591: call the REAL device_shutdown path
			 * (subr_bus.c:2154-2162 -> DEVICE_SHUTDOWN ->
			 * verbatim acpi_panasonic_shutdown) WITHOUT the
			 * serial -- exactly what MOD_SHUTDOWN does for
			 * every device at reboot (subr_bus.c:3549-3551
			 * -> device_shutdown(root_bus) ->
			 * bus_generic_shutdown).
			 */
			if (panasim_created) {
				panasim_vlog("worker: device_shutdown(%s) "
				    "-- verbatim "
				    "acpi_panasonic_shutdown begins",
				    device_get_nameunit(panasim_child));
				device_shutdown(panasim_child);
				panasim_vlog("worker: device_shutdown "
				    "returned (no assert fired)");
			}
			break;
		default:
			break;
		}
		panasim_vlog("worker: cmd=%d DONE", panasim_cmd);
		panasim_cmd_done = lastgen;
		wakeup(&panasim_cmd_done);
	}
}

static int
panasim_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 > 3 && v != 9))
		return (EINVAL);
	panasim_cmd = v;
	panasim_cmdgen++;
	wakeup(&panasim_cmdgen);
	return (0);
}

static int
panasim_sysctl_log(SYSCTL_HANDLER_ARGS)
{
	int len = strlen(panasim_log);

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

SYSCTL_NODE(_hw, OID_AUTO, panasim, CTLFLAG_RD, 0,
    "acpi_panasonic verbatim-interposition rig");

SYSCTL_PROC(_hw_panasim, OID_AUTO, cmd, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, panasim_sysctl_cmd, "I",
    "worker cmd: 1=create fake dev 2=destroy 3=inject notify 0x80 "
    "9=device_shutdown(dev)");
SYSCTL_PROC(_hw_panasim, OID_AUTO, log, CTLTYPE_STRING | CTLFLAG_RD,
    0, 0, panasim_sysctl_log, "A", "rig log");
SYSCTL_INT(_hw_panasim, OID_AUTO, sinf_mode, CTLFLAG_RW,
    &panasim_sinf_mode, 0,
    "SINF reply: 0=FAIL 1=Count2+canary 2=Count2 tight 3=tight+spray 4=sane9");
SYSCTL_INT(_hw_panasim, OID_AUTO, hinf_mode, CTLFLAG_RW,
    &panasim_hinf_mode, 0, "HINF reply: 0=FAIL 1=integer");
SYSCTL_INT(_hw_panasim, OID_AUTO, hinf_val, CTLFLAG_RW,
    &panasim_hinf_val, 0, "HINF integer value (0x81 = key1 press)");
SYSCTL_INT(_hw_panasim, OID_AUTO, power_profile, CTLFLAG_RW,
    &panasim_power_profile, 0, "power_profile_get_state() reply");
SYSCTL_INT(_hw_panasim, OID_AUTO, created, CTLFLAG_RD,
    &panasim_created, 0, "fake acpi_panasonic device attached");
SYSCTL_INT(_hw_panasim, OID_AUTO, cmd_done, CTLFLAG_RD,
    &panasim_cmd_done, 0,
    "generation of the last finished worker cmd");
SYSCTL_INT(_hw_panasim, OID_AUTO, sinf_calls, CTLFLAG_RD,
    &panasim_sinf_calls, 0, "SINF evaluations served");
SYSCTL_INT(_hw_panasim, OID_AUTO, sset_calls, CTLFLAG_RD,
    &panasim_sset_calls, 0, "SSET evaluations served");
SYSCTL_INT(_hw_panasim, OID_AUTO, sset_last_index, CTLFLAG_RD,
    &panasim_sset_last_index, 0, "last SSET index arg");
SYSCTL_INT(_hw_panasim, OID_AUTO, sset_last_val, CTLFLAG_RD,
    &panasim_sset_last_val, 0, "last SSET value arg");
SYSCTL_INT(_hw_panasim, OID_AUTO, last_sinf_status, CTLFLAG_RD,
    &panasim_last_sinf_status, 0, "mode of the last SINF reply");
SYSCTL_INT(_hw_panasim, OID_AUTO, notify_seen, CTLFLAG_RD,
    &panasim_notify_seen, 0, "acpi_UserNotify calls reached");

static void
panasim_boot(void *arg)
{
	lockinit(&panasim_log_lock, "panalog", 0, 0);
	kthread_create(panasim_worker, NULL, NULL, "panawork");
	kprintf(PANASIM "harness up: sizeof(ACPI_OBJECT)=%zu "
	    "9-element package alloc=%zu\n", sizeof(ACPI_OBJECT),
	    sizeof(ACPI_OBJECT) + 9 * sizeof(ACPI_OBJECT));
	panasim_vlog("harness up (interposed externs active; verbatim "
	    "driver compiled in via pana_shim.c)");
}

SYSINIT(panasim, SI_SUB_CONFIGURE, SI_ORDER_MIDDLE, panasim_boot, NULL);
