/*
 * DF-2533 / DF-2534 verification harness -- kernel-compiled.
 *
 * This file is compiled INTO the kernel (conf/files entry
 * "dev/disk/xdisk/df253x_harness.c standard"), NOT kldload'd (runtime
 * kldload of new modules GPFs on this guest).
 *
 * It #includes the VERBATIM shipping xdisk.c (build.sh sha256-gates the
 * guest /usr/src copy against the audited repo copy:
 *   58b0ed1672b5fb63f7d484a7fae6ba04484372c962e13d9c573b69790cfa55c9).
 * Only the *external* functions xdisk.c calls that must not run for real
 * in this harness (device/disk/devstat registration) are interposed via
 * pre-include #define renames -- every xdisk.c function that the findings
 * cite (xa_close, xa_open, xa_strategy, xa_size, xa_terminate_check) is
 * the real, unmodified, shipping machine code.
 *
 * Fabricated state:
 *   - struct cdev allocated with kmalloc (only si_drv1 is used by the
 *     cited paths; xa_open also writes si_bsize_phys/best which live in
 *     the same allocated object).
 *   - struct xa_softc fabricated exactly like xaio_rcvdmsg's LNK_SPAN
 *     create path builds it (xdisk.c:429-470), minus the dmsg plumbing.
 *
 * Triggers (sysctl -w, root only):
 *   debug.df2533_close_null=1  DF-2533 path A: xa_close with si_drv1==NULL
 *                              -> returns ENXIO HOLDING xdisk_lk; proves
 *                              the leak with lockstatus() + a probe thread
 *                              running the real xa_open that wedges.
 *   debug.df2533_close_term=1  DF-2533 path B: xa_close with
 *                              sc->terminating==1 and sc->lk never
 *                              acquired -> xdisk.c:772 releases the wrong
 *                              lock -> kern_lock.c:768 unconditional
 *                              panic("lockmgr: LK_RELEASE: no lock held")
 *                              (while xdisk_lk is still held).
 *   debug.df2534_strat_null=1  DF-2534: xa_strategy with si_drv1==NULL ->
 *                              xdisk.c:801 sc=NULL, :805
 *                              devstat_start_transaction(&sc->stats) ->
 *                              write fault at ~NULL.
 *   debug.df2534_race=<secs>   DF-2534: hammer race. cpu0 thread calls the
 *                              real xa_size in a tight loop; cpu1 thread
 *                              builds a softc, installs it in si_drv1,
 *                              and runs the real xa_terminate_check under
 *                              xdisk_lk (NULL at :665, kfree at :680),
 *                              then immediately re-allocates the freed
 *                              slot from M_XDISK and plants
 *                              0xC0FFEE00DF2534 in d_media_blocks.
 *                              If the reader EVER observes the magic
 *                              value, its stale-pointer deref at :835
 *                              landed after the free + reuse-write:
 *                              use-after-free READ of freed kernel heap
 *                              through the real code path.
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/sysctl.h>
#include <sys/systm.h>

/*
 * Interpose the externals that must not run for real.  The renames apply
 * to xdisk.c's calls (and to the renamed prototypes), xdisk.c itself is
 * untouched on disk -- build.sh refuses to compile against anything but
 * the sha256-verified verbatim copy.
 */
#define make_dev			dfh_make_dev
#define destroy_dev			dfh_destroy_dev
#define dev_ops_remove_all		dfh_dev_ops_remove_all
#define disk_destroy			dfh_disk_destroy
#define devstat_remove_entry		dfh_devstat_remove_entry

#include "xdisk.c"		/* VERBATIM (sha256-gated by build.sh) */

#undef make_dev
#undef destroy_dev
#undef dev_ops_remove_all
#undef disk_destroy
#undef devstat_remove_entry

#include <sys/kthread.h>
#include <sys/lock.h>
/* NOTE: the hammer threads periodically tsleep 1 tick: a spinning kernel
 * thread on DragonFly starves its CPU (lwkt_yield does not preempt between
 * priority classes), which would wedge the guest during the race */

