/*
 * pbufres.c -- KLD harness proving the pfreecnt check-then-decrement race
 * in the pbuf allocator family of sys/vm/vm_pager.c (getpbuf / getpbuf_kva /
 * getpbuf_mem / trypbuf / trypbuf_kva).
 *
 * Mechanism under test (getpbuf_kva, sys/vm/vm_pager.c:462-498):
 *
 *      while (pfreecnt && *pfreecnt <= 0) {          <-- :463 gate, NO lock
 *              tsleep_interlock(pfreecnt, 0); ...
 *      }
 *      ...bucket scan...
 *      spin_lock(&bswspin_kva[iter]);                <-- per-BUCKET lock only
 *      TAILQ_REMOVE(...);
 *      atomic_add_int(&pbuf_kva_count, -1);
 *      if (pfreecnt)
 *              atomic_add_int(pfreecnt, -1);         <-- :489-490 decrement
 *
 * The <=0 gate and the decrement are not atomic with respect to each other
 * and the bucket spinlock differs per CPU (iter = mycpuid & BSWHMASK), so
 * N CPUs can simultaneously observe *pfreecnt == 1, each pass the gate, and
 * each decrement -- driving a subsystem reservation counter that is supposed
 * to cap concurrency at 1 to 1-N, and putting N pbufs in flight for a
 * reservation of 1.
 *
 * This mirrors exactly how in-tree counters are used (nsw_rcount,
 * nsw_wcount_sync/async, v_pbuf_count, mnt_pbuf_count, nfs/smbfs/vinum
 * counters) with small caps under unprivileged-triggerable I/O load.
 *
 * Proves:
 *   (1) vm.pbres_neg  > 0        : counter observed NEGATIVE (race hit)
 *   (2) vm.pbres_min  < 0        : lowest observed counter value
 *   (3) vm.pbres_maxinflight > 1 : more simultaneous pbuf holders than cap
 *
 * On a fixed kernel (atomic cmpxchg reservation) all three stay clean.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/kthread.h>
#include <sys/buf.h>
#include <sys/sysctl.h>
#include <sys/types.h>
#include <sys/time.h>

static int pbres_cnt = 1;		/* subsystem reservation, cap = 1 */
static int pbres_stop;
static int pbres_alive;
static int pbres_inflight;
static int pbres_neg;
static int pbres_min = 0x7fffffff;
static int pbres_maxinflight;
static int pbres_ncpu;
static struct thread *pbres_td[MAXCPU];

SYSCTL_INT(_vm, OID_AUTO, pbres_neg, CTLFLAG_RD, &pbres_neg, 0,
    "observations of reservation counter < 0");
SYSCTL_INT(_vm, OID_AUTO, pbres_min, CTLFLAG_RD, &pbres_min, 0,
    "minimum observed reservation counter value");
SYSCTL_INT(_vm, OID_AUTO, pbres_maxinflight, CTLFLAG_RD, &pbres_maxinflight, 0,
    "max simultaneous pbuf holders (cap is 1)");
SYSCTL_INT(_vm, OID_AUTO, pbres_alive, CTLFLAG_RD, &pbres_alive, 0,
    "threads still running");
SYSCTL_INT(_vm, OID_AUTO, pbres_inflight, CTLFLAG_RD, &pbres_inflight, 0,
    "pbufs currently held by harness");

/*
 * Rescue: bump the reservation counter pbuf_adjcount()-style so sleepers
 * gated by the (known-buggy, DF-0953) wake thresholds can be released for
 * a clean module unload.  Purely harness plumbing.
 */
static int
pbres_bump_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, val = 0;

	error = sysctl_handle_int(oidp, &val, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (val > 0) {
		atomic_add_int(&pbres_cnt, val);
		wakeup(&pbres_cnt);
		kprintf("PBUFRES: bumped reservation by %d -> %d\n",
			val, pbres_cnt);
	}
	return (0);
}
SYSCTL_PROC(_vm, OID_AUTO, pbres_bump, CTLTYPE_INT | CTLFLAG_RW, NULL, 0,
    pbres_bump_sysctl, "I",
    "rescue: add to the reservation counter and wake sleepers");

#define	RUN_SECONDS	10

static void
pbres_thread(void *arg __unused)
{
	int start = ticks;

	while (!pbres_stop && (ticks - start) < RUN_SECONDS * hz) {
		struct buf *bp;
		int c, infl;

		/*
		 * THE RACY CALL: gate at vm_pager.c:463, decrement at :489.
		 */
		bp = getpbuf_kva(&pbres_cnt);

		infl = atomic_fetchadd_int(&pbres_inflight, 1) + 1;
		if (infl > pbres_maxinflight)
			pbres_maxinflight = infl;

		c = pbres_cnt;
		if (c < 0) {
			if (atomic_fetchadd_int(&pbres_neg, 1) == 0)
				kprintf("PBUFRES: counter NEGATIVE (%d), "
					"cap=1, inflight=%d\n", c, infl);
		}
		if (c < pbres_min)
			pbres_min = c;

		atomic_add_int(&pbres_inflight, -1);
		relpbuf(bp, &pbres_cnt);
	}
	atomic_add_int(&pbres_alive, -1);
	wakeup(&pbres_alive);
	kthread_exit();
}

static int
pbres_modevent(module_t mod __unused, int type, void *data __unused)
{
	int i, error;

	switch (type) {
	case MOD_LOAD:
		pbres_ncpu = ncpus;
		kprintf("PBUFRES: starting %d threads (cap=1) for %d s\n",
		    pbres_ncpu, RUN_SECONDS);
		for (i = 0; i < pbres_ncpu; ++i) {
			error = kthread_create_cpu(pbres_thread, NULL,
						   &pbres_td[i], i,
						   "pbres%d", i);
			if (error)
				kprintf("PBUFRES: kthread cpu%d failed %d\n",
					i, error);
			else
				atomic_add_int(&pbres_alive, 1);
		}
		break;
	case MOD_UNLOAD:
		pbres_stop = 1;
		while (pbres_alive)
			tsleep(&pbres_alive, 0, "pbresw", hz / 10);
		kprintf("PBUFRES: done. neg=%d min=%d maxinflight=%d\n",
			pbres_neg, pbres_min, pbres_maxinflight);
		break;
	default:
		return (EOPNOTSUPP);
	}
	return (0);
}

static moduledata_t pbres_mod = { "pbufres", pbres_modevent, NULL };
DECLARE_MODULE(pbufres, pbres_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
MODULE_VERSION(pbufres, 1);
