⬢ DragonFlyBSD Kernel Audit
DF-5507 / amdh_harness.c
← back to finding ↓ download raw
/*
 * amdh_harness.c — in-kernel evidence harness for the pass-2 amdsmb audit
 * (DF-5506: dead poll-loop timeouts / swallowed EC errors; DF-5507:
 * uninitialized caller count from /dev/smb SMB_BREAD).
 *
 * METHOD: this KLD compiles the REAL, UNMODIFIED production source
 * (amdsmb.c, copied verbatim from /usr/src/sys/bus/smbus/amdsmb/amdsmb.c;
 * build.sh asserts byte-identity) and drives its static transaction engine
 * against a stateful emulation of the AMD-8111 ACPI-EC / SMBus 2.0
 * register interface.  bus_read_1()/bus_write_1() are #undef'd and
 * re-#defined to emulator functions BEFORE the #include; amdsmb.c's own
 * AMDSMB_ECINB/ECOUTB macros expand at their use sites, so every EC port
 * access executed by the production code is dispatched into this emulator.
 * The emulated register bank is 512 bytes wide, matching the AMD-8111
 * register file semantics (bank 0x00..0x27 defined; >= 0x28 is adjacent
 * register space — the region DF-1091's slave-driven count reads OOB).
 *
 * The AMD-8111 SMBus 2.0 controller has no QEMU machine model, so this is
 * the closest executable evidence class to real hardware: production code
 * text, in-kernel, against a datasheet-faithful register-level emulator.
 */
#include <sys/param.h>
#include <sys/bus.h>
#include <sys/globaldata.h>
#include <sys/kernel.h>
#include <sys/lock.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/rman.h>
#include <sys/time.h>
#include <sys/bus_private.h>	/* struct bsd_device, for the fake device_t */

/* ------------------------------------------------------------------ */
/* Emulator state (constants come from amdsmb.c itself, below)          */
/* ------------------------------------------------------------------ */

#define	AMDH_FAST		0	/* txn completes after 1 busy poll */
#define	AMDH_WEDGE_PRTCL	1	/* SMB_PRTCL never returns to 0 */
#define	AMDH_WEDGE_IBF		2	/* EC_SC.IBF never clears */
#define	AMDH_WEDGE_OBF		3	/* EC_SC.OBF never sets */

struct amdh_ec_state {
	uint8_t		smb_shadow[512]; /* current register bank image */
	int		ibf, obf;
	int		expect;		/* 0 none, 1 rd-addr, 2 wr-addr, 3 wr-data */
	int		target;
	uint8_t		out;
	int		mode;
	int		fake_len;	/* BCNT presented when a txn completes */
	uint8_t		sts_result;	/* STS presented when a txn completes */
	int		busy;
	int		busy_polls;
	long		n_sc_reads;
	long		n_reg_reads[512];
	uint8_t		bc_written;	/* value last written to SMB_BCNT */
};

static struct amdh_ec_state	E;
static struct resource		fake_res;
static struct bsd_device	fake_dev;

static uint8_t amdh_smb_reg_read(int);
static void amdh_smb_reg_write(int, uint8_t);
static uint8_t amdh_bus_read_1(struct resource *, bus_size_t);
static void amdh_bus_write_1(struct resource *, bus_size_t, uint8_t);

/* ------------------------------------------------------------------ */
/* Redirect the production accessors into the emulator, THEN compile    */
/* the real production source into this translation unit.               */
/* ------------------------------------------------------------------ */

#undef	bus_read_1
#undef	bus_write_1
#define	bus_read_1(r, o)		amdh_bus_read_1((r), (o))
#define	bus_write_1(r, o, v)		amdh_bus_write_1((r), (o), (v))

#include "amdsmb.c"		/* THE REAL PRODUCTION SOURCE (unmodified) */

/* ------------------------------------------------------------------ */
/* AMD-8111 EC + SMBus register-file emulation (definitions)           */
/* ------------------------------------------------------------------ */

static struct amdsmb_softc	fake_sc;
static char			amdh_name[] = "amdh0";

