/*
 * DF-3754 — boot_ps / ps_priv NULL-pointer dereference in ni_dpm_enable
 *           (radeon Northern Islands DPM, sys/dev/drm/radeon/ni_dpm.c)
 *
 * radeon twin of si_dpm DF-3610 / ci_dpm DF-3646 (amdgpu DF-3586/3623).
 *
 * rdev->pm.dpm.boot_ps is assigned in exactly ONE place on the ni parse
 * path — ni_parse_pplib_non_clock_info() (ni_dpm.c:3914-3915) — and that
 * function is only reached inside the `if (ucStateEntrySize - 1)` block of
 * ni_parse_power_table() (ni_dpm.c:4018-4039).  A crafted VBIOS
 * PowerPlayInfo table therefore leaves boot_ps == NULL while the parse
 * still "succeeds" (returns 0) in at least three shapes:
 *
 *   1. ucNumStates == 0            -> no states, num_ps = 0
 *   2. ucStateEntrySize == 1       -> the whole per-state block is skipped,
 *                                     ps_priv stays NULL for every state and
 *                                     non-clock info (BOOT classification)
 *                                     is never parsed
 *   3. ucNumStates >= 1 but no state carries ATOM_PPLIB_CLASSIFICATION_BOOT
 *
 * radeon_pm_init_dpm() (radeon_pm.c:1448-1453) then runs
 *      radeon_dpm_init(rdev);                       // return value IGNORED
 *      current_ps = requested_ps = boot_ps;         // NULL ok'd
 *      ret = radeon_dpm_enable(rdev);
 * -> ni_dpm_enable (ni_dpm.c:3585):
 *   :3589   struct radeon_ps *boot_ps = rdev->pm.dpm.boot_ps;   // NULL
 *   :3641   ret = ni_init_smc_table(rdev);
 *   :1943     radeon_boot_state = rdev->pm.dpm.boot_ps;         // NULL
 *   :1975     ni_populate_smc_initial_state(rdev, NULL, table)
 *   :1683       struct ni_ps *initial_state = ni_get_ps(NULL);
 *   :733-735    return rps->ps_priv;   // read at NULL+0x28 -> page fault -> panic
 *
 * Shape 2 additionally leaves dpm.ps[i].ps_priv == NULL; the first power
 * state change then faults in ni_dpm_pre_set_power_state ->
 * ni_update_requested_ps (ni_dpm.c:3576/3581: `ni_pi->requested_ps = *new_ps`
 * with new_ps == NULL -> read at NULL+0x0).
 *
 * This harness replicates the parse over a synthetic VBIOS buffer using the
 * real table layouts and the real consumer call graph.  [A] executes the
 * boot_ps==NULL dereference in a forked child (expect SIGSEGV at 0x28 — the
 * in-kernel equivalent is an unrecoverable page fault / panic during DPM
 * enable).  [A2] executes the ps_priv==NULL dereference (expect SIGSEGV at
 * 0x0).  [B] shows the one-line guard from fix.diff rejecting the config.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdint.h>
#include <stddef.h>
#include <errno.h>

/* ---------- verbatim-relevant kernel struct replicas ---------- */

/* struct radeon_ps (radeon.h:1342-1356) — ps_priv at 0x28 on LP64 */
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 */
};

/* struct ni_ps (ni_dpm.h:172-176) */
struct rv7xx_pl { uint32_t mclk, sclk, vddc, vddci, flags; };
struct ni_ps {
	uint16_t performance_level_count;
	uint8_t  dc_compatible;
	struct rv7xx_pl performance_levels[16];
};

/* ATOM powerplay table fragments (pptable.h layout, packed) */
struct pp_header {                 /* _ATOM_PPLIB_POWERPLAYTABLE body */
	uint8_t  ucNumStates;
	uint16_t usStateArrayOffset;
	uint16_t usClockInfoArrayOffset;
	uint16_t usNonClockInfoArrayOffset;
	uint8_t  ucClockInfoSize;
	uint8_t  ucNonClockSize;
	uint8_t  ucStateEntrySize;
} __attribute__((packed));

struct pp_state_v1 {               /* _ATOM_PPLIB_STATE */
	uint8_t ucNonClockStateIndex;
	uint8_t ucClockStateIndices[1]; /* ucStateEntrySize-1 follow */
} __attribute__((packed));

struct pp_nonclock {               /* _ATOM_PPLIB_NONCLOCK_INFO (front) */
	uint16_t usSize;
	uint32_t ulCapsAndSettings;
	uint16_t usClassification;
	uint16_t usClassification2;
	uint32_t ulVCLK;
	uint32_t ulDCLK;
} __attribute__((packed));

struct pp_clockinfo {              /* union pplib_clock_info.evergreen (front) */
	uint16_t usEngineClockLow;
	uint8_t  ucEngineClockHigh;
	uint16_t usMemoryClockLow;
	uint8_t  ucMemoryClockHigh;
	uint16_t usVDDC;
	uint16_t usVDDCI;
	uint32_t ulFlags;
} __attribute__((packed));

