/*
 * DF-3173 — autofs timeout-task torn down on the wrong per-CPU taskqueue.
 *
 * Bug (sys/vfs/autofs/autofs.c):
 *   autofs_trigger_one() arms the request's timeout task on the CREATOR's
 *   per-cpu thread taskqueue:
 *       #define _taskqueue_thread (taskqueue_thread[mycpuid])   (:303)
 *       TIMEOUT_TASK_INIT(_taskqueue_thread, &ar->ar_task, ...) (:357)
 *       taskqueue_enqueue_timeout(_taskqueue_thread, ...)       (:359)
 *   but tears it down on the LAST RELEASER's per-cpu queue:
 *       taskqueue_cancel_timeout(_taskqueue_thread, ...)        (:398)
 *       taskqueue_drain_timeout (_taskqueue_thread, ...)        (:399)
 *
 *   The last releaser is not necessarily the creator: the request is
 *   refcounted and shared (dedup at :324-339), and the creator can exit
 *   early on EINTR (:366-377) — e.g. SIGKILL while blocked in the
 *   trigger — leaving a waiter on another CPU holding the last
 *   reference.  When that waiter frees the request on a different CPU
 *   than the creator:
 *
 *   (A) PANIC: if the timeout callout already transitioned the task
 *       onto its home queue (ta_pending>0) while a non-timeout
 *       completion (AUTOFSDONE ioctl, autofs.c:506-520) races the
 *       timeout boundary, taskqueue_cancel_locked() runs STAILQ_REMOVE
 *       of a task that is not on the passed queue's list
 *       (sys/kern/subr_taskqueue.c:424-425) — sys/sys/queue.h:319-331
 *       walks off the end and derefs NULL (ta_link is at offset 0).
 *
 *   (B) UAF: if the home queue's runner already dequeued the task
 *       (ta_pending==0, home tq_running==task), the wrong-queue drain
 *       compares the WRONG queue's tq_running (subr_taskqueue.c:493)
 *       and returns without waiting, so objcache_put(ar) (autofs.c:400)
 *       runs while autofs_task() (:196-211) is still executing on `ar`:
 *       AUTOFS_WARN prints ar->ar_path from freed memory (read-after-
 *       free) and writes ar_error=ETIMEDOUT / ar_wildcards / ar_done /
 *       ar_in_progress into the freed, reusable object (write-after-
 *       free).
 *
 * Harness (production-shaped on the trigger side; BOTH waiters are real
 * user processes doing stat() -> VOP_NRESOLVE -> autofs_trigger):
 *   - CREATOR: the script stats /df3173_mp/<key> at round start, then
 *     SIGKILLs it ~0.95 s in — the EINTR early exit that leaves the
 *     last reference to another CPU.
 *   - LAST RELEASER: a second stat() of the same key (dedup) on
 *     whatever CPU the scheduler picks (5/6 chance per round of
 *     differing from the creator's CPU).
 *   - A userland root "automountd stand-in" (fake_daemon3173.c) answers
 *     AUTOFSDONE just after the 1 s timeout boundary — the situation of
 *     a lagging automountd catching up with the 30 s frontier of its
 *     request queue.
 *
 * Module responsibilities: publish round/key/stage sysctls for the
 * script, read the request's home queue (ar_task.t.ta_queue), occupy
 * the home queue's runner with a spinner task across the boundary
 * (mode A: so the timeout task stays queued; mode B: so it is dequeued
 * right at the boundary and parked on sc_lock by a hold thread), and in
 * mode B re-get the freed object from the per-cpu magazine to observe
 * post-free writes.
 *
 * Mode A (debug.df3173_mode=1)  => foreign-list STAILQ_REMOVE panic.
 * Mode B (debug.df3173_mode=2)  => wrong-queue drain frees the request
 *                                  under the running autofs_task (UAF).
 *
 * Driver script (stat + SIGKILL choreography): run.sh / runB.sh.
 * Build: build.sh.
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/mount.h>
#include <sys/vnode.h>
#include <sys/mutex2.h>
#include <sys/kthread.h>
#include <sys/sysctl.h>
#include <sys/thread.h>
#include <sys/thread2.h>
#include <sys/types.h>
#include <sys/queue.h>
#include <sys/taskqueue.h>
#include <sys/objcache.h>
#include <sys/malloc.h>
#include <sys/spinlock2.h>
#include <cpu/cpufunc.h>

#include <vfs/autofs/autofs.h>

extern struct mntlist mountlist;

static int			g_mode;		/* 0=idle 1=panic 2=uaf */
static struct autofs_mount	*g_amp;
static struct autofs_node	*g_root;

