/*-
 * vpd_harness.c -- driver for the verbatim-extracted VPD parser.
 *
 * Modes (argv[1]):
 *   t1  DF-5276: zero-length VPD identifier string -> kmalloc(1) then an
 *                unbounded sequential device-controlled write (guard page
 *                catches it at a precisely known offset).
 *   t2  DF-5277 + DF-5279: drive the real parser into the error cleanup
 *                after a COMPLETED VPD-R section and print the resulting
 *                cfg.vpd state (vpd_ros==NULL, vpd_rocnt stale != 0) that
 *                crashes pci_freecfg()/pci_get_vpd_readonly_method();
 *                then show the readonly lookup always fails (missing break).
 *   t3  DF-5278: VPD-R item whose length is smaller than its keyword header
 *                -> `remain` goes negative -> parse can never terminate;
 *                allocations grow without bound until the simulated device
 *                is capped (a real malicious device never stops).
 *   t2a DF-5278(a): oversize `remain` -> "invalid VPD data" is printed but
 *                the parser CONTINUES (the state=-1 is overwritten).
 *   t4  baseline: a well-formed VPD parses correctly (harness fidelity).
 */
#include "vpd_shim.h"
#include "vpd_structs.h"	/* VERBATIM pcivar.h:58-78 VPD structures */

/* map the kernel config struct onto a subset (parser touches only these) */
struct shim_pcicfg {
	uint32_t	domain;
	uint8_t		bus;
	uint8_t		slot;
	uint8_t		func;
	struct pcicfg_vpd vpd;
};
#define pcicfgregs	struct shim_pcicfg
struct shim_devinfo {
	struct shim_pcicfg cfg;
};
#define pci_devinfo	shim_devinfo
#define device_get_ivars(x)	((struct shim_devinfo *)(void *)(x))
#define device_get_parent(x)	((device_t)(x))
extern int	shim_bootverbose;
#define bootverbose	shim_bootverbose

#include "vpd_extracted.h"

int shim_bootverbose = 1;
uint8_t	sim_stream[1 << 20];
size_t	sim_len;
size_t	sim_repeat_off, sim_repeat_len;
int	sim_endless;
size_t	sim_serve_cap = (size_t)-1;
size_t	sim_reads, sim_addr_writes;

size_t	sh_nalloc, sh_nrealloc, sh_bytes, sh_maxblk;
const char *sh_phase = "init";

static struct sh_blk *sh_blocks;
static size_t sh_pagesz;

static uint8_t sim_byte(size_t off)
{
	if (off < sim_len)
		return (sim_stream[off]);
	if (sim_endless && sim_repeat_len > 0)
		return (sim_stream[sim_repeat_off +
		    (off - sim_len) % sim_repeat_len]);
	return (0xff);
}

static int sim_off;		/* latched VPD offset */
static int sim_stopped;		/* device refuses to complete */

void
sim_reset(void)
{
	sim_len = sim_repeat_off = sim_repeat_len = 0;
	sim_endless = 0;
	sim_serve_cap = (size_t)-1;
	sim_reads = sim_addr_writes = 0;
	sim_off = 0;
	sim_stopped = 0;
}

uint32_t
sim_read_config(int reg, int width)
{
	sim_reads++;
	if (reg == SIM_VPDBASE + SIM_PCIR_VPD_DATA) {
		uint32_t v = 0;
		int i, sh = 0;

		for (i = 0; i < 4; i++) {
			v |= (uint32_t)sim_byte((size_t)sim_off + i) << sh;
			sh += 8;
		}
		return (v);
	}
	if (reg == SIM_VPDBASE + SIM_PCIR_VPD_ADDR)
		return (sim_stopped ? (uint32_t)(sim_off & 0x7fff) :
		    (uint32_t)(sim_off | 0x8000));
	return (0xffffffff);
}

