mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* usb: devio: usbdev_release() has no barrier against an  in-flight async_completed() callback (use-after-free window on  struct usb_dev_state)gregkh@linuxfoundation.org
@ 2026-10-27 12:35 徐晗翔
  2026-10-27 14:12 ` Greg KH
  0 siblings, 1 reply; 2+ messages in thread
From: 徐晗翔 @ 2026-10-27 12:35 UTC (permalink / raw)
  To: gregkh; +Cc: linux-usb, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 8699 bytes --]

usbdev_release() ends with an unconditional kfree(ps) while an
async_completed() callback that has already moved its async request to
the completed list can still be running inside the lock-dropped window
of cancel_bulk_urbs() -- when the two overlap, the callback re-locks,
rescans the pending list and wakes the wait queue of freed memory.
The interleaving itself is proven reachable on a pristine Linux 7.3
(600/600 controlled window openings, with usbdev_release() entering
the window 445 microseconds after it opened); in the HCD shape I
tested (xHCI, single MSI, HCD_BH), the kfree() is nevertheless held
back by an indirect barrier -- usb_kill_urb() on the sibling URB waits
for a giveback that shares the same per-CPU BH worker with the
stalled callback -- so I am reporting this as a structurally proven
window whose final reachability depends on the HCD's completion
topology, with the open shapes listed below, not as an
immediately-triggerable crash.

The root cause is a one-way gap in the cleanup protocol: the
destroy_async() family only waits (via usb_kill_urb) for URBs it finds
on the pending list, but the moment async_completed() moves an async
to the completed list (devio.c:634) that URB escapes every destroy
traversal, and usb_kill_urb()'s wait condition (use_count == 0) only
covers the URB's own callback -- a callback that has started and not
yet returned is waited for by nobody.  That callback still touches ps
at four points after dropping ps->lock inside cancel_bulk_urbs()
(spin_unlock :614 -> usb_unlink_urb() -> spin_lock :617, then
wake_up(&ps->wait) :653 and the final unlock :654), while
usbdev_release() has no wait_event/completion of any kind between its
recovery loop and kfree(ps) (:1126).  The cleaner does not drain and
the callback holds no reference: both barriers are missing.  The
premises are ordinary: any usbfs-accessible device (root / usbfs
group / container with /dev/bus/usb) with a bulk endpoint that can
error-complete -- a plain usb-storage gadget STALLing an invalid CBW
is enough (verified: -EPIPE) -- and two threads on one fd.

The detailed description:
* Test environment / affected version
  Linux version 7.3.0 (gcc 13.3.0, GNU ld 2.42) #1 SMP PREEMPT_DYNAMIC
  -- vanilla mainline 7.3 release tarball (kernel.org), KASAN enabled,
  source unmodified.  QEMU/KVM x86_64 (4 vCPU), qemu-xhci + usb-storage
  (BOT bulk-in 0x81 / bulk-out 0x02).  The reproducer (attached) is a
  userspace hammer plus a kprobe window amplifier; neither modifies
  the kernel.  The involved code shape has been in place since the
  2.6 era; I found no upstream fix or matching public report for this
  window (syzbot's usbfs UAF history concerns other objects).

* Mechanism
  1. A bulk URB (B0) completes with an error (e.g. -EPIPE).
     async_completed() runs in the controller's completion context,
     takes ps->lock, moves B0's async to ps->async_completed (:634)
     -- from this point no destroy-family traversal can see B0 -- and,
     for a negative status with a bulk address set, calls
     cancel_bulk_urbs() (:649-651).
  2. cancel_bulk_urbs() marks the pending CONTINUATION URB (B1),
     takes a URB reference, then drops ps->lock to call
     usb_unlink_urb(B1) and re-locks (:609-618).  usb_unlink_urb() is
     purely asynchronous -- it does not wait for B1's callback.
  3. Concurrently, close(fd) runs usbdev_release(): releaseintf ->
     destroy_async_on_interface -> destroy_async -> usb_kill_urb(B1)
     (kills what is still pending -- B0 is not), then the recovery
     loop frees the completed B0 (kfree(as), transfer/setup buffers)
     and the function ends with kfree(ps) (:1126).  No code waits for
     an already-started callback.
  4. If kfree(ps) lands inside the callback's lock-dropped window, the
     callback's re-lock (:617 atomic write on freed slab memory), its
     pending-list rescan (:6f), wake_up(&ps->wait) (:653) and final
     unlock (:654) all operate on freed memory.  On slab reuse,
     __wake_up_common() interprets reused content as wait queue
     entries and calls their func pointer.
  Why ps is the only victim (why this has stayed hidden): during the
  callback the URB holds two references (submit's get at hcd.c:1524,
  released only at giveback return, hcd.c:1671), so the release-side
  usb_free_urb() merely drops it to one and all of giveback's own
  tail accesses stay safe; the callback copies as->status/signr/pid/
  cred to locals under the lock and touches only those after; the
  buffers are only read while the callback still holds ps->lock,
  which the release-side free path must acquire first.  The one
  object used exactly in the unprotected interval -- and without a
  reference count -- is ps itself.

* Evidence on pristine 7.3, and the honest limitation
  - Premise: submitting a 31-byte invalid CBW to the usb-storage
    bulk-out endpoint makes the device STALL it: B0 completes with
    status -32 (-EPIPE).  A generic storage device is enough; no
    custom gadget required.
  - Negative control: with B1 submitted WITHOUT
    USBDEVFS_URB_BULK_CONTINUATION, cancel_bulk_urbs() marks nothing
    and the window never opens -- 0/1000 rounds with the amplifier
    loaded, zero splats.  The window is exactly the audited
    continuation path.
  - Controlled window (kprobe stalls the callback at the
    usb_unlink_urb call site, return-address-filtered to
    async_completed+0x485, for 5 ms): the window opened in 600/600
    rounds, and usbdev_release() entered 445 us after the window
    opened -- the audited interleaving (release racing an in-flight
    callback that has already left the pending list) is real.
  - The limitation, measured: across 422 valid paired windows,
    usbdev_release() never completed inside the 5 ms window
    (release-return minus window-open: min 5.931 / median 8.016 /
    max 15.413 ms; 0/422); natural mode, no amplifier, 20000 rounds:
    zero splats.  The cause is an indirect barrier specific to the
    tested completion topology: usb_kill_urb(B1) in the release path
    waits for B1's giveback, and with xHCI single-MSI + HCD_BH, B0's
    and B1's completions are batched on the same per-CPU BH worker
    (hcd.c giveback batching) -- the stalled B0 callback blocks B1's
    giveback, so the kill waits and drags the release past the
    window.  This serialization is incidental, not designed.
  The window therefore needs one additional condition: the marked
  CONTINUATION URB's completion path must be independent of the
  stalled callback's execution context.  Shapes that plausibly
  satisfy it (open questions, listed for review rather than claimed):
  1. HCDs without HCD_BH whose urb_dequeue gives back synchronously
     in the killer's context (dummy_hcd / UHCI/OHCI class);
  2. xhci_urb_dequeue()'s synchronous giveback branches
     (td_on_ring() failure, device vdev removed, EP_CLEARING_TT;
     xhci.c:1856-1860);
  3. multi-MSI/MSI-X vectors or IRQ routing placing B0's and B1's
     completions on different CPUs' workers.
  (vCPU deschedule, the classic amplifier, only lengthens the
  callback-side window; it cannot shorten the release-side kill wait,
  so alone it does not break the barrier shape above.)


* Reproducer (attached)
  usbdevrace.c -- userspace hammer, one round = open / disconnect
  kernel driver / claim / CLEAR_HALT / submit B0 (invalid CBW,
  flags=0) + B1 (CONTINUATION) / delay / close(fd).  Modes: nat
  (continuation on), ctl (continuation off, negative control), errchk
  (single-shot premise check printing B0's status).  Build: gcc
  -static -O2 -o usbdevrace usbdevrace.c; usage:
  ./usbdevrace nat 600 300 100.
  usbdev_stall.c -- kprobe amplifier: stalls the callback at the
  usb_unlink_urb call site (mode=1, ret_off=0x485 for my build,
  recomputable; mode=2 targets the re-lock site) for stall_us, with a
  kretprobe on usbdev_release producing the window-open/release
  timing line.  Build as an external module; insmod usbdev_stall.ko
  stall_us=5000 max_hits=2000.
  My sequence was: errchk (premise) -> ctl 1000 (negative) -> nat 600
  with the amplifier (window + timing) -> nat 20000 without (natural).


* Impact
  If the additional condition holds: atomic spinlock writes, list
  reads and waitqueue wakeups on freed slab memory; on reuse a
  controlled-content func pointer call is conceivable, otherwise
  random corruption/oops.  The permission premise is usbfs node
  access (root / usbfs group / container mount), so this is a
  within-boundary memory-safety defect, not a privilege escape.
  Without the condition it is a latent, timing-engineered race --
  which is presumably why it has never surfaced.



Regards,
Hanxiang Xu
xuhx@hust.edu.cn



[-- Attachment #2: usbdev_stall.c --]
[-- Type: text/plain, Size: 6089 bytes --]

// SPDX-License-Identifier: GPL-2.0
/*
 * usbdev_stall.c - controlled-window instrument for the devio Case-3 race
 * (usbdev_release vs in-flight async_completed / cancel_bulk_urbs).
 *
 * The audited window is inside async_completed() (cancel_bulk_urbs inlined):
 *
 *   call _raw_spin_unlock   @ async_completed+0x478   (devio.c:614)
 *   call usb_unlink_urb     @ async_completed+0x480   (devio.c:615)  <- window
 *   call usb_free_urb       @ async_completed+0x488   (devio.c:616)
 *   call _raw_spin_lock     @ async_completed+0x491   (devio.c:617)  <- UAF access
 *
 * (offsets measured on verification/build-kasan-7.3/vmlinux, KASAN build;
 *  cancel_bulk_urbs is inlined into async_completed in this build)
 *
 * mode A (default): kprobe on usb_unlink_urb, pre_handler matches the return
 *   address == async_completed_base + ret_off (0x485) and busy-waits
 *   stall_us microseconds. This places the stall INSIDE the drop-lock window
 *   (after spin_unlock, before usb_unlink_urb executes), which is the exact
 *   window analysed in the audit report section 5.4 steps 6c-6e.
 *
 * mode B: kprobe on _raw_spin_lock, same ra filter with ret_off=0x496,
 *   stalling after unlink has executed but before the re-lock (report step
 *   6e). _raw_spin_lock is hot; the ra filter makes it cheap enough for the
 *   idle initramfs workload.
 *
 * Both modes print ktime timestamps on every hit. A kretprobe on
 * usbdev_release prints entry/return timestamps so the serial log yields a
 * single readable timeline: if release returns while a stall hit is in
 * flight, kfree(ps) happened inside the window and the next ps access from
 * the callback is the audited UAF.
 */
#define pr_fmt(fmt) "USBDEVSTALL: " fmt

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/ktime.h>
#include <linux/delay.h>
#include <linux/atomic.h>

static int stall_us = 5000;
module_param(stall_us, int, 0600);
MODULE_PARM_DESC(stall_us, "busy-wait length at the matched call site");

static int mode = 1;
module_param(mode, int, 0600);
MODULE_PARM_DESC(mode, "1=stall at usb_unlink_urb call site (0x485), 2=stall at re-lock site (0x496)");

static int max_hits = 2000;
module_param(max_hits, int, 0600);
MODULE_PARM_DESC(max_hits, "stop stalling after this many hits (0=unlimited)");

static ulong ret_off = 0x485;
module_param(ret_off, ulong, 0600);
MODULE_PARM_DESC(ret_off, "return-address offset inside async_completed");

static ulong ret_off_b = 0x496;
module_param(ret_off_b, ulong, 0600);

static unsigned long ac_base;
static atomic_t hits = ATOMIC_INIT(0);
static atomic_t near = ATOMIC_INIT(0);

static int stall_pre(struct kprobe *p, struct pt_regs *regs)
{
	unsigned long ra = *(unsigned long *)regs->sp;
	unsigned long want = ac_base + ret_off - 4;	/* kprobe base skips ENDBR */
	int h;

	if (ra == want) {
		h = atomic_inc_return(&hits);
		if (max_hits && h > max_hits)
			return 0;
		pr_warn("hit#%d t=%llu stall_us=%d ra=%lx (base+0x%lx)\n",
			h, ktime_get_ns(), stall_us, ra, ra - ac_base);
		udelay(stall_us);
		return 0;
	}
	if (ra > ac_base - 16 && ra < ac_base + 0x900) {
		h = atomic_inc_return(&near);
		if (h <= 8)
			pr_warn("near#%d ra=base+0x%lx (want base+0x%lx)\n",
				h, ra - ac_base, ret_off - 4);
	}
	return 0;
}

/* second probe for mode 2: raw_spin_lock, filtered the same way */
static int stall_pre_b(struct kprobe *p, struct pt_regs *regs)
{
	unsigned long ra = *(unsigned long *)regs->sp;
	unsigned long want = ac_base + ret_off_b - 4;	/* kprobe base skips ENDBR */
	int h;

	if (ra != want)
		return 0;

	h = atomic_inc_return(&hits);
	if (max_hits && h > max_hits)
		return 0;

	pr_warn("hit#%d(relock) t=%llu stall_us=%d\n",
		h, ktime_get_ns(), stall_us);
	udelay(stall_us);
	return 0;
}

static struct kprobe kp_unlink = {
	.symbol_name = "usb_unlink_urb",
	.pre_handler = stall_pre,
};

static struct kprobe kp_lock = {
	.symbol_name = "_raw_spin_lock",
	.pre_handler = stall_pre_b,
};

static struct kretprobe kr_release = {
	.handler = NULL,	/* set below */
	.entry_handler = NULL,
};

static int rel_entry(struct kretprobe_instance *ri, struct pt_regs *regs)
{
	pr_info("usbdev_release enter t=%llu pid=%d\n",
		ktime_get_ns(), current->pid);
	return 0;
}

static int rel_ret(struct kretprobe_instance *ri, struct pt_regs *regs)
{
	pr_info("usbdev_release ret  t=%llu pid=%d\n",
		ktime_get_ns(), current->pid);
	return 0;
}

static unsigned long sym_addr(const char *name)
{
	struct kprobe probe = { .symbol_name = name, };
	unsigned long addr = 0;

	if (register_kprobe(&probe) == 0) {
		addr = (unsigned long)probe.addr;
		unregister_kprobe(&probe);
	}
	return addr;
}

static int __init usbdev_stall_init(void)
{
	int ret;

	ac_base = sym_addr("async_completed");
	if (!ac_base) {
		pr_err("cannot resolve async_completed\n");
		return -EINVAL;
	}
	pr_info("async_completed base=%lx (+0x%lx unlink call, +0x%lx relock call)\n",
		ac_base, ret_off, ret_off_b);

	if (mode == 2) {
		ret = register_kprobe(&kp_lock);
		if (ret) {
			pr_err("register _raw_spin_lock probe failed: %d\n", ret);
			return ret;
		}
	} else {
		ret = register_kprobe(&kp_unlink);
		if (ret) {
			pr_err("register usb_unlink_urb probe failed: %d\n", ret);
			return ret;
		}
	}

	kr_release.kp.symbol_name = "usbdev_release";
	kr_release.entry_handler = rel_entry;
	kr_release.handler = rel_ret;
	ret = register_kretprobe(&kr_release);
	if (ret)
		pr_warn("kretprobe on usbdev_release failed: %d (timeline degraded)\n", ret);

	pr_info("armed mode=%d stall_us=%d ret_off=0x%lx max_hits=%d\n",
		mode, stall_us, mode == 2 ? ret_off_b : ret_off, max_hits);
	return 0;
}

static void __exit usbdev_stall_exit(void)
{
	if (mode == 2)
		unregister_kprobe(&kp_lock);
	else
		unregister_kprobe(&kp_unlink);
	unregister_kretprobe(&kr_release);
	pr_info("removed hits=%d near_miss=%d\n",
		atomic_read(&hits), atomic_read(&near));
}

module_init(usbdev_stall_init);
module_exit(usbdev_stall_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("controlled-window kprobe stall for devio usbdev_release race verification");

[-- Attachment #3: usbdevrace.c --]
[-- Type: text/plain, Size: 8425 bytes --]

// 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 <nat|ctl> [rounds] [close_delay_us] [verbose_every]
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <dirent.h>
#include <sys/ioctl.h>
#include <linux/usbdevice_fs.h>
#include <linux/usb/ch9.h>

#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;
}

^ permalink raw reply	[flat|nested] 2+ messages in thread

* Re: usb: devio: usbdev_release() has no barrier against an in-flight async_completed() callback (use-after-free window on  struct usb_dev_state)gregkh@linuxfoundation.org
  2026-10-27 12:35 usb: devio: usbdev_release() has no barrier against an in-flight async_completed() callback (use-after-free window on struct usb_dev_state)gregkh@linuxfoundation.org 徐晗翔
@ 2026-10-27 14:12 ` Greg KH
  0 siblings, 0 replies; 2+ messages in thread