static uint8_t
amdh_smb_reg_read(int idx)
{
	KKASSERT(idx >= 0 && idx < 512);
	E.n_reg_reads[idx]++;
	if (idx == SMB_PRTCL) {
		if (E.busy) {
			E.busy_polls++;
			if (E.mode == AMDH_WEDGE_PRTCL)
				return (E.smb_shadow[SMB_PRTCL]);
			if (E.busy_polls >= 2) {	/* 1 busy poll, then done */
				int i;
				E.busy = 0;
				E.smb_shadow[SMB_PRTCL] = 0;
				E.smb_shadow[SMB_STS] = E.sts_result;
				E.smb_shadow[SMB_BCNT] = E.fake_len;
				for (i = 0; i < E.fake_len && i < 32; i++)
					E.smb_shadow[SMB_DATA + i] = 0xB0 + i;
			}
		}
		return (E.smb_shadow[SMB_PRTCL]);
	}
	return (E.smb_shadow[idx]);
}

static void
amdh_smb_reg_write(int idx, uint8_t v)
{
	E.smb_shadow[idx] = v;
	if (idx == SMB_BCNT)
		E.bc_written = v;
	if (idx == SMB_PRTCL && v != 0) {
		E.busy = 1;
		E.busy_polls = 0;
	}
}

static uint8_t
amdh_bus_read_1(struct resource *r, bus_size_t o)
{
	uint8_t v;

	if (r != &fake_res) {
		kprintf("amdh: [!] bus_read_1 on FOREIGN resource\n");
		return (0xFF);
	}
	if (o == EC_SC) {
		E.n_sc_reads++;
		v = (E.ibf ? EC_SC_IBF : 0) | (E.obf ? EC_SC_OBF : 0);
		/* model: the EC consumes input by the next poll */
		if (E.mode != AMDH_WEDGE_IBF)
			E.ibf = 0;
		if (E.mode == AMDH_WEDGE_OBF)
			v &= (uint8_t)~EC_SC_OBF;	/* OBF never observed */
		return (v);
	}
	if (o == EC_DATA) {
		E.obf = 0;
		return (E.out);
	}
	kprintf("amdh: [!] unexpected port READ 0x%jx\n", (uintmax_t)o);
	return (0xFF);
}

static void
amdh_bus_write_1(struct resource *r, bus_size_t o, uint8_t v)
{
	if (r != &fake_res) {
		kprintf("amdh: [!] bus_write_1 on FOREIGN resource\n");
		return;
	}
	if (o == EC_CMD) {
		E.ibf = 1;
		E.expect = (v == EC_CMD_RD) ? 1 : (v == EC_CMD_WR) ? 2 : 0;
		return;
	}
	if (o == EC_DATA) {
		E.ibf = 1;
		switch (E.expect) {
		case 1:			/* register address, host wants a read */
			/*
			 * A wedged output path never presents the data:
			 * OBF stays clear and EC_DATA keeps the PREVIOUS
			 * output byte (stale).
			 */
			if (E.mode != AMDH_WEDGE_OBF) {
				E.out = amdh_smb_reg_read(v);
				E.obf = 1;
			}
			E.expect = 0;
			break;
		case 2:
			E.target = v;
			E.expect = 3;
			break;
		case 3:
			amdh_smb_reg_write(E.target, v);
			E.expect = 0;
			break;
		default:
			break;
		}
		return;
	}
	kprintf("amdh: [!] unexpected port WRITE 0x%jx <- 0x%02x\n",
	    (uintmax_t)o, v);
}

/* ------------------------------------------------------------------ */
/* Tests                                                               */
/* ------------------------------------------------------------------ */

static void
amdh_reset(int mode, int fake_len, uint8_t sts)
{
	int i;

	memset(&E, 0, sizeof(E));
	E.mode = mode;
	E.fake_len = fake_len;
	E.sts_result = sts;
	/* adjacent register space content (what OOB register reads return) */
	for (i = 0x28; i < 512; i++)
		E.smb_shadow[i] = 0xE0 | (i & 0x1F);
}

static int
amdh_count_pattern(char *buf, int n, uint8_t pat)
{
	int i, c = 0;
	for (i = 0; i < n; i++)
		if ((uint8_t)buf[i] == pat)
			c++;
	return (c);
}

