/*
 * gpo_detach.c -- DF-5518 race trigger: detach (and delete) the live
 * gpio_intel/gpio_acpi device pair while AML is concurrently executing
 * GeneralPurposeIo field accesses (TZ0._TMP hammer, driven by the acpi_tz
 * poll loop: each _TMP performs @ITER@ 59-pin GPIO field reads through
 * gpio_acpi_space_handler).
 *
 * device_detach(gpio_intel0)
 *   -> bus_generic_detach -> gpio_acpi_detach (gpio_acpi.c:486-497)
 *        -> gpio_acpi_unmap_aei()                 [AEI half: known DF-1094]
 *        -> AcpiRemoveAddressSpaceHandler()       [does NOT wait for the
 *             in-flight gpio_acpi_space_handler dispatch; ACPICA runs the
 *             handler with the interpreter EXITED (evregion.c:392-401), so
 *             the removal proceeds concurrently and frees the HandlerObj
 *             -- including the ContextMutex the running handler holds]
 *   -> parent teardown: bus_teardown_intr, mem_res released, lockuninit
 * device_delete_children + device_delete_child
 *   -> gpio_acpi_softc (with sc->space_handler_data == the space handler's
 *      HandlerContext) and gpio_intel_softc are FREED while the handler may
 *      still be running -> UAF.
 *
 * kfree'd-but-unreused memory still looks intact, so a naive first-touch
 * UAF is silent.  To make the use deterministic we immediately reclaim the
 * freed zones with poisoned allocations: any in-flight handler deref
 * (GPIO_READ_PIN -> device_get_softc -> sc->ranges / sc->lk) then touches
 * 0xDE.. garbage -> fatal trap.
 *
 * Load with `kldload ./gpo_detach.ko`.
 */
#include <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/bus.h>
#include <sys/param.h>
#include <vm/vm_page.h>

static int
gpo_detach_modevent(module_t mod, int type, void *arg)
{
	devclass_t dc;
	device_t dev, parent;
	void *p[256];
	volatile uint32_t *dbg1 = (volatile uint32_t *)PHYS_TO_DMAP(0xD6004);
	uint32_t d1a, d1b, d1c;
	int error, i, j, n;
	static const int sizes[] = { 128, 256, 512, 1024, 2048 };

	switch (type) {
	case MOD_LOAD:
		dc = devclass_find("gpio_intel");
		if (dc == NULL) {
			kprintf("gpo_detach: no gpio_intel devclass\n");
			return (ENXIO);
		}
		dev = devclass_get_device(dc, 0);
		if (dev == NULL) {
			kprintf("gpo_detach: no gpio_intel0 device\n");
			return (ENXIO);
		}
		kprintf("gpo_detach: racing %s...\n",
		    device_get_nameunit(dev));
		d1a = *dbg1;
		error = device_detach(dev);
		d1b = *dbg1;
		if (error == 0) {
			/*
			 * NO console output between here and the poison
			 * spray: serial kprintf costs ~100us/line and eats
			 * the entire use-after-free window.
			 */
			device_delete_children(dev);
			parent = device_get_parent(dev);
			device_delete_child(parent, dev);

			/*
			 * Reclaim the just-freed zones (softcs ~128/2048,
			 * device_t ~256) with poisoned content so an
			 * in-flight gpio_acpi_space_handler faults
			 * deterministically instead of reading stale bytes.
			 */
			n = 0;
			for (i = 0; i < (int)NELEM(sizes); i++) {
				for (j = 0; j < 32 && n < 256; j++, n++) {
					p[n] = kmalloc(sizes[i], M_DEVBUF,
					    M_WAITOK);
					if (p[n] != NULL)
						memset(p[n], 0xDE, sizes[i]);
				}
			}
			d1c = *dbg1;
			/* deliberately leak p[]; we want the window covered */
			kprintf("gpo_detach: done, sprayed %d poison allocs; "
			    "hammer dbg1 pre=%u post-detach=%u post-spray=%u\n",
			    n, d1a, d1b, d1c);
		}
		break;
	case MOD_UNLOAD:
		break;
	default:
		return (EOPNOTSUPP);
	}
	return (0);
}

static moduledata_t gpo_detach_mod = {
	"gpo_detach", gpo_detach_modevent, NULL
};
DECLARE_MODULE(gpo_detach, gpo_detach_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
MODULE_VERSION(gpo_detach, 1);