/* ---------------- interposition stubs ---------------- */

static struct cdev dfh_ctrl_dev;	/* fake control cdev, never registered */
static u_long dfh_disk_destroy_calls;
static u_long dfh_devstat_remove_calls;

cdev_t
dfh_make_dev(struct dev_ops *ops, int minor, uid_t uid, gid_t gid,
	     int perms, const char *fmt, ...)
{
	/* do not create /dev/xdisk; keep the module inert */
	return (&dfh_ctrl_dev);
}

void
dfh_destroy_dev(cdev_t dev)
{
	/* no-op */
}

int
dfh_dev_ops_remove_all(struct dev_ops *ops)
{
	return (0);
}

void
dfh_disk_destroy(struct disk *disk)
{
	atomic_fetchadd_long(&dfh_disk_destroy_calls, 1);
}

void
dfh_devstat_remove_entry(struct devstat *ds)
{
	atomic_fetchadd_long(&dfh_devstat_remove_calls, 1);
}

/* ---------------- shared helpers ---------------- */

static int dfh_inited;

static void
dfh_init(void)
{
	if (!dfh_inited) {
		TAILQ_INIT(&xaiocomq);
		RB_INIT(&xa_device_tree);
		lockinit(&xdisk_lk, "xdisk", 0, 0);
		dfh_inited = 1;
	}
	xa_debug = 0;		/* keep the hammer loops off the console */
}

static struct cdev *
dfh_new_dev(void)
{
	struct cdev *dev;

	dev = kmalloc(sizeof(*dev), M_XDISK, M_WAITOK | M_ZERO);
	return (dev);
}

static xa_softc_t *
dfh_new_sc(struct cdev *dev)
{
	xa_softc_t *sc;

	sc = kmalloc(sizeof(*sc), M_XDISK, M_WAITOK | M_ZERO);
	sc->unit = 77;
	sc->info.d_media_blocks = 0xAAAA;
	sc->info.d_media_blksize = 512;
	lockinit(&sc->lk, "xalk", 0, 0);
	TAILQ_INIT(&sc->spanq);
	TAILQ_INIT(&sc->bioq);
	TAILQ_INIT(&sc->tag_freeq);
	TAILQ_INIT(&sc->tag_pendq);
	if (dev)
		dev->si_drv1 = sc;
	return (sc);
}

/* =================================================================
 * DF-2533 -- xa_close defensive paths
 * =================================================================
 */

static volatile int df33_open_started;
static volatile int df33_open_finished;
static volatile int df33_open_ret;

static void
df33_open_thr(void *arg)
{
	struct dev_open_args ap;
	cdev_t dev = arg;

	bzero(&ap, sizeof(ap));
	ap.a_head.a_dev = dev;

	df33_open_started = 1;
	wakeup(&df33_open_started);

	/* the REAL xa_open: writes si_bsize_*, then blocks forever on the
	 * leaked xdisk_lk (baseline) */
	df33_open_ret = xa_open(&ap);
	df33_open_finished = 1;
	wakeup(&df33_open_finished);

	kthread_exit();
}

