/*
 * DF-4951 — vega12_pp_tables_uninitialize double-fini NULL deref
 * ================================================================
 * Lifted-copy harness (DF-1179/DF-4395/DF-4545 convention; the in-kernel
 * path needs a Vega12 ASIC, absent on the audit guest).
 *
 * Faithful replica of the kernel call sequence:
 *
 *   BOOT:   hwmgr_hw_init (hwmgr.c:207)
 *             pptable_init  = vega12_pp_tables_initialize
 *                             (vega12_processpptables.c:266-294, kzalloc
 *                              hwmgr->pptable :272, header checks :280,
 *                              init_powerplay_table_information :289)
 *             backend_init  = vega12_hwmgr_backend_init  [OK at boot]
 *             -> dpm_enabled = true, sysfs pp_table attr exists
 *             (amdgpu_pm.c:1992-1998 only gates on dpm_enabled)
 *
 *   WRITE1: root writes /sys/.../pp_table with a table that passes the
 *           header checks but whose backend init fails (e.g. SMU task
 *           failure):
 *             pp_dpm_set_pp_table (amd_powerplay.c:676, gates only on
 *               hwmgr && hwmgr->pm_en :680-681 -- pm_en stays TRUE on
 *               every hwmgr_hw_init error path)
 *             -> amd_powerplay_reset (:660)
 *                hwmgr_hw_fini (hwmgr.c:267, pm_en true)
 *                  pptable_fini #1  -> frees pptable, hwmgr->pptable=NULL
 *                hwmgr_hw_init
 *                  pptable_init     -> OK (fresh kzalloc)
 *                  backend_init     -> FAILS (injected)
 *                  -> err1: backend_fini; pptable_fini #2
 *                     (hwmgr.c:257-262) -> hwmgr->pptable = NULL
 *                reset returns error; driver stays bound, attrs alive,
 *                dpm_enabled stale true, pm_en true
 *
 *   WRITE2: root writes pp_table again (or rmmod / GPU reset / driver
 *           teardown -- any path reaching hwmgr_hw_fini):
 *             pp_dpm_set_pp_table -> amd_powerplay_reset -> hwmgr_hw_fini
 *               pptable_fini #3 = vega12_pp_tables_uninitialize
 *                 pp_table_info = hwmgr->pptable == NULL
 *                 kfree(pp_table_info->power_saving_clock_max)  (:301)
 *                 -> LOAD from NULL+16 -> kernel panic
 *
 * Struct offsets are the kernel's (phm_ppt_v3_information, hwmgr.h:579-594,
 * naturally aligned x86-64: power_saving_clock_max @ +16), so the fault
 * address 0x10 matches the in-kernel fault exactly.
 *
 * Cases:
 *   1 BOOT-OK            healthy init + healthy reset          -> no fault
 *   2 BUG                write1(backend fail) + write2         -> SIGSEGV @ 0x10
 *   3 CONTROL-LEAK       write1(bad header, err: path leaks) + write2
 *                         -> no fault (frees the leaked pptable),
 *                            demonstrates why write2 only crashes after
 *                            the err1/err2 (fini-inside-init) failure
 *   4 PATCHED            case 2 with fix.diff NULL guard       -> no fault
 *
 * Build: cc -O2 -Wall -o vega12_dfini_null vega12_dfini_null.c
 * Run:   ./vega12_dfini_null
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <ucontext.h>
#include <unistd.h>
#include <errno.h>
#include <stddef.h>
#include <sys/wait.h>

typedef uint8_t  u8;  typedef uint8_t  UCHAR;
typedef uint16_t u16; typedef uint16_t USHORT;
typedef uint32_t u32; typedef uint32_t ULONG;

/* ---- vega12_pptable.h ---- */
#define ATOM_VEGA12_TABLE_REVISION_VEGA12	9
#define ATOM_VEGA12_ODSETTING_GFXCLKFMAX	0
#define ATOM_VEGA12_ODSETTING_UCLKFMAX		8
#define ATOM_VEGA12_ODSETTING_POWERPERCENTAGE	9
enum { ATOM_VEGA12_ODSETTING_COUNT = 15 };
enum { ATOM_VEGA12_PPCLOCK_COUNT   = 10 };
#define ATOM_VEGA12_PP_THERMALCONTROLLER_NONE	0