void
sim_write_config(int reg, uint32_t val, int width)
{
	if (reg == SIM_VPDBASE + SIM_PCIR_VPD_ADDR) {
		sim_addr_writes++;
		sim_off = val & 0x7fff;
		if ((size_t)sim_off >= sim_serve_cap ||
		    (!sim_endless && (size_t)sim_off >= sim_len &&
		    sim_repeat_len == 0))
			sim_stopped = 1;	/* cap reached / stream over */
		else
			sim_stopped = 0;	/* completes instantly */
	}
}

/* ---------------- allocator ---------------- */

void *
sh_kmalloc(size_t sz, int flags)
{
	struct sh_blk *b;

	if (sh_pagesz == 0)
		sh_pagesz = (size_t)sysconf(_SC_PAGESIZE);
	b = calloc(1, sizeof(*b));
	b->sz = sz;
	b->mapsz = ((sz + sh_pagesz - 1) / sh_pagesz + 1) * sh_pagesz;
	b->base = mmap(NULL, b->mapsz, PROT_READ | PROT_WRITE,
	    MAP_PRIVATE | MAP_ANON, -1, 0);
	if (b->base == MAP_FAILED) {
		printf("shim: mmap failed\n");
		exit(70);
	}
	if (mprotect((char *)b->base + b->mapsz - sh_pagesz, sh_pagesz,
	    PROT_NONE) != 0) {
		printf("shim: mprotect failed\n");
		exit(70);
	}
	b->p = b->base;
	if (flags & M_ZERO)
		memset(b->p, 0, sz);
	b->next = sh_blocks;
	sh_blocks = b;
	sh_nalloc++;
	sh_bytes += sz;
	if (sz > sh_maxblk)
		sh_maxblk = sz;
	return (b->p);
}

void *
sh_krealloc(void *p, size_t sz)
{
	struct sh_blk *b;
	void *np;

	np = sh_kmalloc(sz, M_ZERO);
	if (p != NULL) {
		for (b = sh_blocks; b != NULL; b = b->next) {
			if (b->p == p) {
				memcpy(np, p, b->sz < sz ? b->sz : sz);
				sh_kfree(p);
				break;
			}
		}
	}
	sh_nrealloc++;
	/* keep accounting honest: the array growth is what matters */
	return (np);
}

void
sh_kfree(void *p)
{
	struct sh_blk **pp, *b;

	if (p == NULL)
		return;
	for (pp = &sh_blocks; (b = *pp) != NULL; pp = &b->next) {
		if (b->p == p) {
			*pp = b->next;
			munmap(b->base, b->mapsz);
			free(b);
			return;
		}
	}
}
struct sh_blk *
sh_find_block(const void *addr)
{
	struct sh_blk *b;

	for (b = sh_blocks; b != NULL; b = b->next) {
		if ((const char *)addr >= (const char *)b->base &&
		    (const char *)addr < (const char *)b->base + b->mapsz)
			return (b);
	}
	return (NULL);
}

/* ---------------- SIGSEGV reporting ---------------- */

static void
sh_segv(int sig, siginfo_t *si, void *uc)
{
	struct sh_blk *b;

	fflush(stdout);
	fprintf(stderr, "\n*** SIGSEGV writing %p during phase '%s' ***\n",
	    si->si_addr, sh_phase);
	b = sh_find_block(si->si_addr);
	if (b != NULL)
		fprintf(stderr, "    faulting allocation: base=%p kmalloc size=%zu "
		    "(guard page at +%zu)\n"
		    "    => %zd bytes were written past a %zu-byte "
		    "kmalloc allocation\n",
		    b->base, b->sz, b->mapsz - sh_pagesz,
		    (char *)si->si_addr - (char *)b->base, b->sz);
	else
		fprintf(stderr, "    (fault outside any tracked allocation)\n");
	fflush(stderr);
	_exit(77);
}

/* ---------------- tests ---------------- */

static struct shim_pcicfg cfg;
static struct shim_dev pcib_dev, child_dev;

static void
fresh_cfg(void)
{
	memset(&cfg, 0, sizeof(cfg));
	cfg.domain = 0;
	cfg.bus = 2;
	cfg.slot = 3;
	cfg.func = 1;
	cfg.vpd.vpd_reg = SIM_VPDBASE;
	cfg.vpd.vpd_cached = 0;
	sim_reset();
}

