mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* usb: xhci: unlocked secondary-interrupter/virt-device teardown  vs sideband API users -- use-after-free/double-free (remove and  resume-reset paths; needs CONFIG_XHCI_SIDEBAND)
@ 2026-10-30 11:41 徐晗翔
  2026-10-30 12:02 ` Greg KH
  0 siblings, 1 reply; 2+ messages in thread
From: 徐晗翔 @ 2026-10-30 11:41 UTC (permalink / raw)
  To: mathias.nyman, gregkh; +Cc: linux-kernel, linux-usb

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

xhci frees its interrupter[] and devs[] software state on two paths
that hold no xhci->lock -- xhci_stop() -> xhci_mem_cleanup()
(xhci-mem.c:1928-2014), and the reset_registers branch of
xhci_resume() after a host reset error (STS_SRE/STS_HCE,
xhci.c:1152-1196, which runs xhci_free_virt_devices_depth_first()
synchronously on the resume path) -- while the sideband export API
consumed by offload clients (xhci_remove_secondary_interrupter(),
which unlinks under xhci->lock but FREES outside it;
xhci_stop_endpoint_sync(), which reads ep->ring / ep->vdev->out_ctx
under the lock) is only protected by that same lock.  An in-tree
client with asynchronous messages (the Qualcomm QMI audio-offload
driver, qc_audio_offload.c) can call these at any time, and its
entry check (chip->shutdown, set by snd-usb-audio's asynchronous
disconnect) cannot cover the window: in the remove variant the
disconnect ordering only partially helps, and in the resume-reset
variant the xHCI-side frees happen BEFORE hub_wq has disconnected
the audio device, so the entry check passes while the objects are
already gone.  I confirmed the core interleaving on a pristine
Linux 7.3 KASAN kernel by driving the exported API as a stand-in
client (create/remove secondary interrupter in a loop, matching the
in-tree client's call shape) against native sysfs unbind: 20
slab-use-after-free reports with the use stack hitting
xhci_remove_secondary_interrupter+0x2d and the free stack ending in
kfree <- xhci_pci_remove <- pci_device_remove <- unbind_store.

The detailed description:

* The two unlocked teardown paths vs the locked API surface
  - Remove: xhci_stop() (under xhci->mutex) calls xhci_mem_cleanup(),
    which walks interrupters[] (xhci-mem.c:1936-1942) and devs[]
    (:1951-1955), frees event rings/ERST/contexts and kfrees the
    arrays, NULLing them at :2003/:2011-2012 -- all without
    xhci->lock.
  - Resume reset: xhci_resume()'s STS_SRE/STS_HCE branch
    (xhci.c:1146) does xhci_cleanup_command_queue (:1179) and
    xhci_free_virt_devices_depth_first (:1185-1186) with no lock
    (the spinlock was dropped at :1172), then xhci_debugfs_exit
    (:1194).
  - The export side: xhci_remove_secondary_interrupter()
    (xhci-mem.c:1881-1906) validates and unlinks the interrupter
    under xhci->lock, then calls xhci_free_interrupter() AFTER
    unlocking; xhci_stop_endpoint_sync() (xhci.c:2907-2924) reads
    ep->ring and ep->vdev->out_ctx under the lock.  Neither takes
    xhci->mutex, so the mutex serializing mem_cleanup against the
    primary paths does not serialise these.

* Client reachability (the in-tree user)

  qc_audio_offload.c processes QMI requests from the ADSP in work
  context: the disable branch and error paths call
  xhci_sideband_remove_interrupter() (:1250, :1515, via
  handle_uaudio_stream_req :1545+); the disconnect callback notifies
  the ADSP, waits a bounded timeout (:372), then unregisters and
  kfrees the sideband (:1876-1915) without clearing dev->sb.  The
  entry check at :1595-1600 (!subs || !chip ||
  atomic_read(&chip->shutdown)) only stops messages arriving AFTER
  snd-usb-audio's disconnect completed:
  - Remove variant (A): usb_remove_hcd() disconnects the root hub
    before xhci_stop() frees the software state, so in-flight
    messages that passed the check race the frees: the interrupter
    is freed by both sides (double free of ERST/event ring/ir), or
    mem_cleanup's unlocked loop writes ERST registers of an
    interrupter already freed by the client.
  - Endpoint variant (B): the client's stop-endpoint path reads
    ep->ring under xhci->lock while mem_cleanup frees the rings and
    the containing virt device without the lock.
  - Resume variant (C, sharpest): on a resume reset error,
    xhci_free_virt_devices_depth_first() runs synchronously BEFORE
    hub_wq processes the lost-power disconnect -- chip->shutdown is
    still 0, the entry check passes, and the endpoint/interrupter
    accesses land on freed memory.  Host reset errors on real
    hardware are recurrent, so this is repeatable, not one-shot.

* Evidence (pristine 7.3 KASAN, QEMU qemu-xhci; instrument attached)
  sbxrace.c is a loadable stand-in client built on the exported,
  unconditionally-compiled EXPORT_SYMBOL_GPL API
  (xhci_create_secondary_interrupter /
  xhci_remove_secondary_interrupter), replaying the in-tree client's
  call shape while the native sysfs unbind runs the real teardown:
    BUG: KASAN: slab-use-after-free in
    xhci_remove_secondary_interrupter+0x2d       (20 reports)
    Read of size 8 ... by task sbxrace
    Allocated by task 1: __usb_create_hcd <- xhci_pci_common_probe
    Freed by task 1: kfree <- xhci_pci_remove <- pci_device_remove
                     <- unbind_store
  (a first run with a short client hold window had zero hits in 12
  rounds; widening the hold to ~3 ms hit within 40 rounds --
  consistent with the analysis: the race needs the API call to
  overlap the teardown window).  Fidelity note: the client side is a
  stand-in because the QMI peer (ADSP firmware) is not available;
  it uses the same exported entry points the real client uses, and
  the teardown side is entirely native.



Regards,
Hanxiang Xu
xuhx@hust.edu.cn



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

// SPDX-License-Identifier: GPL-2.0
/*
 * sbxrace.c - BUG-2 (xhci_mem_cleanup lockless free vs sideband secondary
 * interrupter teardown) driver.
 *
 * Audit case: drivers/usb/host manual review 2026-10-29, BUG-2 variant A/B.
 *
 *   CPU-A (unbind -> xhci_pci_remove -> usb_remove_hcd -> usb_stop_hcd
 *          -> xhci_pci_stop -> xhci_stop -> xhci_mem_cleanup):
 *       [NO xhci->lock] ir = interrupters[i];
 *                       xhci_remove_interrupter(ir); xhci_free_interrupter(ir)
 *   CPU-B (sideband client, e.g. QMI worker of qc_audio_offload):
 *       xhci_remove_secondary_interrupter(hcd, ir):
 *           spin_lock(xhci->lock); ...; interrupters[N]=NULL; spin_unlock
 *           xhci_free_interrupter(ir)   <-- lock-drop free
 *
 * Neither side's lock covers the other: double free / UAF on the interrupter,
 * its event ring and the ERST coherent memory.
 *
 * This module reproduces CPU-B using the very EXPORT_SYMBOL_GPL APIs a real
 * sideband client calls (xhci_create_secondary_interrupter /
 * xhci_remove_secondary_interrupter, both compiled in unconditionally in this
 * tree). The hcd pointer is captured by a kprobe on xhci_run() (every
 * successful bind re-probes and re-arms it). Userspace drives CPU-A with
 * unbind/bind loops on /sys/bus/pci/drivers/xhci_hcd.
 */
#define pr_fmt(fmt) "SBXRACE: " fmt

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/atomic.h>

/* opaque types: we only carry pointers */
struct usb_hcd;
struct xhci_interrupter;

extern struct xhci_interrupter *
xhci_create_secondary_interrupter(struct usb_hcd *hcd, unsigned int segs,
				  u32 imod_interval, unsigned int intr_num);
extern void xhci_remove_secondary_interrupter(struct usb_hcd *hcd,
					      struct xhci_interrupter *ir);

static int rounds = 2000;
module_param(rounds, int, 0600);
MODULE_PARM_DESC(rounds, "create/remove rounds the sideband kthread runs");

static int delay_us = 100;
module_param(delay_us, int, 0600);
MODULE_PARM_DESC(delay_us, "pause between create and remove (us)");

static struct usb_hcd *g_hcd;
static atomic_t g_hcd_seen = ATOMIC_INIT(0);

static int run_pre(struct kprobe *p, struct pt_regs *regs)
{
	/* xhci_run(struct usb_hcd *hcd): RDI */
	g_hcd = (struct usb_hcd *)regs->di;
	atomic_set(&g_hcd_seen, 1);
	pr_info("captured hcd=%px (bind #%d)\n", g_hcd,
		atomic_read(&g_hcd_seen));
	return 0;
}

static struct kprobe kp_run = {
	.symbol_name = "xhci_run",
	.pre_handler = run_pre,
};

static struct task_struct *sb_thread;
static atomic_t stop_req = ATOMIC_INIT(0);
static atomic_t n_create = ATOMIC_INIT(0);
static atomic_t n_remove = ATOMIC_INIT(0);
static atomic_t n_null = ATOMIC_INIT(0);

static int sb_loop(void *unused)
{
	int i;

	pr_info("sideband kthread up, waiting for hcd capture\n");
	while (!atomic_read(&g_hcd_seen) && !kthread_should_stop())
		msleep(20);
	pr_info("hcd=%px, starting %d create/remove rounds\n", g_hcd, rounds);

	for (i = 0; i < rounds && !atomic_read(&stop_req); i++) {
		struct xhci_interrupter *ir;

		/* re-read: rebind installs a fresh hcd */
		ir = xhci_create_secondary_interrupter(g_hcd, 1, 40000, 0);
		if (ir) {
			atomic_inc(&n_create);
			if (delay_us)
				udelay(delay_us);
			xhci_remove_secondary_interrupter(g_hcd, ir);
			atomic_inc(&n_remove);
		} else {
			atomic_inc(&n_null);
		}
		if ((i % 200) == 0 && i)
			pr_info("round %d: create=%d remove=%d null=%d\n",
				i, atomic_read(&n_create),
				atomic_read(&n_remove), atomic_read(&n_null));
	}
	pr_info("done: rounds=%d create=%d remove=%d null=%d\n",
		i, atomic_read(&n_create), atomic_read(&n_remove),
		atomic_read(&n_null));
	atomic_set(&stop_req, 2);
	return 0;
}

static int __init sbxrace_init(void)
{
	int ret;

	ret = register_kprobe(&kp_run);
	if (ret) {
		pr_err("register_kprobe(xhci_run) failed: %d\n", ret);
		return ret;
	}
	sb_thread = kthread_run(sb_loop, NULL, "sbxrace");
	if (IS_ERR(sb_thread)) {
		unregister_kprobe(&kp_run);
		return PTR_ERR(sb_thread);
	}
	pr_info("loaded (rounds=%d delay_us=%d); start unbind/bind loops now\n",
		rounds, delay_us);
	return 0;
}

static void __exit sbxrace_exit(void)
{
	atomic_set(&stop_req, 1);
	while (atomic_read(&stop_req) != 2)
		msleep(50);
	kthread_stop(sb_thread);
	unregister_kprobe(&kp_run);
	pr_info("removed\n");
}

module_init(sbxrace_init);
module_exit(sbxrace_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("BUG-2 sideband interrupter teardown race driver");

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

* Re: usb: xhci: unlocked secondary-interrupter/virt-device teardown vs sideband API users -- use-after-free/double-free (remove and resume-reset paths; needs CONFIG_XHCI_SIDEBAND)
  2026-10-30 11:41 usb: xhci: unlocked secondary-interrupter/virt-device teardown vs sideband API users -- use-after-free/double-free (remove and resume-reset paths; needs CONFIG_XHCI_SIDEBAND) 徐晗翔
@ 2026-10-30 12:02 ` Greg KH
  0 siblings, 0 replies; 2+ messages in thread
From: Greg KH @ 2026-10-30 12:02 UTC (permalink / raw)
  To: 徐晗翔; +Cc: mathias.nyman, linux-kernel, linux-usb

On Fri, Oct 30, 2026 at 07:41:05PM +0800, 徐晗翔 wrote:
> xhci frees its interrupter[] and devs[] software state on two paths
> that hold no xhci->lock -- xhci_stop() -> xhci_mem_cleanup()

<snip>

These are all pointless, sorry.

Please just submit working patches.  Doing fault injection of random
failures is an interesting research project, but doesn't do much for us
without a working patch to resolve the issue.  I've just dropped all of
these "reports" now.

thanks,

greg k-h

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

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

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-30 11:41 usb: xhci: unlocked secondary-interrupter/virt-device teardown vs sideband API users -- use-after-free/double-free (remove and resume-reset paths; needs CONFIG_XHCI_SIDEBAND) 徐晗翔
2026-10-30 12:02 ` 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®