#pragma pack(push, 1)
struct atom_common_table_header {
	USHORT structuresize;
	UCHAR  format_revision;
	UCHAR  content_revision;
};
typedef struct _PPTable_t_stub {
	ULONG Version;
	UCHAR pad[2044];
} PPTable_t;
typedef struct _ATOM_Vega12_POWERPLAYTABLE {
	struct atom_common_table_header sHeader;	/* +0  */
	UCHAR  ucTableRevision;			/* +6  */
	USHORT usTableSize;			/* +7  */
	ULONG  ulGoldenPPID;			/* +9  */
	ULONG  ulGoldenRevision;			/* +13 */
	USHORT usFormatID;			/* +17 */
	ULONG  ulPlatformCaps;			/* +19 */
	UCHAR  ucThermalControllerType;		/* +23 */
	USHORT usSmallPowerLimit1;		/* +24 */
	USHORT usSmallPowerLimit2;		/* +26 */
	USHORT usBoostPowerLimit;			/* +28 */
	USHORT usODTurboPowerLimit;		/* +30 */
	USHORT usODPowerSavePowerLimit;		/* +32 */
	USHORT usSoftwareShutdownTemp;		/* +34 */
	ULONG  PowerSavingClockMax[ATOM_VEGA12_PPCLOCK_COUNT];  /* +36 */
	ULONG  PowerSavingClockMin[ATOM_VEGA12_PPCLOCK_COUNT];  /* +76 */
	ULONG  ODSettingsMax[ATOM_VEGA12_ODSETTING_COUNT];      /* +116 */
	ULONG  ODSettingsMin[ATOM_VEGA12_ODSETTING_COUNT];      /* +176 */
	USHORT usReserve[5];			/* +236 */
	PPTable_t smcPPTable;			/* +246 */
} ATOM_Vega12_POWERPLAYTABLE;
#pragma pack(pop)

/* ---- hwmgr.h:579-594, natural alignment (NOT packed) -- offsets matter */
struct phm_ppt_v3_information {
	u8  uc_thermal_controller_type;		/* +0  */
	u16 us_small_power_limit1;		/* +2  */
	u16 us_small_power_limit2;		/* +4  */
	u16 us_boost_power_limit;		/* +6  */
	u16 us_od_turbo_power_limit;		/* +8  */
	u16 us_od_powersave_power_limit;	/* +10 */
	u16 us_software_shutdown_temp;		/* +12 */
	u32 *power_saving_clock_max;		/* +16  <-- first deref */
	u32 *power_saving_clock_min;		/* +24 */
	u8  *od_feature_capabilities;		/* +32 */
	u32 *od_settings_max;			/* +40 */
	u32 *od_settings_min;			/* +48 */
	void *smc_pptable;			/* +56 */
};

struct pp_table_func {
	int (*pptable_init)(void *hwmgr);
	int (*pptable_fini)(void *hwmgr);
};

/* minimal pp_hwmgr: only the fields this path touches */
struct pp_hwmgr {
	int  pm_en;
	void *ps;
	struct pp_table_func *pptable_func;
	struct phm_ppt_v3_information *pptable;
	const ATOM_Vega12_POWERPLAYTABLE *soft_pp_table;
	size_t soft_pp_table_size;
	const ATOM_Vega12_POWERPLAYTABLE *hardcode_pp_table;
	/* injection knobs */
	int backend_inject_fail;
};

/* ---- allocation accounting (leak visibility) ---- */
static long g_allocs, g_frees;

static void *kzalloc(size_t n)
{
	void *p = calloc(1, n ? n : 1);
	if (p) g_allocs++;
	return p;
}
static void kfree(void *p)
{
	if (p) g_frees++;
	free(p);
}

/* ============ vega12_processpptables.c lifted code ============ */

static int check_powerplay_tables(const ATOM_Vega12_POWERPLAYTABLE *t)	/* :64 */
{
	if (!(t->sHeader.format_revision >= ATOM_VEGA12_TABLE_REVISION_VEGA12)) {
		printf("    [kernel] \"Unsupported PPTable format!\"\n");
		return -1;
	}
	if (!(t->sHeader.structuresize > 0)) {
		printf("    [kernel] \"Invalid PowerPlay Table!\"\n");
		return -1;
	}
	return 0;
}

/* smu_helper.c:64-84 */
static int phm_copy_overdrive_settings_limits_array(u32 **dst, const u32 *src,
						    u32 count)
{
	u32 i, *table;
	table = kzalloc(sizeof(u32) * count);
	if (!table)
		return -12;			/* -ENOMEM */
	for (i = 0; i < count; i++)
		table[i] = src[i];
	*dst = table;
	return 0;
}

