/*
 * DF-3852 harness — BIOS-controlled voltage-GPIO register index injected
 *                    into RREG32/WREG32 without range validation.
 *
 * Kernel source replicated 1:1 (logic, not hardware):
 *   sys/dev/drm/radeon/radeon_combios.c:2739-2763  (VOLTAGE_GPIO record parse:
 *        misc & 0x4  ->  gpio.reg  = RBIOS16(...) * 4      [16-bit BIOS word]
 *                        gpio.mask = (1 << tmp)           [BIOS byte])
 *   sys/dev/drm/radeon/r100.c:352-364  r100_pm_misc() sink:
 *        tmp = RREG32(voltage->gpio.reg); tmp |=/= mask; WREG32(reg, tmp);
 *   sys/dev/drm/radeon/r100.c:4125-4138 r100_mm_{rreg,wreg}_slow():
 *        reg >= rmmio_size  ->  writel(reg, RADEON_MM_INDEX); MM_DATA  (indexed)
 *
 * Demonstration: a COMBIOS powerplay table with misc bit 0x4 set and
 * gpio register word 0xFFFF yields gpio.reg = 0xFFFF*4 = 0x3FFFC, i.e. a
 * read-modify-write at MMIO offset 262140 — 3x past the 64 KB radeon MMIO
 * BAR — routed through the MM_INDEX indirection with the raw attacker
 * offset. In-range values (< 0x10000) still let the VBIOS pick ANY register
 * in the device space for a single-bit RMW.
 */
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

/* ---- minimal rdev simulation ---------------------------------------- */
static uint8_t  bios[512];                 /* crafted COMBIOS image        */
static uint32_t rmmio_sim[0x10000 / 4];    /* the real BAR is 64 KB        */
#define RBIOS8(i)   (bios[i])
#define RBIOS16(i)  (RBIOS8(i) | (RBIOS8((i) + 1) << 8))
#define RMMIO_SIZE  0x10000                 /* BAR length, radeon pre-AVIVO */

static unsigned long oob_indexed_writes, inbar_writes;

/* r100.c:4125 r100_mm_rreg_slow / :4132 r100_mm_wreg_slow (MM_INDEX path) */
static uint32_t mm_rreg(uint32_t reg)
{
	if (reg < RMMIO_SIZE)
		return rmmio_sim[reg / 4];
	printf("  [slow] RREG32: MM_INDEX <- 0x%08x (>= rmmio_size 0x%x) "
	       "-> indexed read OUTSIDE the MMIO BAR\n", reg, RMMIO_SIZE);
	return 0;
}
static void mm_wreg(uint32_t reg, uint32_t v)
{
	if (reg < RMMIO_SIZE) {
		rmmio_sim[reg / 4] = v;
		inbar_writes++;
	} else {
		oob_indexed_writes++;
		printf("  [slow] WREG32: MM_INDEX <- 0x%08x (>= rmmio_size 0x%x) "
		       "-> indexed write OUTSIDE the MMIO BAR, value 0x%08x\n",
		       reg, RMMIO_SIZE, v);
	}
}

/* ---- radeon_combios.c:2720-2781, the powerplay VOLTAGE_GPIO parse ---- */
struct radeon_gpio { int valid; uint32_t reg, mask; };
static struct radeon_gpio voltage_gpio;

static void combios_powerplay_voltage_parse(uint16_t offset)
{
	uint8_t rev = RBIOS8(offset);
	uint16_t misc = RBIOS16(offset + 0x5);
	uint8_t tmp;

	if (!(misc & 0x4))                  /* radeon_combios.c:2739 */
		return;

	voltage_gpio.valid = true;          /* :2747 */
	if (rev < 6) {
		voltage_gpio.reg  = RBIOS16(offset + 0x5 + 0xb) * 4;   /* :2749-2750 */
		tmp = RBIOS8(offset + 0x5 + 0xd);                      /* :2751      */
		voltage_gpio.mask = (1 << tmp);                        /* :2752      */
	} else {
		uint16_t vtoff = RBIOS16(offset + 0x5 + 0xc);          /* :2755      */
		voltage_gpio.reg  = RBIOS16(vtoff) * 4;                /* :2757-2758 */
		tmp = RBIOS8(vtoff + 0x2);                             /* :2759      */
		voltage_gpio.mask = (1 << tmp);                        /* :2760      */
	}
	printf("[combios] VOLTAGE_GPIO parsed: gpio.reg=0x%08x gpio.mask=0x%08x "
	       "(reg raw word 0x%04x * 4)\n",
	       voltage_gpio.reg, voltage_gpio.mask, voltage_gpio.reg / 4);
}

/* ---- r100.c:345 r100_pm_misc() sink --------------------------------- */
static void r100_pm_misc(void)
{
	uint32_t tmp;

	if (!voltage_gpio.valid)
		return;
	/* ATOM_PM_MISCINFO_VOLTAGE_DROP_SUPPORT == 0x4 == the same bit the
	 * combios parser tested: one crafted word enables the whole path.   */
	tmp = mm_rreg(voltage_gpio.reg);
	tmp |= voltage_gpio.mask;           /* active_high variant */
	mm_wreg(voltage_gpio.reg, tmp);
}

int main(void)
{
	uint16_t pp = 0x40;                 /* powerplay table offset in image */

	/* crafted powerplay table, rev 5 (rev<6 branch) */
	memset(bios, 0, sizeof(bios));
	bios[pp + 0x0] = 5;                 /* rev                            */
	bios[pp + 0x5] = 0x04;              /* misc lo: bit2 -> VOLTAGE_GPIO  */
	bios[pp + 0x6] = 0x00;              /* misc hi (also DROPSUPPORT=0x4) */
	bios[pp + 0x5 + 0xb] = 0xff;        /* gpio register word lo          */
	bios[pp + 0x5 + 0xc] = 0xff;        /* gpio register word hi -> 0xFFFF*/
	bios[pp + 0x5 + 0xd] = 0x09;        /* mask bit 9                     */

	printf("== DF-3852: crafted COMBIOS powerplay voltage-GPIO record ==\n");
	combios_powerplay_voltage_parse(pp);
	printf("[pm] r100_pm_misc() executing RREG32/WREG32 RMW at that index:\n");
	r100_pm_misc();

	printf("\nresult: %lu in-BAR write(s), %lu out-of-BAR indexed write(s)\n",
	       inbar_writes, oob_indexed_writes);
	if (voltage_gpio.reg >= RMMIO_SIZE && oob_indexed_writes == 1) {
		printf("PRIMITIVE PROVEN: BIOS word 0x%x -> MMIO offset 0x%x "
		       ">= rmmio_size 0x%x -> raw offset driven into MM_INDEX.\n",
		       voltage_gpio.reg / 4, voltage_gpio.reg, RMMIO_SIZE);
		return 0;
	}
	fprintf(stderr, "unexpected\n");
	return 1;
}