From: Greg KH @ 2026-10-27 14:12 UTC (permalink / raw)
  To: 徐晗翔; +Cc: linux-usb, linux-kernel

On Tue, Oct 27, 2026 at 08:35:55PM +0800, 徐晗翔 wrote:
> usbdev_release() ends with an unconditional kfree(ps) while an
> async_completed() callback that has already moved its async request to
> the completed list can still be running inside the lock-dropped window
> of cancel_bulk_urbs() -- when the two overlap, the callback re-locks,
> rescans the pending list and wakes the wait queue of freed memory.
> The interleaving itself is proven reachable on a pristine Linux 7.3
> (600/600 controlled window openings, with usbdev_release() entering
> the window 445 microseconds after it opened); in the HCD shape I
> tested (xHCI, single MSI, HCD_BH), the kfree() is nevertheless held
> back by an indirect barrier -- usb_kill_urb() on the sibling URB waits
> for a giveback that shares the same per-CPU BH worker with the
> stalled callback -- so I am reporting this as a structurally proven
> window whose final reachability depends on the HCD's completion
> topology, with the open shapes listed below, not as an
> immediately-triggerable crash.

Cool, can you send a patch to resolve this as you obviously have a
reproducer?  Same tool that made the reproducer can make a working patch
and test it out.

thanks,

greg k-h

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-10-27 14:12 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-27 12:35 usb: devio: usbdev_release() has no barrier against an in-flight async_completed() callback (use-after-free window on struct usb_dev_state)gregkh@linuxfoundation.org 徐晗翔
2026-10-27 14:12 ` Greg KH

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®