/* smu_helper.c:42-62 */
static int phm_copy_clock_limits_array(u32 **dst, const u32 *src, u32 count)
{
	u32 i, *table;
	table = kzalloc(sizeof(u32) * count);
	if (!table)
		return -12;
	for (i = 0; i < count; i++)
		table[i] = src[i];
	*dst = table;
	return 0;
}

static int append_vbios_pptable(void *smc)	/* :102, VBIOS data -> OK */
{
	(void)smc;
	return 0;
}

static int init_powerplay_table_information(		/* :191 */
		struct pp_hwmgr *hwmgr,
		const ATOM_Vega12_POWERPLAYTABLE *powerplay_table)
{
	struct phm_ppt_v3_information *pptable_information =
		(struct phm_ppt_v3_information *)hwmgr->pptable;
	PPTable_t *smc;
	int result;

	/* :209-215 overdrive limits read straight from the table */
	/* :217-224: od copies -- RETURN VALUES IGNORED (kernel parity) */
	phm_copy_overdrive_settings_limits_array(
			&pptable_information->od_settings_max,
			powerplay_table->ODSettingsMax,
			ATOM_VEGA12_ODSETTING_COUNT);
	phm_copy_overdrive_settings_limits_array(
			&pptable_information->od_settings_min,
			powerplay_table->ODSettingsMin,
			ATOM_VEGA12_ODSETTING_COUNT);

	/* :252-253: power-saving clock copies -- RETURNS IGNORED (parity) */
	phm_copy_clock_limits_array(&pptable_information->power_saving_clock_max,
			powerplay_table->PowerSavingClockMax,
			ATOM_VEGA12_PPCLOCK_COUNT);
	phm_copy_clock_limits_array(&pptable_information->power_saving_clock_min,
			powerplay_table->PowerSavingClockMin,
			ATOM_VEGA12_PPCLOCK_COUNT);

	/* :255-259 */
	smc = kzalloc(sizeof(PPTable_t));
	if (!smc)
		return -12;
	pptable_information->smc_pptable = smc;
	memcpy(pptable_information->smc_pptable, &powerplay_table->smcPPTable,
	       sizeof(PPTable_t));

	/* :261 */
	result = append_vbios_pptable(pptable_information->smc_pptable);
	return result;
}

static int vega12_pp_tables_initialize(void *ctx)	/* :267 */
{
	struct pp_hwmgr *hwmgr = ctx;
	const ATOM_Vega12_POWERPLAYTABLE *powerplay_table;
	int result = 0;

	/* :272 */
	hwmgr->pptable = kzalloc(sizeof(struct phm_ppt_v3_information));
	if (hwmgr->pptable == NULL)
		return -12;

	/* :276 get_powerplay_table: cached soft table (our injected blob) */
	powerplay_table = hwmgr->soft_pp_table;
	if (!powerplay_table) {
		printf("    [kernel] \"Missing PowerPlay Table!\"\n");
		return -1;
	}

	/* :280 */
	result = check_powerplay_tables(powerplay_table);
	if (result != 0)
		return result;			/* <-- LEAK: pptable not freed */

	/* :289 */
	result = init_powerplay_table_information(hwmgr, powerplay_table);
	if (result != 0)
		return result;			/* <-- LEAK likewise */

	return result;
}

static int vega12_pp_tables_uninitialize(void *ctx)	/* :296 -- THE BUG */
{
	struct pp_hwmgr *hwmgr = ctx;
	struct phm_ppt_v3_information *pp_table_info =
			(struct phm_ppt_v3_information *)(hwmgr->pptable);

#ifdef PATCHED	/* fix.diff: NULL guard (DF-4951) */
	if (pp_table_info == NULL)
		return 0;
#endif

	kfree(pp_table_info->power_saving_clock_max);	/* :301 <-- NULL+16 */
	pp_table_info->power_saving_clock_max = NULL;

	kfree(pp_table_info->power_saving_clock_min);
	pp_table_info->power_saving_clock_min = NULL;

	kfree(pp_table_info->od_settings_max);
	pp_table_info->od_settings_max = NULL;

	kfree(pp_table_info->od_settings_min);
	pp_table_info->od_settings_min = NULL;

	kfree(pp_table_info->smc_pptable);
	pp_table_info->smc_pptable = NULL;

	kfree(hwmgr->pptable);				/* :316 */
	hwmgr->pptable = NULL;

	return 0;
}