static uint8_t
pat(int i)
{
	return ((uint8_t)((i * 31) + 7));
}

static void
t1(void)
{
	size_t n = 0, i;

	sh_phase = "t1: zero-length VPD identifier string (DF-5276)";
	fresh_cfg();
	sim_stream[n++] = 0x82;		/* long-form item, name 0x2 String */
	sim_stream[n++] = 0x00;		/* length low  == 0 */
	sim_stream[n++] = 0x00;		/* length high == 0 */
	for (i = 0; i < 8192; i++)	/* device-controlled payload */
		sim_stream[n++] = pat(i);
	sim_len = n;

	printf("t1: launching verbatim pci_read_vpd(): String item length 0\n"
	    "t1: parser does kmalloc(remain+1) == kmalloc(1), then state 1\n"
	    "t1: writes one payload byte per VPD byte with no bound...\n");
	fflush(stdout);

	pci_read_vpd(&pcib_dev, &cfg);

	/* we only get here if the write stayed in bounds */
	if (cfg.vpd.vpd_ident != NULL) {
		int e = 0;
		while (e < 8192 && (uint8_t)cfg.vpd.vpd_ident[e] == pat(e))
			e++;
		printf("t1: returned; %d contiguous payload bytes at "
		    "vpd_ident (allocation was 1 byte)\n", e);
	} else {
		printf("t1: returned; vpd_ident==NULL (parser rejected the "
		    "zero-length item -- fixed kernel)\n");
	}
}