#define ATOM_PPLIB_CLASSIFICATION_BOOT 0x0008

/* ---------- driver state replica ---------- */
struct dpm_state {
	struct radeon_ps *ps;        /* rdev->pm.dpm.ps */
	int num_ps;                  /* rdev->pm.dpm.num_ps */
	struct radeon_ps *boot_ps;   /* rdev->pm.dpm.boot_ps */
} dpm;

struct ni_power_info_rep { struct ni_ps requested_ps; } ni_pi_rep;
struct radeon_ps requested_rps;                 /* eg_pi->requested_rps */

/* ---------- ni_dpm.c function replicas ---------- */

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

/* ni_parse_pplib_non_clock_info (ni_dpm.c:3894-3918), BOOT part only */
static void ni_parse_pplib_non_clock_info(struct radeon_ps *rps,
					  struct pp_nonclock *nc)
{
	rps->class = nc->usClassification;
	if (rps->class & ATOM_PPLIB_CLASSIFICATION_BOOT)
		dpm.boot_ps = rps;        /* ni_dpm.c:3914-3915 — the ONLY set */
}

/* ni_parse_pplib_clock_info (ni_dpm.c:3920-3938), relevant parts */
static void ni_parse_pplib_clock_info(struct radeon_ps *rps, int index,
				      struct pp_clockinfo *ci)
{
	struct ni_ps *ps = ni_get_ps(rps);
	ps->performance_level_count = index + 1;   /* ni_dpm.c:3929 */
	ps->performance_levels[index].sclk = ci->usEngineClockLow;
	ps->performance_levels[index].mclk = ci->usMemoryClockLow;
}

/* ni_parse_power_table (ni_dpm.c:3985-4043) — verbatim walk */
static int ni_parse_power_table(uint8_t *bios, size_t bios_len,
				uint16_t data_offset)
{
	struct pp_header *hdr = (struct pp_header *)(bios + data_offset);
	struct pp_state_v1 *st;
	struct pp_nonclock *nc;
	struct pp_clockinfo *ci;
	uint8_t *idx;
	struct ni_ps *pspriv;
	int i, j;

	/* ni_dpm.c:4003 — array alloc (never NULL here) */
	dpm.ps = calloc(hdr->ucNumStates, sizeof(struct radeon_ps));

	/* ni_dpm.c:4008-4040 */
	for (i = 0; i < hdr->ucNumStates; i++) {
		st = (struct pp_state_v1 *)(bios + data_offset +
			hdr->usStateArrayOffset + i * hdr->ucStateEntrySize);
		nc = (struct pp_nonclock *)(bios + data_offset +
			hdr->usNonClockInfoArrayOffset +
			st->ucNonClockStateIndex * hdr->ucNonClockSize);
		if (hdr->ucStateEntrySize - 1) {          /* ni_dpm.c:4018 */
			pspriv = calloc(1, sizeof(struct ni_ps));
			if (pspriv == NULL) {
				free(dpm.ps);
				return -ENOMEM;
			}
			dpm.ps[i].ps_priv = pspriv;
			ni_parse_pplib_non_clock_info(&dpm.ps[i], nc);
			idx = (uint8_t *)&st->ucClockStateIndices[0];
			for (j = 0; j < (hdr->ucStateEntrySize - 1); j++) {
				ci = (struct pp_clockinfo *)(bios + data_offset +
					hdr->usClockInfoArrayOffset +
					idx[j] * hdr->ucClockInfoSize);
				ni_parse_pplib_clock_info(&dpm.ps[i], j, ci);
			}
		}
	}
	dpm.num_ps = hdr->ucNumStates;                 /* ni_dpm.c:4041 */
	return 0;
}

/* ni_populate_smc_initial_state (ni_dpm.c:1679-1683 + first data use) */
static uint32_t ni_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 */
}

/* ni_init_smc_table (ni_dpm.c:1938/1943/1975) */
static int ni_init_smc_table(struct radeon_ps *boot_state)
{
	struct radeon_ps *radeon_boot_state = boot_state;   /* :1943 */
	(void)radeon_boot_state;
	return (int)ni_populate_smc_initial_state(boot_state); /* :1975 */
}

/* ni_dpm_enable (ni_dpm.c:3585-3641) */
static int ni_dpm_enable(struct radeon_ps *boot_ps)
{
	return ni_init_smc_table(boot_ps);                  /* :3641 */
}

/* ni_update_requested_ps (ni_dpm.c:3573-3583) */
static void ni_update_requested_ps(struct radeon_ps *rps)
{
	struct ni_ps *new_ps = ni_get_ps(rps);              /* :3576 — NULL */
	requested_rps = *rps;
	ni_pi_rep.requested_ps = *new_ps;                   /* :3581 — *NULL */
}