static struct pp_table_func vega12_pptable_funcs = {
	vega12_pp_tables_initialize,
	vega12_pp_tables_uninitialize,
};

/* ============ hwmgr.c lifted flow ============ */

static int backend_init_inj(struct pp_hwmgr *hwmgr)
{
	return hwmgr->backend_inject_fail ? -22 : 0;	/* -EINVAL */
}
static int backend_fini_inj(struct pp_hwmgr *hwmgr) { (void)hwmgr; return 0; }

static int hwmgr_hw_init(struct pp_hwmgr *hwmgr)	/* :207 */
{
	int ret;

	if (!hwmgr->pm_en)
		return 0;

	ret = hwmgr->pptable_func->pptable_init(hwmgr);	/* :222 */
	if (ret)
		goto err;				/* :223-224 -- no fini! */

	ret = backend_init_inj(hwmgr);			/* :229 */
	if (ret)
		goto err1;				/* -> fini #1 */

	/* psm_init_power_state_table etc. omitted: not reached in BUG case */
	printf("    [kernel] dpm fully enabled (pm.dpm_enabled = true)\n");
	return 0;
err1:
	backend_fini_inj(hwmgr);			/* :258-259 */
	hwmgr->pptable_func->pptable_fini(hwmgr);	/* :261-262 fini INSIDE init */
err:
	return ret;
}

static int hwmgr_hw_fini(struct pp_hwmgr *hwmgr)	/* :267 */
{
	if (!hwmgr || !hwmgr->pm_en)
		return 0;

	/* phm_stop_thermal_controller / psm_set_boot_states /
	 * psm_adjust_power_state_dynamic / phm_disable_* :
	 * all func-guarded or `if (!hwmgr->ps) return 0` (pp_psm.c:178)
	 * -- no-ops in the post-failed-init state modelled here. */
	hwmgr->pptable_func->pptable_fini(hwmgr);	/* :280-281 */
	return 0;
}

/* ============ amd_powerplay.c lifted flow ============ */

static int amd_powerplay_reset(void *handle)		/* :660 */
{
	struct pp_hwmgr *hwmgr = handle;
	int ret;

	ret = hwmgr_hw_fini(hwmgr);
	if (ret)
		return ret;
	ret = hwmgr_hw_init(hwmgr);
	if (ret)
		return ret;
	return 0;	/* hwmgr_handle_task(COMPLETE_INIT) elided */
}

static int pp_dpm_set_pp_table(void *handle, const char *buf, size_t size) /* :676 */
{
	struct pp_hwmgr *hwmgr = handle;
	int ret;

	if (!hwmgr || !hwmgr->pm_en)			/* :680-681 -- pm_en TRUE */
		return -22;

	/* :683-693 hardcode_pp_table caching */
	(void)buf; (void)size;
	ret = amd_powerplay_reset(handle);		/* :697 */
	return ret;
}

/* ============ VBIOS/user table blobs ============ */

static ATOM_Vega12_POWERPLAYTABLE good_table = {
	.sHeader = { .structuresize = sizeof(ATOM_Vega12_POWERPLAYTABLE),
		     .format_revision = ATOM_VEGA12_TABLE_REVISION_VEGA12,
		     .content_revision = 15 },
	.ucTableRevision = 9,
	.usTableSize = sizeof(ATOM_Vega12_POWERPLAYTABLE),
};
static ATOM_Vega12_POWERPLAYTABLE badhdr_table = {	/* fails :68 check */
	.sHeader = { .structuresize = sizeof(ATOM_Vega12_POWERPLAYTABLE),
		     .format_revision = 1 },
};

/* ============ runner ============ */

static void run_case_body(const char *name, int write1_backend_fail,
			  int write1_badhdr, int patched);

static const char *g_case;
static void segv_handler(int sig, siginfo_t *si, void *uc)
{
	ucontext_t *u = uc;
	(void)sig;
	printf(">>> SIGSEGV: fault address %p (power_saving_clock_max = "
	       "pptable + 0x10)\n", si->si_addr);
	printf(">>> faulting RIP context ucontext=%p -- kernel equivalent: "
	       "kfree(NULL->power_saving_clock_max) in "
	       "vega12_pp_tables_uninitialize (vega12_processpptables.c:301)\n", u);
	printf(">>> PANIC-CLASS FAULT REPRODUCED (case %s)\n", g_case);
	_exit(99);
}