static volatile int		g_round;	/* published round number */
static char			g_key[32];	/* published round key */
static int			g_pub_round;	/* sysctl shadow */
static char			g_pub_key[32];	/* sysctl shadow */
static volatile int		g_stage;	/* 1 = releaser may stat */
static int			g_pub_stage;

static struct autofs_request	*g_ar_seen;	/* this round's request */
static struct autofs_request	*g_ar_last;	/* last seen request ptr
						 * (not cleared on free;
						 * getters compare against
						 * this) */
static int			g_ar_id_seen = -1;
static struct taskqueue		*g_ar_q;	/* queue the timeout task
						 * was armed on (= the
						 * CREATOR cpu's queue) */

SYSCTL_INT(_debug, OID_AUTO, df3173_mode, CTLFLAG_RW, &g_mode, 0,
    "1=panic variant, 2=uaf variant");
SYSCTL_INT(_debug, OID_AUTO, df3173_round, CTLFLAG_RD, &g_pub_round, 0,
    "current round (script sync)");
SYSCTL_STRING(_debug, OID_AUTO, df3173_key, CTLFLAG_RD, g_pub_key,
    sizeof(g_pub_key), "current round key (script sync)");
SYSCTL_INT(_debug, OID_AUTO, df3173_stage, CTLFLAG_RD, &g_pub_stage, 0,
    "1 = request created, releaser may stat");

/*
 * sleeper task: occupies the home taskqueue's single runner thread
 * across the timeout boundary by SLEEPING inside the task function.
 * (A busy-spinning task would starve this kernel's per-cpu softclock
 * thread - kern_timeout.c:423 "softclock %d" - and the timeout callout
 * itself would never fire; sleeping keeps the runner busy, the
 * timeout task queued behind it, and softclock running.)
 */
static void		sleep_fn(void *, int);
static struct task	g_sleep_task = TASK_INITIALIZER(0, sleep_fn, NULL);
static volatile int	g_sleep_upto;

static void
sleep_fn(void *arg, int pending)
{
	int delta = g_sleep_upto - ticks;

	kprintf("DF3173:   sleeper running on cpu%d until tick %d "
	    "(now %d)\n", mycpuid, g_sleep_upto, ticks);
	while ((int)(ticks - g_sleep_upto) < 0) {
		delta = g_sleep_upto - ticks;
		tsleep(&g_sleep_upto, 0, "df3173sl", delta > 0 ? delta : 1);
	}
	kprintf("DF3173:   sleeper done at tick %d\n", ticks);
}

/*
 * hold thread (mode B): holds sc_lock across the dequeue moment so the
 * dequeued autofs_task parks on the mutex while the last releaser's
 * free path (which drops sc_lock at autofs.c:397 before cancel/drain/
 * put at :398-400) runs without it.
 */
static volatile int	g_hold_stop;