static volatile uint32_t sink;

static void run_boot_deref(struct radeon_ps *boot_ps)
{
	sink = (uint32_t)ni_dpm_enable(boot_ps);
	printf("    UNREACHABLE: deref survived\n");
}

static void run_pspriv_deref(struct radeon_ps *rps)
{
	ni_update_requested_ps(rps);
	printf("    UNREACHABLE: deref survived\n");
}

/* ---------- synthetic VBIOS builders ---------- */

static uint8_t vbios[512];

static void build_vbios(unsigned num_states, unsigned state_entry_size,
			unsigned class_of_state0)
{
	struct pp_header *h;
	struct pp_state_v1 *st;
	struct pp_nonclock *nc;
	struct pp_clockinfo *ci;
	unsigned i;

	memset(vbios, 0, sizeof(vbios));
	h = (struct pp_header *)(vbios + 16);      /* data_offset = 16 */
	h->ucNumStates = num_states;
	h->ucStateEntrySize = state_entry_size;
	h->ucClockInfoSize = sizeof(struct pp_clockinfo);
	h->ucNonClockSize = sizeof(struct pp_nonclock);
	h->usStateArrayOffset = sizeof(struct pp_header) + 4;
	h->usNonClockInfoArrayOffset = h->usStateArrayOffset +
		num_states * state_entry_size + 4;
	h->usClockInfoArrayOffset = h->usNonClockInfoArrayOffset +
		sizeof(struct pp_nonclock);

	for (i = 0; i < num_states; i++) {
		st = (struct pp_state_v1 *)(vbios + 16 + h->usStateArrayOffset +
			i * state_entry_size);
		st->ucNonClockStateIndex = 0;
		st->ucClockStateIndices[0] = 0;
	}
	nc = (struct pp_nonclock *)(vbios + 16 + h->usNonClockInfoArrayOffset);
	nc->usClassification = class_of_state0;    /* 0x0001 = UI battery etc. */
	ci = (struct pp_clockinfo *)(vbios + 16 + h->usClockInfoArrayOffset);
	ci->usEngineClockLow = 40000;
	ci->usMemoryClockLow = 30000;
}

static void reset_dpm(void)
{
	memset(&dpm, 0, sizeof(dpm));
	dpm.boot_ps = NULL;
}

int main(void)
{
	printf("== DF-3754 harness: ni_dpm boot_ps/ps_priv NULL deref ==\n");
	printf("offsetof(struct radeon_ps, ps_priv) = 0x%zx (kernel: 0x28, LP64)\n\n",
	       offsetof(struct radeon_ps, ps_priv));

	/* ---- shape 1+3: parse succeeds, boot_ps stays NULL ---- */
	reset_dpm();
	build_vbios(3, 2, 0x0002 /* UI balanced: NOT BOOT */);
	int rc = ni_parse_power_table(vbios, sizeof(vbios), 16);
	printf("[shape 3] 3 states, none ATOM_PPLIB_CLASSIFICATION_BOOT:\n"
	       "    ni_parse_power_table rc=%d, num_ps=%d, boot_ps=%p\n",
	       rc, dpm.num_ps, (void *)dpm.boot_ps);

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

	/* ---- shape 2: ucStateEntrySize == 1 -> ps_priv NULL ---- */
	reset_dpm();
	build_vbios(2, 1, ATOM_PPLIB_CLASSIFICATION_BOOT);
	rc = ni_parse_power_table(vbios, sizeof(vbios), 16);
	printf("\n[shape 2] ucStateEntrySize==1: rc=%d, num_ps=%d, boot_ps=%p,\n"
	       "    ps[0].ps_priv=%p ps[1].ps_priv=%p (never assigned)\n",
	       rc, dpm.num_ps, (void *)dpm.boot_ps,
	       dpm.ps ? dpm.ps[0].ps_priv : NULL,
	       dpm.ps ? dpm.ps[1].ps_priv : NULL);
	printf("    (a BOOT-classified non-clock entry exists but is never parsed:\n"
	       "    the whole block is skipped, so boot_ps is NULL too)\n");

	printf("\n[A2] Buggy path: ni_dpm_pre_set_power_state ->\n"
	       "     ni_update_requested_ps -> `*new_ps` with new_ps==NULL\n");
	fflush(stdout);
	p = fork();
	if (p == 0) { run_pspriv_deref(&dpm.ps[0]); _exit(0); }
	st = 0; waitpid(p, &st, 0);
	if (WIFSIGNALED(st))
		printf("     child died from signal %d: read at NULL+0x0\n"
	       "     >>> kernel: page-fault at 0 -> panic on first state change <<<\n",
		       WTERMSIG(st));
	else
		printf("     child exited %d (NOT expected)\n", WEXITSTATUS(st));

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

	return 0;
}