static void run_case(const char *name, int write1_backend_fail, int write1_badhdr,
		     int patched __attribute__((unused)))
{
	pid_t pid;
	int st;

	fflush(stdout);
	pid = fork();
	if (pid == 0) {		/* child: one case, may fault */
		run_case_body(name, write1_backend_fail, write1_badhdr, patched);
		_exit(0);
	}
	waitpid(pid, &st, 0);
	if (WIFSIGNALED(st))
		printf("    case %s: child killed by signal %d\n\n", name,
		       WTERMSIG(st));
	else if (WIFEXITED(st) && WEXITSTATUS(st) == 99)
		printf("    case %s: child _exit(99) PANIC-CLASS FAULT\n\n",
		       name);
	else
		printf("    case %s: child clean exit %d\n\n", name,
		       WEXITSTATUS(st));
}

static void run_case_body(const char *name, int write1_backend_fail,
			  int write1_badhdr, int patched __attribute__((unused)))
{
	struct pp_hwmgr hwmgr_s, *hwmgr = &hwmgr_s;
	long a0, f0;

	g_case = name;
	printf("== CASE %s ==\n", name);
	a0 = g_allocs; f0 = g_frees;

	memset(hwmgr, 0, sizeof(*hwmgr));
	hwmgr->pm_en = 1;
	hwmgr->pptable_func = &vega12_pptable_funcs;

	/* BOOT */
	hwmgr->soft_pp_table = &good_table;
	hwmgr->backend_inject_fail = 0;
	if (hwmgr_hw_init(hwmgr) != 0) {
		printf("    boot init FAILED (unexpected)\n");
		return;
	}

	/* root WRITE #1 */
	hwmgr->soft_pp_table = write1_badhdr ? &badhdr_table : &good_table;
	hwmgr->backend_inject_fail = write1_backend_fail;
	printf("    [user ] root writes pp_table #1 (%s)\n",
	       write1_badhdr ? "bad header" :
	       (write1_backend_fail ? "valid hdr, backend fails" : "healthy"));
	{
		int r = pp_dpm_set_pp_table(hwmgr, NULL, 0);
		printf("    amd_powerplay_reset #1 -> %s (%d), "
		       "hwmgr->pptable now %s\n", r ? "FAIL" : "ok", r,
		       hwmgr->pptable ? "non-NULL" : "NULL");
	}

	/* root WRITE #2 (or rmmod/teardown reaching hwmgr_hw_fini) */
	hwmgr->soft_pp_table = &good_table;
	hwmgr->backend_inject_fail = 0;
	printf("    [user ] root writes pp_table #2 / driver teardown "
	       "-> hwmgr_hw_fini\n");
	{
		int r = pp_dpm_set_pp_table(hwmgr, NULL, 0);
		printf("    amd_powerplay_reset #2 -> %s (%d) (unreached "
		       "on fault)\n", r ? "FAIL" : "ok", r);
	}
	printf("    case %s survived; alloc balance this case: %+ld "
	       "(kernel: leaked phm_ppt_v3_information + members on "
	       "error paths)\n", name, (g_allocs - a0) - (g_frees - f0));
}

int main(void)
{
	struct sigaction sa;

	memset(&sa, 0, sizeof(sa));
	sa.sa_sigaction = segv_handler;
	sa.sa_flags = SA_SIGINFO;
	sigaction(SIGSEGV, &sa, NULL);
	setvbuf(stdout, NULL, _IOLBF, 0);	/* survive _exit in handler */

	printf("DF-4951 lifted-copy harness: vega12 double pptable_fini "
	       "NULL deref\n");
	printf("phm_ppt_v3_information.power_saving_clock_max offset = %zu "
	       "(kernel x86-64: 16)\n\n",
	       offsetof(struct phm_ppt_v3_information, power_saving_clock_max));

	run_case("1 BOOT-OK (healthy reset)", 0, 0, 0);
	run_case("2 BUG: write1 backend-fail + write2", 1, 0, 0);
	run_case("3 CONTROL-LEAK: write1 bad-header + write2", 0, 1, 0);
#ifdef PATCHED
	printf("(PATCHED build: NULL guard active -- case 2 must survive)\n");
	run_case("4 PATCHED: write1 backend-fail + write2", 1, 0, 1);
#endif
	return 0;
}
