usb: xhci: debugfs ring files stay readable after their rings are freed (remove and resume-reset orderings) -- UAF / NULL-deref on read

From: 徐晗翔

Date: Fri Oct 30 2026 - 07:48:49 EST


The xhci debugfs ring dump files (/sys/kernel/debug/usb/*/command-ring,
event-ring, devices/*/ep*) have unlocked show callbacks that walk the
ring's segment list, but in both teardown orderings the rings are
freed before the debugfs entries are removed: in xhci_stop(),
xhci_mem_cleanup() (xhci.c:732) runs before xhci_debugfs_exit()
(:733); in the resume reset_registers branch,
xhci_free_virt_devices_depth_first() (:1185-1186) runs before
xhci_debugfs_exit() (:1194). A read overlapping the teardown --
including through an fd opened before the removal, since debugfs
removal does not revoke open files -- then dereferences freed memory:
a NULL-deref for the command ring (cmd_ring is NULLed at
xhci-mem.c:1947) and use-after-free for the event ring and endpoint
rings. I confirmed it both ways on a pristine Linux 7.3 KASAN kernel:
with the teardown held between mem_cleanup and debugfs_exit, cat-ing
the files produced 45 reports -- null-ptr-deref in
xhci_ring_trb_show+0x2c (task cat) and slab-use-after-free in
xhci_ring_dump_segment; and with no instrumentation at all, a plain
unbind/bind loop plus a tight cat loop produced 41
xhci_ring_dump_segment slab-use-after-free reports -- naturally
reachable, no timing tricks. The resume variant is repeatable on
real hardware via suspend/resume cycles that hit a host reset error
(STS_SRE/STS_HCE). The audience is whoever can read debugfs
(usually root): a local DoS with high determinism, low severity
otherwise.


The detailed description:

* The orderings and the unprotected readers (Linux 7.3)
- Remove: xhci_stop(): xhci_mem_cleanup() at xhci.c:732 frees the
command ring (NULLs it, xhci-mem.c:1947), the interrupters and
their event rings (:1936-1942, kfree of the interrupter object
:1862-1873) and all device rings (:1951-1953);
xhci_debugfs_exit() only at xhci.c:733.
- Resume reset: after STS_SRE/STS_HCE, the branch frees all virt
devices at xhci.c:1185-1186 and calls xhci_debugfs_exit() at
:1194 -- same inversion. (The debugfs tree is then rebuilt by
xhci_run() at :670, so the stale window recurs on every such
resume.)
- The show callbacks take no locks and dereference directly:
xhci_ring_enqueue/dequeue/cycle_show
(xhci-debugfs.c:166-195), xhci_ring_trb_show walking
first_seg (:216-225). The files' private data are addresses of
pointer fields (&xhci->cmd_ring, &interrupters[0]->event_ring),
so even reading the ring pointer itself is a UAF for the event
ring.
- debugfs removal does not close open fds (simple_recursive_
removal, fs/debugfs/inode.c:778-787) and single_open's show runs
per read (fs/seq_file.c:572+): any overlap works, no precise
timing needed.

* Evidence (pristine 7.3 KASAN, QEMU qemu-xhci; instrument attached)
Controlled (xh5stall.c parks the teardown at xhci_debugfs_exit()'s
entry, i.e. exactly the xhci.c:732->733 state; cat the files in
the window):
BUG: KASAN: null-ptr-deref in xhci_ring_trb_show+0x2c
Read of size 8 at 0 by task cat
cat -> vfs_read -> seq_read -> xhci_ring_trb_show
BUG: KASAN: slab-use-after-free in xhci_ring_dump_segment+0x26b
(freed 96-byte segment region; free side xhci_mem_cleanup)
Natural (no module at all): 150 unbind/bind rounds with a tight cat
loop -> 41 slab-use-after-free in xhci_ring_dump_segment by task
cat. So the race is hit by ordinary userspace without any
instrumentation.



Regards,
Hanxiang Xu
xuhx@xxxxxxxxxxx


// SPDX-License-Identifier: GPL-2.0
/*
* xh5stall.c - BUG-5 (xhci debugfs ring entries outlive xhci_mem_cleanup)
* controlled-window instrument.
*
* Audit case: drivers/usb/host manual review 2026-10-29, BUG-5.
*
* xhci_stop(): xhci_mem_cleanup() xhci.c:732
* -> xhci_ring_free(cmd_ring); cmd_ring = NULL
* -> xhci_free_interrupter(interrupters[0]); kfree(ir)
* xhci_debugfs_exit() xhci.c:733 <-- files removed HERE
*
* Between the two calls (and for any fd opened earlier), the debugfs ring
* files' show callbacks dereference the freed/NULL'd state:
* command-ring/trbs: ring = *(struct xhci_ring **)&xhci->cmd_ring == NULL
* -> ring->first_seg NULL deref oops
* event-ring/trbs: private data points INSIDE the kfree()d interrupter
* -> KASAN slab-UAF read, then wild ring deref
*
* This module stalls the teardown thread at xhci_debugfs_exit() entry with
* a busy-wait kprobe pre_handler; userspace cats the files during the stall.
* Kernel sources are NOT modified.
*/
#define pr_fmt(fmt) "XH5STALL: " fmt

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

static int stall_ms = 3000;
module_param(stall_ms, int, 0600);
MODULE_PARM_DESC(stall_ms, "busy-wait at xhci_debugfs_exit entry (mem_cleanup already done)");

static int hits_max = 6;
module_param(hits_max, int, 0600);

static atomic_t hits = ATOMIC_INIT(0);

static int stall_pre(struct kprobe *p, struct pt_regs *regs)
{
int h;

if (stall_ms <= 0)
return 0;
h = atomic_inc_return(&hits);
if (hits_max > 0 && h > hits_max)
return 0;
pr_warn("stall #%d: %d ms at xhci_debugfs_exit entry, xhci=%px - CAT THE RING FILES NOW\n",
h, stall_ms, (void *)regs->di);
{
u64 target = ktime_get_mono_fast_ns() + (u64)stall_ms * 1000000;

while (ktime_get_mono_fast_ns() < target)
cpu_relax();
}
pr_warn("stall resumed, debugfs teardown continues\n");
return 0;
}

static struct kprobe kp_exit = {
.symbol_name = "xhci_debugfs_exit",
.pre_handler = stall_pre,
};

static int __init xh5stall_init(void)
{
int ret = register_kprobe(&kp_exit);

if (ret) {
pr_err("register_kprobe(xhci_debugfs_exit) failed: %d\n", ret);
return ret;
}
pr_info("armed: stall %d ms at xhci_debugfs_exit (after mem_cleanup)\n",
stall_ms);
return 0;
}

static void __exit xh5stall_exit(void)
{
unregister_kprobe(&kp_exit);
pr_info("removed, stall hits=%d\n", atomic_read(&hits));
}

module_init(xh5stall_init);
module_exit(xh5stall_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("BUG-5 debugfs ring UAF controlled window");