static void
hold_fn(void *arg)
{
	int delta = (int)(intptr_t)arg - ticks;

	while (delta > 0) {
		tsleep(&g_hold_stop, 0, "df3173hw", delta);
		delta = (int)(intptr_t)arg - ticks;
	}
	mtx_lock_ex_quick(&autofs_softc->sc_lock);
	kprintf("DF3173:   HOLD sc_lock at tick %d\n", ticks);
	delta = g_hold_stop - ticks;
	while ((int)(ticks - g_hold_stop) < 0) {
		delta = g_hold_stop - ticks;
		tsleep(&g_hold_stop, 0, "df3173hl", delta > 0 ? delta : 1);
	}
	mtx_unlock_ex(&autofs_softc->sc_lock);
	kprintf("DF3173:   RELEASE sc_lock at tick %d\n", ticks);
	kthread_exit();
}

/*
 * Mode B marker getters: one pinned per cpu; the freed request lands in
 * the last releaser's per-cpu magazine, so the getter pinned to that
 * cpu re-gets the exact object and observes post-free writes.
 */
static volatile int		g_get_gen;
static struct autofs_request	*g_mark_obj[MAXCPU];
static volatile int		g_mark_e0[MAXCPU], g_mark_e1[MAXCPU];
static volatile int		g_mark_done[MAXCPU];
static volatile int		g_mark_same[MAXCPU];

static void
getter_fn(void *arg)
{
	int id = (int)(intptr_t)arg;
	int mygen = 0;

	for (;;) {
		struct autofs_request *o;

		while (g_get_gen == mygen)
			tsleep(&g_get_gen, 0, "df3173g", 1);
		mygen = g_get_gen;

		o = objcache_get(autofs_request_objcache, M_WAITOK);
		g_mark_same[id] = (o == g_ar_last);
		g_mark_e0[id] = o->ar_error;
		memset(o->ar_path, 'Z', 16);
		o->ar_path[16] = 0;
		g_mark_obj[id] = o;
		kprintf("DF3173:   MARKER[%d] obj=%p (request was %p) %s "
		    "e0=%d\n", id, o, g_ar_last,
		    g_mark_same[id] ? "SAME-BLOCK" : "different-block",
		    g_mark_e0[id]);

		tsleep(&g_mark_obj[id], 0, "df3173m", hz / 5);
		g_mark_e1[id] = o->ar_error;
		g_mark_done[id] = o->ar_done;
		kprintf("DF3173:   MARKER[%d] readback e=%d done=%d "
		    "%s\n", id, g_mark_e1[id], g_mark_done[id],
		    (g_mark_same[id] && g_mark_e0[id] == 0 &&
		     g_mark_e1[id] == ETIMEDOUT) ?
		    "<== WRITE-AFTER-FREE (ar_error 0->ETIMEDOUT)" :
		    (g_mark_same[id] && g_mark_e1[id] == ETIMEDOUT) ?
		    "<== WRITE-AFTER-FREE (e0 already 145 at get)" :
		    "(no post-get write this round)");
		wakeup(&g_mark_obj[id]);
	}
}

static void
grab_request_info(const char *key)
{
	struct autofs_request *ar;

	g_ar_seen = NULL;
	g_ar_id_seen = -1;
	g_ar_q = NULL;
	mtx_lock_ex_quick(&autofs_softc->sc_lock);
	TAILQ_FOREACH(ar, &autofs_softc->sc_requests, ar_next) {
		if (strcmp(ar->ar_key, key) == 0) {
			g_ar_seen = ar;
			g_ar_last = ar;
			g_ar_id_seen = ar->ar_id;
			/* the queue the timeout task is armed on */
			g_ar_q = ar->ar_task.t.ta_queue;
			break;
		}
	}
	mtx_unlock_ex(&autofs_softc->sc_lock);
}

static int
qcpu(struct taskqueue *q)
{
	int i;

	for (i = 0; i < ncpus; i++) {
		if (q == taskqueue_thread[i])
			return (i);
	}
	return (-1);
}