static int
df33_close_null(SYSCTL_HANDLER_ARGS)
{
	int error, val = 0;
	struct dev_close_args ap;
	struct cdev *dev;
	struct thread *td = NULL;
	int st_after, stuck, i;

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

	dfh_init();
	kprintf("DF2533-A: === DF-2533 path 1: xa_close with si_drv1 == NULL "
		"(xdisk.c:769-770) ===\n");

	dev = dfh_new_dev();
	dev->si_drv1 = NULL;		/* raced-destruction state */

	bzero(&ap, sizeof(ap));
	ap.a_head.a_dev = dev;

	kprintf("DF2533-A: before xa_close: xdisk_lk lockstatus=%d\n",
		lockstatus(&xdisk_lk, curthread));

	error = xa_close(&ap);
	st_after = lockstatus(&xdisk_lk, curthread);
	kprintf("DF2533-A: xa_close returned %d\n", error);
	kprintf("DF2533-A: xdisk_lk lockstatus=%d after return "
		"(LK_EXCLUSIVE=2 => EXCLUSIVE LOCK LEAKED)\n", st_after);

	/*
	 * Consequence proof: the real xa_open on another thread can never
	 * acquire xdisk_lk again -> every subsequent xdisk operation wedges.
	 */
	df33_open_started = 0;
	df33_open_finished = 0;
	kthread_create_cpu(df33_open_thr, dev, &td, 0, "df33opn");

	for (i = 0; i < 100 && !df33_open_started; ++i)
		tsleep(&df33_open_started, 0, "df33a", hz / 10);
	/* give the probe thread a full second to get stuck */
	tsleep(&df33_open_finished, 0, "df33a", hz);

	stuck = df33_open_started && !df33_open_finished;
	kprintf("DF2533-A: probe real-xa_open thread: started=%d finished=%d "
		"=> %s\n", df33_open_started, df33_open_finished,
		stuck ? "STUCK FOREVER on leaked xdisk_lk "
		      "(all later xdisk ops deadlock)" :
			"not stuck (leak not present?)");

	/*
	 * Cleanup so the box stays usable for the remaining triggers: we are
	 * the owner of the leaked lock, release it (evidence captured above).
	 */
	if (stuck) {
		lockmgr(&xdisk_lk, LK_RELEASE);
		kprintf("DF2533-A: cleanup: harness released the leaked "
			"xdisk_lk (a real caller never could)\n");
	}
	for (i = 0; i < 100 && !df33_open_finished; ++i)
		tsleep(&df33_open_finished, 0, "df33a", hz / 10);
	kprintf("DF2533-A: probe thread completed: finished=%d xa_open=%d\n",
		df33_open_finished, df33_open_ret);

	kfree(dev, M_XDISK);
	kprintf("DF2533-A: done\n");
	return (0);
}

SYSCTL_PROC(_debug, OID_AUTO, df2533_close_null,
	    CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, df33_close_null, "I",
	    "write 1: xa_close raced-destruction NULL path (lock leak)");

static int
df33_close_term(SYSCTL_HANDLER_ARGS)
{
	int error, val = 0;
	struct dev_close_args ap;
	struct cdev *dev;
	xa_softc_t *sc;
	int st_after;

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

	dfh_init();
	kprintf("DF2533-B: === DF-2533 path 2: xa_close with "
		"sc->terminating == 1 (xdisk.c:771-773) ===\n");

	dev = dfh_new_dev();
	sc = dfh_new_sc(dev);
	sc->terminating = 1;		/* raced-destruction state */
	/* sc->lk is lockinit'd but NEVER acquired -- exactly the state the
	 * defensive code runs in */

	bzero(&ap, sizeof(ap));
	ap.a_head.a_dev = dev;

	kprintf("DF2533-B: sc=%p sc->terminating=%d sc->lk.lk_count=0x%lx\n",
		sc, sc->terminating, (unsigned long)sc->lk.lk_count);
	kprintf("DF2533-B: calling real xa_close: expect xdisk.c:772 "
		"lockmgr(&sc->lk, LK_RELEASE) on the NEVER-acquired sc->lk "
		"-> lockmgr_release() unconditional panic "
		"(kern_lock.c:768), while xdisk_lk stays held\n");

	error = xa_close(&ap);
	st_after = lockstatus(&xdisk_lk, curthread);
	kprintf("DF2533-B: xa_close RETURNED %d (no panic?!), "
		"xdisk_lk lockstatus=%d, sc->lk.lk_count=0x%lx\n",
		error, st_after, (unsigned long)sc->lk.lk_count);

	/* only reachable on a kernel where the wrong-lock release does not
	 * panic: still demonstrate the xdisk_lk leak + corrupted sc->lk */
	if (st_after == LK_EXCLUSIVE) {
		kprintf("DF2533-B: xdisk_lk still exclusively held after "
			"return => LEAKED; releasing for cleanup\n");
		lockmgr(&xdisk_lk, LK_RELEASE);
	}
	kfree(dev, M_XDISK);
	kfree(sc, M_XDISK);
	kprintf("DF2533-B: done\n");
	return (0);
}

