/*
 * DF-3610 — boot_ps NULL-pointer dereference in si_dpm_enable (radeon si_dpm.c)
 *
 * Twin of amdgpu DF-3586.  rdev->pm.dpm.boot_ps is only ever set in
 * si_parse_pplib_non_clock_info() (si_dpm.c:6726-6727) when a VBIOS power
 * state carries ATOM_PPLIB_CLASSIFICATION_BOOT (0x0008).  A crafted VBIOS
 * whose state array has no such state (or ucNumEntries == 0) leaves
 * boot_ps == NULL, but si_parse_power_table() still returns 0.
 * radeon_pm_init_dpm() (radeon_pm.c:1448-1453) ignores the dpm.init return
 * value, assigns current_ps/requested_ps = boot_ps and calls
 * radeon_dpm_enable() -> si_dpm_enable(rdev):
 *
 *   si_dpm.c:6354   struct radeon_ps *boot_ps = rdev->pm.dpm.boot_ps;   // NULL
 *   si_dpm.c:6401   ret = si_init_smc_table(rdev);
 *   si_dpm.c:4700     radeon_boot_state = rdev->pm.dpm.boot_ps;         // NULL
 *   si_dpm.c:4746     si_populate_smc_initial_state(rdev, NULL, table)
 *   si_dpm.c:4372       struct ni_ps *initial_state = ni_get_ps(NULL)
 *   ni_dpm.c:733-735    return rps->ps_priv;    // read at NULL+0x28 -> panic
 *
 * This harness replicates the parse + first deref.  Part [A] executes the
 * real dereference in a forked child (expect SIGSEGV at si_addr = 0x28 —
 * in-kernel equivalent: unrecoverable NULL-deref panic during boot/DPM
 * enable).  Part [B] shows the one-line guard from fix.diff rejecting the
 * configuration instead.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdint.h>
#include <errno.h>
#include <stddef.h>

/* --- verbatim-relevant replica of struct radeon_ps (radeon.h:1342-1356) --- */
struct radeon_ps {
	uint32_t caps;
	uint32_t class;
	uint32_t class2;
	uint32_t vclk;
	uint32_t dclk;
	uint32_t evclk;
	uint32_t ecclk;
	uint8_t  vce_active;            /* bool */
	uint8_t  _pad[3];
	int32_t  vce_level;             /* enum */
	void    *ps_priv;               /* offset 0x28 on LP64 */
};

struct ni_pl { uint32_t mclk, sclk, vddc; };
struct ni_ps { uint32_t performance_level_count; struct ni_pl performance_levels[16]; };

#define ATOM_PPLIB_CLASSIFICATION_BOOT 0x0008   /* pptable.h:230 */

static struct radeon_ps *g_boot_ps;   /* == rdev->pm.dpm.boot_ps */

/* ni_dpm.c:733-735 */
static struct ni_ps *ni_get_ps(struct radeon_ps *rps)
{
	return rps->ps_priv;                    /* NULL+0x28 read when rps==NULL */
}

/* si_dpm.c:4368-4372 (first statements of si_populate_smc_initial_state) */
static uint32_t si_populate_smc_initial_state(struct radeon_ps *radeon_initial_state)
{
	struct ni_ps *initial_state = ni_get_ps(radeon_initial_state);
	return initial_state->performance_levels[0].mclk;   /* first data deref */
}

/* si_parse_pplib_non_clock_info (si_dpm.c:6706-6730), BOOT-classification only */
static void parse_non_clock_info(struct radeon_ps *rps, uint16_t usClassification)
{
	rps->class = usClassification;
	if (rps->class & ATOM_PPLIB_CLASSIFICATION_BOOT)
		g_boot_ps = rps;
}

/* si_init_smc_table (si_dpm.c:4696/4700/4746) */
static int si_init_smc_table(struct radeon_ps *boot_state)
{
	return (int)si_populate_smc_initial_state(boot_state);
}

/* si_dpm_enable (si_dpm.c:6349-6354, 6401) */
static int si_dpm_enable(struct radeon_ps *boot_ps)
{
	return si_init_smc_table(boot_ps);
}

static volatile uint32_t sink;

static void run_deref(struct radeon_ps *boot_ps)
{
	sink = (uint32_t)si_dpm_enable(boot_ps);   /* the kernel panics in here */
	printf("    UNREACHABLE: deref survived\n");
}

int main(void)
{
	/* crafted VBIOS: 3 power states, none classified BOOT */
	struct radeon_ps ps[3];
	struct ni_ps priv[3];
	uint8_t class_i[] = { 0x0001 /* UI battery */, 0x0002 /* UI balanced */,
			      0x0004 /* UI performance */ };

	printf("== DF-3610 harness: crafted VBIOS with no ATOM_PPLIB_CLASSIFICATION_BOOT state ==\n\n");
	printf("rdev->pm.dpm.boot_ps initial            = %p\n", (void *)g_boot_ps);
	for (int i = 0; i < 3; i++) {
		memset(&ps[i], 0, sizeof(ps[i]));
		memset(&priv[i], 0, sizeof(priv[i]));
		priv[i].performance_level_count = 1;
		priv[i].performance_levels[0].mclk = 60000 + i;
		ps[i].ps_priv = &priv[i];
		parse_non_clock_info(&ps[i], class_i[i]);
	}
	printf("after si_parse_power_table (3 states): boot_ps = %p\n", (void *)g_boot_ps);
	printf("si_parse_power_table returns 0 (parse 'succeeded')\n");
	printf("radeon_pm.c:1448 ignores dpm.init rc; :1449 current_ps=requested_ps=boot_ps\n");
	printf("offsetof(struct radeon_ps, ps_priv)    = 0x%zx\n", offsetof(struct radeon_ps, ps_priv));

	printf("\n[A] Buggy path: si_dpm_enable -> si_init_smc_table -> "
	       "si_populate_smc_initial_state -> ni_get_ps(NULL)->ps_priv\n");
	fflush(stdout);
	pid_t p = fork();
	if (p == 0) {
		run_deref(g_boot_ps);
		_exit(0);
	}
	int st = 0; waitpid(p, &st, 0);
	if (WIFSIGNALED(st))
		printf("    child died from SIGSEGV (signal %d): NULL+0x28 read\n"
		       "    >>> In kernel: page-fault at 0x28 -> panic during DPM enable <<<\n",
		       WTERMSIG(st));
	else
		printf("    child exited %d (NOT expected)\n", WEXITSTATUS(st));

	printf("\n[B] Fixed path (fix.diff: si_dpm_enable guard `if (!boot_ps) return -EINVAL;`)\n");
	if (g_boot_ps == NULL) {
		printf("    rc=-EINVAL (DPM enable refused, no dereference)\n");
	} else {
		printf("    boot_ps non-NULL, normal path\n");
	}
	return 0;
}