static void
t2(void)
{
	size_t n = 0;
	const char *v;
	int r;
	struct shim_devinfo di2;
	struct shim_pcicfg cfg2;

	sh_phase = "t2: error cleanup after a completed section (DF-5277)";
	fresh_cfg();
	/* section A: VPD-R, len 4, keyword "XY" len 1 value 'Z'
	 *   -> completes: vpd_rocnt = 1, array shrunk to 1 entry */
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x04; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'X'; sim_stream[n++] = 'Y'; sim_stream[n++] = 0x01;
	sim_stream[n++] = 'Z';
	/* section B: VPD-R again, keyword "RV" with length 0
	 *   -> "bad keyword length", cksumvalid=0, state=-1 -> cleanup
	 *      frees vpd_ros and NULLs it, but vpd_rocnt stays 1 */
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x03; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'R'; sim_stream[n++] = 'V'; sim_stream[n++] = 0x00;
	sim_len = n;

	printf("t2: stage1 parse...\n"); fflush(stdout);
	pci_read_vpd(&pcib_dev, &cfg);
	printf("t2: stage1 done\n"); fflush(stdout);

	printf("t2: post-parse cfg.vpd state:\n"
	    "t2:   vpd_ros  = %p   (freed and NULLed by cleanup)\n"
	    "t2:   vpd_rocnt= %d   (STALE -- cleanup does not reset it)\n"
	    "t2:   vpd_ident= %p\n"
	    "t2:   vpd_w    = %p, vpd_wcnt = %d\n",
	    cfg.vpd.vpd_ros, cfg.vpd.vpd_rocnt, cfg.vpd.vpd_ident,
	    cfg.vpd.vpd_w, cfg.vpd.vpd_wcnt);
	if (cfg.vpd.vpd_ros == NULL && cfg.vpd.vpd_rocnt != 0)
		printf("t2: CRASH PRECONDITION REPRODUCED: pci_freecfg() will\n"
		    "t2: dereference vpd_ros[%d].value == *(NULL+8), and\n"
		    "t2: kfree(vpd_ros=NULL) panics on DragonFly.\n",
		    cfg.vpd.vpd_rocnt);

	/* DF-5279: the readonly lookup misses its break (valid table) */
	sh_phase = "t2: readonly keyword lookup, valid table (DF-5279)";
	memset(&cfg2, 0, sizeof(cfg2));
	cfg2.vpd.vpd_reg = SIM_VPDBASE;
	cfg2.bus = 4; cfg2.slot = 5; cfg2.func = 6;
	sim_reset();
	n = 0;
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x04; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'N'; sim_stream[n++] = 'A'; sim_stream[n++] = 0x01;
	sim_stream[n++] = 'X';
	sim_stream[n++] = 0x78;		/* End item: clean exit, table stays */
	sim_len = n;
	printf("t2: stage2a parse (valid NA keyword + End)...\n"); fflush(stdout);
	pci_read_vpd(&pcib_dev, &cfg2);
	printf("t2: stage2a done\n"); fflush(stdout);
	memset(&di2, 0, sizeof(di2));
	di2.cfg = cfg2;
	v = (const char *)(void *)-1;
	r = pci_get_vpd_readonly_method(&child_dev, (device_t)&di2, "NA", &v);
	printf("t2: vpd_ros[0] = {\"%c%c\", \"%s\"}; lookup(\"NA\") -> "
	    "ret=%d vptr=%p\n", cfg2.vpd.vpd_ros[0].keyword[0],
	    cfg2.vpd.vpd_ros[0].keyword[1], cfg2.vpd.vpd_ros[0].value, r, v);
	printf("t2: %s\n", (r == ENXIO && v == NULL) ?
	    "BUG DF-5279: keyword exists but ENXIO/NULL returned (missing break)"
	    : "lookup OK");

	/* DF-5277 (readonly flavor): completed section, then I/O error ->
	 * cleanup NULLs vpd_ros but leaves vpd_rocnt stale -> the REAL
	 * verbatim lookup dereferences vpd_ros[0].keyword at NULL. */
	sh_phase = "t2: readonly lookup after error cleanup (DF-5277)";
	memset(&cfg2, 0, sizeof(cfg2));
	cfg2.vpd.vpd_reg = SIM_VPDBASE;
	cfg2.bus = 4; cfg2.slot = 5; cfg2.func = 6;
	sim_reset();
	n = 0;
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x04; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'N'; sim_stream[n++] = 'A'; sim_stream[n++] = 0x01;
	sim_stream[n++] = 'X';
	sim_len = n;			/* no End: next read stalls -> ENXIO */
	printf("t2: stage2b parse (section completes, then device stops)..."
	    "\n"); fflush(stdout);
	pci_read_vpd(&pcib_dev, &cfg2);
	printf("t2: stage2b: vpd_ros=%p vpd_rocnt=%d -- now calling the "
	    "verbatim lookup on this state:\n", cfg2.vpd.vpd_ros,
	    cfg2.vpd.vpd_rocnt);
	fflush(stdout);
	memset(&di2, 0, sizeof(di2));
	di2.cfg = cfg2;
	v = (const char *)(void *)-1;
	r = pci_get_vpd_readonly_method(&child_dev, (device_t)&di2, "NA", &v);
	printf("t2: BUG: lookup returned %d vptr=%p without crashing\n", r, v);
}

static void
t2a(void)
{
	size_t n = 0, i;

	sh_phase = "t2a: oversize remain (DF-5278a dead check)";
	fresh_cfg();
	sim_stream[n++] = 0x82;		/* String */
	sim_stream[n++] = 0x00;		/* length 0x0500 = 1280 */
	sim_stream[n++] = 0x05;
	for (i = 0; i < 1280; i++)
		sim_stream[n++] = (uint8_t)('a' + (i % 26));
	sim_stream[n++] = 0x78;		/* End item */
	sim_len = n;

	pci_read_vpd(&pcib_dev, &cfg);
	printf("t2a: remain=0x500 exceeded the 0x7f*4 bound; the kernel\n"
	    "t2a: printed \"invalid VPD data\" but then CONTINUED:\n");
	if (cfg.vpd.vpd_ident != NULL)
		printf("t2a:   vpd_ident len = %zu (parse did not abort -- "
		    "state=-1 was overwritten)\n", strlen(cfg.vpd.vpd_ident));
	else
		printf("t2a:   vpd_ident NULL (parse aborted -- fixed kernel)\n");
}