/* sample the round request's state for the choreography debug log */
static void
sample_request(const char *tag, int round)
{
	struct autofs_request *ar = NULL;
	int ref = -1, done = -1, prog = -1, id = -1, pend = -1, qc = -2;
	int cflags = -1;

	mtx_lock_ex_quick(&autofs_softc->sc_lock);
	TAILQ_FOREACH(ar, &autofs_softc->sc_requests, ar_next) {
		if (strcmp(ar->ar_key, g_key) == 0) {
			id = ar->ar_id;
			ref = ar->ar_refcount;
			done = ar->ar_done;
			prog = ar->ar_in_progress;
			pend = ar->ar_task.t.ta_pending;
			qc = qcpu(ar->ar_task.t.ta_queue);
			cflags = ar->ar_task.f;
			break;
		}
	}
	mtx_unlock_ex(&autofs_softc->sc_lock);
	kprintf("DF3173:   R%02d %s req=%s id=%d ref=%d done=%d "
	    "inprog=%d ta_pending=%d tq=cpu%d cflags=%x\n", round, tag,
	    ar ? "alive" : "GONE", id, ref, done, prog, pend, qc, cflags);
}

static void
orchestrator_fn(void *arg)
{
	thread_t t;
	int round, hits, crossc, i;

	while (g_mode == 0)
		tsleep(&g_mode, 0, "df3173idle", hz / 10);

	kprintf("DF3173: mode=%d starting (%s)\n", g_mode,
	    g_mode == 1 ? "PANIC variant: foreign-queue STAILQ_REMOVE"
			: "UAF variant: wrong-queue drain frees running task");

	hits = crossc = 0;
	for (round = 1; round <= 40 && g_mode; round++) {
		ksnprintf(g_key, sizeof(g_key), "k3173_r%03d", round);

		/* publish round; the script stats the creator then waits
		 * for stage 1 to stat the releaser */
		strlcpy(g_pub_key, g_key, sizeof(g_pub_key));
		g_pub_round = round;
		g_stage = 0;
		g_pub_stage = 0;
		g_round = round;
		wakeup(&g_round);

		for (i = 0; i < 500; i++) {	/* wait for creator request
						 * (long: syncs with the
						 * script's poll loop) */
			tsleep(&g_round, 0, "df3173o", 1);
			grab_request_info(g_key);
			if (g_ar_seen != NULL)
				break;
		}
		if (g_ar_seen == NULL) {
			kprintf("DF3173: R%02d creator never created?!\n",
			    round);
			continue;
		}
		g_stage = 1;
		g_pub_stage = 1;
		kprintf("DF3173: R%02d T0 creator=userstat id=%d ar=%p "
		    "home_tq=cpu%d-queue key=%s\n", round, g_ar_id_seen,
		    g_ar_seen, qcpu(g_ar_q), g_key);
		sample_request("t0.0", round);

		/*
		 * T0+0.60: sleeper task on the HOME queue (the creator's):
		 * occupies the home runner so the 1s callout's task stays
		 * QUEUED (ta_pending=1).  mode A: runner busy until
		 * T0+1.30; mode B: T0+1.06 (task then dequeued and parked
		 * on sc_lock by the hold thread).
		 */
		tsleep(&g_round, 0, "df3173o", hz * 6 / 10);
		sample_request("t0.6", round);
		g_sleep_upto = ticks + (g_mode == 1 ? 70 : 46);
		taskqueue_enqueue(g_ar_q, &g_sleep_task);

		if (g_mode == 2) {
			int hold_at = ticks + 42;	/* T0+1.02 */

			g_hold_stop = ticks + 75;	/* T0+1.35 */
			kthread_create_cpu(hold_fn,
			    (void *)(intptr_t)hold_at, &t, 2, "df3173hold");
		}

		/*
		 * Script SIGKILLs the creator at stat+0.955 (EINTR exit,
		 * refcount 2->1).  Daemon AUTOFSDONE at T0+1.05 (mode 1)
		 * / T0+1.075 (mode 2) wakes the releaser stat, which is
		 * then the LAST releaser and frees the request on ITS
		 * cpu (5/6 chance of differing from the creator's).
		 */

		/* wait for the request to leave the TAILQ (freed);
		 * fine-grained sampling through the boundary window */
		for (i = 0; i < 160; i++) {
			tsleep(&g_round, 0, "df3173o", 1);
			if (i % 5 == 0)
				sample_request("w", round);
			grab_request_info(g_key);
			if (g_ar_seen == NULL)
				break;
		}
		sample_request("post", round);

		{
			int j;

			for (j = 0; j < ncpus; j++) {
				g_mark_same[j] = 0;
				g_mark_e1[j] = 0;
			}
			g_get_gen++;
			wakeup(&g_get_gen);
			tsleep(&g_round, 0, "df3173o", hz * 3 / 10);
			for (j = 0; j < ncpus; j++) {
				if (g_mark_same[j])
					kprintf("DF3173:   R%02d freed-object "
					    "resurfaced on cpu%d magazine "
					    "(home=cpu%d)%s\n", round, j,
					    qcpu(g_ar_q),
					    j != qcpu(g_ar_q) ?
					    "  <== CROSS-CPU FREE" : "");
				if (g_mark_same[j] &&
				    g_mark_e1[j] == ETIMEDOUT)
					hits++;
			}
			if (g_ar_seen == NULL)
				crossc++;
		}
		kprintf("DF3173: R%02d complete (request freed=%s)\n",
		    round, g_ar_seen == NULL ? "yes" : "NO (still queued?)");

		tsleep(&g_round, 0, "df3173o", hz);
	}

	kprintf("DF3173: finished %d rounds; freed-rounds=%d; uaf_marks=%d\n",
	    round - 1, crossc, hits);
	kprintf("DF3173: %s\n", hits ? "UAF CONFIRMED" :
	    "no marker observed (see VERDICT.md)");
	kthread_exit();
}