SYSCTL_PROC(_debug, OID_AUTO, df2533_close_term,
	    CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, df33_close_term, "I",
	    "write 1: xa_close raced-destruction terminating path (wrong lock)");

/* =================================================================
 * DF-2534 -- xa_strategy / xa_size lifetime
 * =================================================================
 */

static void
df34_bio_done(struct bio *bio)
{
	kprintf("DF2534-S: harness bio_done fired (bio completed with "
		"b_error=%d b_flags=0x%x)\n",
		bio->bio_buf->b_error, bio->bio_buf->b_flags);
}

static int
df34_strat_null(SYSCTL_HANDLER_ARGS)
{
	int error, val = 0;
	struct dev_strategy_args ap;
	struct cdev *dev;
	struct bio *bio;
	struct buf *bp;

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

	dfh_init();
	kprintf("DF2534-S: === DF-2534: xa_strategy with si_drv1 == NULL "
		"(xdisk.c:801,805) ===\n");

	dev = dfh_new_dev();
	dev->si_drv1 = NULL;

	bio = kmalloc(sizeof(*bio), M_XDISK, M_WAITOK | M_ZERO);
	bp = kmalloc(sizeof(*bp), M_XDISK, M_WAITOK | M_ZERO);
	bp->b_cmd = BUF_CMD_READ;
	bp->b_bcount = 512;
	bio->bio_buf = bp;
	bio->bio_offset = 0;
	bio->bio_done = df34_bio_done;	/* survivable biodone if patched */

	bzero(&ap, sizeof(ap));
	ap.a_head.a_dev = dev;
	ap.a_bio = bio;

	kprintf("DF2534-S: calling real xa_strategy: xdisk.c:801 loads "
		"sc=NULL, :805 devstat_start_transaction(&sc->stats) "
		"derefs ~NULL+offset -> kernel page fault\n");

	error = xa_strategy(&ap);
	kprintf("DF2534-S: xa_strategy returned %d without faulting "
		"(fixed?) bio_done=%d\n", error, bp->b_error);

	kfree(bio, M_XDISK);
	kfree(bp, M_XDISK);
	kfree(dev, M_XDISK);
	kprintf("DF2534-S: done\n");
	return (0);
}

SYSCTL_PROC(_debug, OID_AUTO, df2534_strat_null,
	    CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, df34_strat_null, "I",
	    "write 1: xa_strategy with NULL softc");

/*
 * ---- hammer race: real xa_size vs real xa_terminate_check ----
 */
#define DF34_MAGIC	0xC0FFEE00DF2534ULL

static struct cdev *df34_race_dev;
static volatile int df34_run;
static volatile int df34_r_done, df34_t_done;
static u_long df34_r_iters, df34_t_iters, df34_null_seen, df34_hits;
static u_long df34_reuse_eq, df34_reuse_ne;

static void
df34_reader_thr(void *arg)
{
	struct dev_psize_args ap;
	u_long spin = 0;

	while (df34_run) {
		if ((++spin & 0x3FFFF) == 0)
			tsleep(&df34_run, 0, "df34y", 1);
		bzero(&ap, sizeof(ap));
		ap.a_head.a_dev = df34_race_dev;
		if (xa_size(&ap) == ENXIO) {
			df34_null_seen++;
		} else if ((uint64_t)ap.a_result == DF34_MAGIC) {
			df34_hits++;
			if (df34_hits < 4)
				kprintf("DF2534-RACE: *** UAF READ CAPTURED "
					"(#%lu): real xa_size dereferenced "
					"FREED-then-REUSED softc through the "
					"stale si_drv1 pointer "
					"(result=0x%016lx)\n",
					df34_hits,
					(unsigned long)ap.a_result);
		}
		df34_r_iters++;
	}
	df34_r_done = 1;
	wakeup(&df34_r_done);
	kthread_exit();
}