static void
amdh_run_tests(void)
{
	struct timeval t1, t2;
	long ms, scr0, scr1;
	u_char count, val;
	char buf[64];
	int rc, i, g, fail = 0;
	static const int gs[] = {0, 1, 5, 31, 32, 33, 255};

	kprintf("AMDH: BEGIN amdsmb production-code harness\n");

	/* --- T0: sanity, healthy controller happy path --- */
	amdh_reset(AMDH_FAST, 32, SMB_STS_OK);
	count = 32;
	memset(buf, 0xAA, sizeof(buf));
	rc = amdsmb_bread(&fake_dev, 0x5A, 0x42, &count, buf);
	kprintf("AMDH T0 sanity: rc=%d count=%d buf[0..3]=%02x %02x %02x %02x"
	    " (expect rc=%d count=32 data=B0 B1 B2 B3)\n",
	    rc, count, (uint8_t)buf[0], (uint8_t)buf[1], (uint8_t)buf[2],
	    (uint8_t)buf[3], SMB_ENOERR);
	if (rc != SMB_ENOERR || count != 32 || (uint8_t)buf[0] != 0xB0 ||
	    (uint8_t)buf[31] != 0xCF || amdh_count_pattern(buf, 32, 0xAA)) {
		kprintf("AMDH T0: FAIL (emulator/production mismatch)\n");
		fail++;
	} else
		kprintf("AMDH T0: PASS\n");

	/* --- T1: wedged SMB_PRTCL (never completes) --- */
	/* leave stale DATA/BCNT from the T0 transaction, then wedge */
	E.mode = AMDH_WEDGE_PRTCL;
	E.fake_len = 9;		/* what a NEW txn would deliver (never runs) */
	count = 32;
	memset(buf, 0xAA, sizeof(buf));
	microuptime(&t1);
	rc = amdsmb_bread(&fake_dev, 0x5A, 0x42, &count, buf);
	microuptime(&t2);
	ms = (t2.tv_sec - t1.tv_sec) * 1000 + (t2.tv_usec - t1.tv_usec) / 1000;
	kprintf("AMDH T1 wedge_prtcl: rc=%d (BUG: expect %d=ENOERR, correct "
	    "would be %d=ETIMEOUT) count=%d elapsed=%ldms busy_polls=%d\n",
	    rc, SMB_ENOERR, SMB_ETIMEOUT, count, ms, E.busy_polls);
	kprintf("AMDH T1 data: buf[0..3]=%02x %02x %02x %02x (STALE T0 bytes "
	    "if 0xB0..) stale_0xAA=%d\n", (uint8_t)buf[0], (uint8_t)buf[1],
	    (uint8_t)buf[2], (uint8_t)buf[3], amdh_count_pattern(buf, 64, 0xAA));
	if (rc == SMB_ENOERR && ms >= 4900)
		kprintf("AMDH T1: BUG REPRODUCED (wedged controller reported "
		    "SUCCESS after %ldms busy spin; SMB_ETIMEOUT is dead code)\n",
		    ms);
	else {
		kprintf("AMDH T1: NOT reproduced\n");
		fail++;
	}

	/* --- T2: wedged EC input path (IBF never clears) --- */
	amdh_reset(AMDH_WEDGE_IBF, 32, SMB_STS_OK);
	scr0 = E.n_sc_reads;
	val = 0x77;
	rc = amdsmb_ec_read(&fake_sc, SMB_STS, &val);
	scr1 = E.n_sc_reads;
	kprintf("AMDH T2 wedge_ibf: ec_read rc=%d (BUG: expect 0; correct "
	    "would be 1) val=0x%02x sc_reads=%ld (500-poll budget exhausted, "
	    "write proceeded anyway)\n", rc, val, scr1 - scr0);
	if (rc == 0 && (scr1 - scr0) > 500) {
		kprintf("AMDH T2: BUG REPRODUCED (EC timeout branch is dead "
		    "code: %ld polls, still 'success')\n", scr1 - scr0);
	} else {
		kprintf("AMDH T2: NOT reproduced\n");
		fail++;
	}

	/* --- T2b: wedged EC output path (OBF never sets) --- */
	amdh_reset(AMDH_WEDGE_OBF, 32, SMB_STS_OK);
	E.out = 0x5C;		/* stale byte from a PREVIOUS EC data read */
	scr0 = E.n_sc_reads;
	val = 0x77;
	rc = amdsmb_ec_read(&fake_sc, SMB_BCNT, &val);
	scr1 = E.n_sc_reads;
	kprintf("AMDH T2b wedge_obf: ec_read rc=%d (BUG: expect 0; correct "
	    "would be 1) val=0x%02x (STALE byte 0x5C accepted as SMB_BCNT!) "
	    "sc_reads=%ld\n", rc, val, scr1 - scr0);
	if (rc == 0 && val == 0x5C && (scr1 - scr0) > 500) {
		kprintf("AMDH T2b: BUG REPRODUCED (OBF timeout branch is dead "
		    "code; stale EC data byte returned as register value)\n");
	} else {
		kprintf("AMDH T2b: NOT reproduced\n");
		fail++;
	}

	/* --- T3: uninitialized caller count (DF-5507) --- */
	kprintf("AMDH T3 uninit-caller-count truth table "
	    "(smb.c:317 bcount is uninit; len=32 healthy slave):\n");
	for (i = 0; i < (int)(sizeof(gs) / sizeof(gs[0])); i++) {
		g = gs[i];
		amdh_reset(AMDH_FAST, 32, SMB_STS_OK);
		count = (u_char)g;
		memset(buf, 0xAA, sizeof(buf));
		rc = amdsmb_bread(&fake_dev, 0x5A, 0x42, &count, buf);
		if (rc == SMB_EINVAL) {
			kprintf("AMDH T3 g=%3d -> rc=%d SMB_EINVAL: SMB_BREAD "
			    "fails, feature dead for this stack byte\n",
			    g, rc);
		} else {
			kprintf("AMDH T3 g=%3d -> rc=%d count=%d "
			    "stale_0xAA_bytes_in_out=%d (kernel-stack bytes "
			    "smbioctl would copyout: %d)\n", g, rc,
			    count, amdh_count_pattern(buf, count, 0xAA),
			    count - g < 0 ? 0 : count - g);
		}
	}
	kprintf("AMDH T3w: bwrite with garbage count -> wire BCNT value:\n");
	for (i = 0; i < (int)(sizeof(gs) / sizeof(gs[0])); i++) {
		g = gs[i];
		amdh_reset(AMDH_FAST, 32, SMB_STS_OK);
		memset(buf, 0x11, sizeof(buf));
		count = (u_char)g;
		rc = amdsmb_bwrite(&fake_dev, 0x5A, 0x42, count, buf);
		kprintf("AMDH T3w g=%3d -> rc=%d transmitted_BCNT=0x%02x%s\n",
		    g, rc, E.bc_written,
		    (g >= 1 && g <= 32) ? " (garbage length put on the wire)" :
		    " (rejected before wire)");
	}

	/* --- T4: OOB register reads with len=255 (KNOWN DF-1091:
	 *     environment corroboration only, NOT a new finding) --- */
	amdh_reset(AMDH_FAST, 255, SMB_STS_OK);
	count = 32;
	memset(buf, 0xAA, sizeof(buf));
	rc = amdsmb_bread(&fake_dev, 0x5A, 0x42, &count, buf);
	{
		int maxidx = -1, j;
		for (j = 0; j < 512; j++)
			if (E.n_reg_reads[j] && j > maxidx)
				maxidx = j;
		kprintf("AMDH T4 (known DF-1091): rc=%d count=%d highest "
		    "register index read=0x%02x (bank ends 0x27)\n",
		    rc, count, maxidx);
	}

	kprintf("AMDH: END (%d harness self-failure(s))\n", fail);
}

static int
amdh_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		memset(&fake_res, 0, sizeof(fake_res));
		fake_res.r_bustag = 1;		/* MEM space (cosmetic here) */
		fake_res.r_bushandle = 0;
		memset(&fake_dev, 0, sizeof(fake_dev));
		fake_dev.nameunit = amdh_name;
		fake_dev.softc = &fake_sc;
		memset(&fake_sc, 0, sizeof(fake_sc));
		fake_sc.res = &fake_res;
		fake_sc.smbus = &fake_dev;
		lockinit(&fake_sc.lock, "amdh", 0, LK_CANRECURSE);
		amdh_run_tests();
		return (0);
	case MOD_UNLOAD:
		lockuninit(&fake_sc.lock);
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t amdh_mod = { "amdh", amdh_modevent, NULL };
DECLARE_MODULE(amdh, amdh_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);