static int
df3173_load(struct module *m, int what, void *arg)
{
	static thread_t ggets[MAXCPU];
	static thread_t orch;
	int error, ci;

	switch (what) {
	case MOD_LOAD:
		{
			struct mount *mp;

			TAILQ_FOREACH(mp, &mountlist, mnt_list) {
				if (strncmp(mp->mnt_stat.f_fstypename,
				    "autofs", MFSNAMELEN) == 0)
					break;
			}
			if (mp == NULL) {
				kprintf("DF3173: no autofs mount "
				    "(run run.sh first)\n");
				return (ENODEV);
			}
			g_amp = VFSTOAUTOFS(mp);
			g_root = g_amp->am_root;
			kprintf("DF3173: autofs mount %s amp=%p root=%p\n",
			    mp->mnt_stat.f_mntonname, g_amp, g_root);
		}

		error = 0;
		for (ci = 0; ci < ncpus; ci++) {
			char name[24];

			ksnprintf(name, sizeof(name), "df3173g%d", ci);
			error |= kthread_create_cpu(getter_fn,
			    (void *)(intptr_t)ci, &ggets[ci], ci, "%s",
			    name);
		}
		error |= kthread_create(orchestrator_fn, NULL, &orch,
		    "df3173orch");
		if (error) {
			kprintf("DF3173: kthread_create failed %d\n", error);
			return (error);
		}
		kprintf("DF3173: harness loaded; "
		    "sysctl debug.df3173_mode=1|2 to start\n");
		return (0);
	case MOD_UNLOAD:
		/*
		 * Harness kthreads live in this module's text; refuse to
		 * unload so the module can never be freed under them
		 * (reset the guest between runs instead).
		 */
		return (EBUSY);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t df3173_mod = {
	"df3173_race",
	df3173_load,
	NULL
};

DECLARE_MODULE(df3173_race, df3173_mod, SI_SUB_PSEUDO, SI_ORDER_ANY);
MODULE_DEPEND(df3173_race, autofs, 1, 1, 1);