static void
df34_term_thr(void *arg)
{
	xa_softc_t *sc;
	void *q;
	u_long spin = 0;

	while (df34_run) {
		if ((++spin & 0x3FFFF) == 0)
			tsleep(&df34_run, 0, "df34y", 1);
		sc = dfh_new_sc(NULL);

		lockmgr(&xdisk_lk, LK_EXCLUSIVE);
		RB_INSERT(xa_softc_tree, &xa_device_tree, sc);
		sc->dev = df34_race_dev;
		df34_race_dev->si_drv1 = sc;	/* device live */

		/* the REAL xa_terminate_check: RB_REMOVE, disk_destroy
		 * (stubbed), si_drv1 = NULL (:665), kfree(sc) (:680) */
		xa_terminate_check(sc);

		lockmgr(&xdisk_lk, LK_RELEASE);

		/* immediate same-zone re-alloc: slab LIFO usually hands the
		 * exact block back; plant the magic so a stale deref is
		 * unmistakable */
		q = kmalloc(sizeof(xa_softc_t), M_XDISK, M_WAITOK | M_ZERO);
		if (q == (void *)sc) {
			((xa_softc_t *)q)->info.d_media_blocks = DF34_MAGIC;
			df34_reuse_eq++;
		} else {
			df34_reuse_ne++;
		}
		kfree(q, M_XDISK);

		df34_t_iters++;
	}
	df34_t_done = 1;
	wakeup(&df34_t_done);
	kthread_exit();
}

static int
df34_race(SYSCTL_HANDLER_ARGS)
{
	int error, val = 0, i;
	struct thread *rtd = NULL, *ttd = NULL;

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

	dfh_init();
	kprintf("DF2534-RACE: === DF-2534: real xa_size (cpu0) vs real "
		"xa_terminate_check (cpu1) for %d s ===\n", val);

	df34_race_dev = dfh_new_dev();
	df34_run = 1;
	df34_r_done = df34_t_done = 0;
	df34_r_iters = df34_t_iters = df34_null_seen = df34_hits = 0;
	df34_reuse_eq = df34_reuse_ne = 0;

	kthread_create_cpu(df34_reader_thr, NULL, &rtd, 0, "df34rd");
	kthread_create_cpu(df34_term_thr, NULL, &ttd, 1, "df34tm");

	for (i = 0; i < val * 10; ++i)
		tsleep(&df34_run, 0, "df34w", hz / 10);

	df34_run = 0;
	for (i = 0; i < 100 && (!df34_r_done || !df34_t_done); ++i)
		tsleep(&df34_r_done, 0, "df34w", hz / 10);

	kprintf("DF2534-RACE: reader xa_size iterations: %lu "
		"(ENXIO/NULL path: %lu)\n", df34_r_iters, df34_null_seen);
	kprintf("DF2534-RACE: terminator terminate_check iterations: %lu "
		"(same-address reuse: %lu, other: %lu)\n",
		df34_t_iters, df34_reuse_eq, df34_reuse_ne);
	kprintf("DF2534-RACE: UAF READS CAPTURED (stale deref saw "
		"0xC0FFEE00DF2534 written AFTER the free): %lu\n", df34_hits);
	kprintf("DF2534-RACE: %s\n", df34_hits ?
		"USE-AFTER-FREE READ CONFIRMED in the real xa_size" :
		"no stale read observed this run (see VERDICT.md)");

	kfree(df34_race_dev, M_XDISK);
	kprintf("DF2534-RACE: done\n");
	return (0);
}

SYSCTL_PROC(_debug, OID_AUTO, df2534_race,
	    CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, df34_race, "I",
	    "write <seconds>: hammer real xa_size vs xa_terminate_check");