static void
t3(void)
{
	size_t n = 0;
	size_t served;
	size_t before_bytes = sh_bytes, before_alloc = sh_nalloc;

	sh_phase = "t3: unterminatable VPD-R section (DF-5278)";
	fresh_cfg();
	/* VPD-R item with length 1: the 3-byte keyword header alone drives
	 * `remain` to -2; it can never again reach 0, so states 2<->3 cycle
	 * for as long as the device serves data. */
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x01; sim_stream[n++] = 0x00;
	sim_len = n;
	sim_repeat_off = n;
	sim_repeat_len = 4;
	sim_stream[n++] = 'K'; sim_stream[n++] = 'W';
	sim_stream[n++] = 0x01; sim_stream[n++] = 'X';
	sim_endless = 1;
	sim_serve_cap = 20000;	/* the ONLY thing that stops us */

	printf("t3: malicious device serves an endless keyword stream;\n"
	    "t3: capping it at %zu bytes (guest VA limits the demo; a real device never stops).\n",
	    sim_serve_cap);
	fflush(stdout);

	pci_read_vpd(&pcib_dev, &cfg);

	served = sim_off;
	printf("t3: parser stopped ONLY because the simulator capped the\n"
	    "t3: device at %zu bytes (vrs.off=%zu, config reads=%zu).\n",
	    sim_serve_cap, served, sim_reads);
	printf("t3: allocations during the capped run: %zu kmalloc/krealloc,"
	    " %zu bytes, largest %zu bytes\n",
	    sh_nalloc - before_alloc, sh_bytes - before_bytes, sh_maxblk);
	printf("t3: state machine had not terminated: every keyword kept\n"
	    "t3: consuming bytes with remain<0; with an uncapped device the\n"
	    "t3: loop and the allocations grow without bound.\n");
}

static void
t4(void)
{
	size_t n = 0;

	sh_phase = "t4: well-formed VPD (baseline)";
	fresh_cfg();
	sim_stream[n++] = 0x82; sim_stream[n++] = 0x04; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'T'; sim_stream[n++] = 'E'; sim_stream[n++] = 'S';
	sim_stream[n++] = 'T';
	sim_stream[n++] = 0x90; sim_stream[n++] = 0x04; sim_stream[n++] = 0x00;
	sim_stream[n++] = 'N'; sim_stream[n++] = 'A'; sim_stream[n++] = 0x01;
	sim_stream[n++] = 'X';
	sim_stream[n++] = 0x78;		/* End */
	sim_len = n;

	pci_read_vpd(&pcib_dev, &cfg);
	printf("t4: ident=\"%s\" rocnt=%d ros[0]=\"%c%c\"->\"%s\" "
	    "(parse OK, harness faithful)\n",
	    cfg.vpd.vpd_ident ? cfg.vpd.vpd_ident : "(null)",
	    cfg.vpd.vpd_rocnt,
	    cfg.vpd.vpd_ros ? cfg.vpd.vpd_ros[0].keyword[0] : '?',
	    cfg.vpd.vpd_ros ? cfg.vpd.vpd_ros[0].keyword[1] : '?',
	    cfg.vpd.vpd_ros ? cfg.vpd.vpd_ros[0].value : "(null)");
}

int
main(int argc, char **argv)
{
	struct sigaction sa;

	sa.sa_sigaction = sh_segv;
	sa.sa_flags = SA_SIGINFO;
	sigemptyset(&sa.sa_mask);
	sigaction(SIGSEGV, &sa, NULL);
	sigaction(SIGBUS, &sa, NULL);

	if (argc < 2) {
		fprintf(stderr, "usage: %s t1|t2|t2a|t3|t4\n", argv[0]);
		return (2);
	}
	if (strcmp(argv[1], "t1") == 0)
		t1();
	else if (strcmp(argv[1], "t2") == 0)
		t2();
	else if (strcmp(argv[1], "t2a") == 0)
		t2a();
	else if (strcmp(argv[1], "t3") == 0)
		t3();
	else if (strcmp(argv[1], "t4") == 0)
		t4();
	else {
		fprintf(stderr, "unknown mode %s\n", argv[1]);
		return (2);
	}
	return (0);
}
