* 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; 10+ 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] 10+ 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
2026-11-04 12:54 ` soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work) 徐晗翔
` (5 more replies)
0 siblings, 6 replies; 10+ 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] 10+ messages in thread
* soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work)
2026-10-27 14:12 ` Greg KH
@ 2026-11-04 12:54 ` 徐晗翔
2026-11-04 13:04 ` Greg KH
2026-11-04 12:57 ` usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove 徐晗翔
` (4 subsequent siblings)
5 siblings, 1 reply; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 12:54 UTC (permalink / raw)
To: andersson, konradybcio; +Cc: linux-arm-msm, linux-usb, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 5410 bytes --]
pmic_glink_altmode schedules two works from asynchronous glink/pdr
notifications -- the per-port alt_port->work (USBC/HPD state machine:
typec_mux_set/typec_switch_set/typec_retimer_set with 5-second ack
waits, pmic_glink_altmode_request() taking altmode->lock and
wait_for_completion_timeout(&altmode->pan_ack, 5*HZ), and
drm_aux_hpd_bridge_notify()) and the global enable_work (scheduled by
pmic_glink_altmode_pdr_notify() on ADSP service state changes) -- but
the driver has NO .remove and NO cancel_work_sync/flush anywhere in
the file: everything (the typec mux/switch/retimer handles, the
pmic_glink client registration, the struct pmic_glink_altmode) is
released by devm actions in reverse order while a queued or running
work is untouched by any of those steps. A USBC notification or a
pdr state transition in flight at unbind/rmmod/ADSP-restart time
therefore makes the worker run on freed handles, a freed client
binding and a freed altmode structure -- an in-tree use-after-free
with a window stretched to seconds by the 5 s ack wait. Notably the
ADSP restart (pdr down->up) is precisely the scenario that triggers
the auxiliary re-enumeration/teardown, so the enable_work instance's
reachability is as high as the port-work one. There is also a
non-UAF data race worth fixing in the same pass: the notify callback
(rpmsg/rx context) writes alt_port->orientation/svid/mode/hpd_state/
mux_ctrl and the 8-byte tbt_data struct copy, while the worker reads
the same fields with no common lock (altmode->lock is only taken
inside pmic_glink_altmode_request) -- KCSAN-visible, with tbt_data
tearable and worst case a transient wrong mux configuration.
The detailed description:
* Teardown sequence with nothing stopping the works (Linux 7.3)
No .remove (pmic_glink_altmode.c:709-713); on auxiliary unbind /
rmmod qcom_pmic_glink / ADSP restart re-enumeration, devm runs in
reverse: glink client release (pmic_glink.c:69-78; only list_del --
does not wait for driver-private works), aux-bridge unregister
(aux-bridge.c:82 devm action), then per-port
typec_switch_put/typec_retimer_put/typec_mux_put (:672-677/:657-662/
:642-647), then the devm kzalloc'd struct pmic_glink_altmode goes
last (:580).
Meanwhile instance 1, alt_port->work (worker :349-394): touches the
switch (:355), mux (:196/254/292/314/336), retimer (:204/262/300/
322/344) handles after their puts -- UAF; pmic_glink_altmode_request
(:393 -> :144-175) takes altmode->lock (:154) and waits up to 5 s on
pan_ack (:168) -- the window is seconds; drm_aux_hpd_bridge_notify
(:377) on a freed bridge.
Instance 2, enable_work: scheduled at :561 by pdr_notify
(pmic_glink.c:216-232 walks clients and calls client->pdr_notify
:230 -- the last batch before the client's list_del can queue it);
the worker (:546-554) runs pmic_glink_altmode_request
(ALTMODE_PAN_EN), touching the same lock/client/pan_ack set -- no
cancel anywhere either.
* Closed-loop exclusions (why nothing protects this today)
The client list_del only stops new callbacks (system_wq has no
relation to pg->client_lock); the devm order only decides which
freed object is hit first; the mux layer's try_module_get pins
code, not these objects; the device-model reference pins the
provider device struct, not the devm data. The probe-failure
path is safe (returns before client registration, no work ever
scheduled) -- the exposure is exactly unbind/restart with a work
in flight, and USBC notifications are high-frequency (plug/HPD/
IRQ_HPD), the worker can reside up to 5 s.
* Evidence (verification; instrument attached)
Qualcomm pmic_glink firmware/ADSP is not available in the test VM,
so the dynamic verification ran the structural isomorph of this
interleave on the native typec class layer (kmod/typcrace.c: an
anx7411-shaped worker with the same "unlocked unregister vs work
holding objects" shape, driven against the native
typec_register/unregister paths):
BUG: KASAN: slab-use-after-free in typec_altmode_update_active
(14 reports across six access points + sysfs_notify + the full
kobject_uevent chain, then a strlen wild-pointer Oops)
alloc: typec_register_altmode <- (provider probe)
free: kfree <- device_release <- kobject_put <- unregister
(the audited "handle freed while work runs" shape)
use: the parked work's continuation
natural (no instrumentation): an Oops in klist_del <- device_del
<- device_unregister <- the work function, ~minutes into a
create/work/remove loop
The pmic_glink side itself is a line-level source finding: the
above isomorph exercises the identical structure (async work vs
devm-released handles and body, no stop/wait), and the driver
file contains no cancel/flush to contradict it.
* Suggested fix
Register a devm action that cancels both works FIRST (i.e. the
last-registered devm action = first executed), or implement
.remove:
cancel_work_sync(&altmode->enable_work);
for (port...) if (altmode->ports[port].altmode)
cancel_work_sync(&altmode->ports[port].work);
placed before any put. For the data race: fold the notify-side
field writes and the worker's reads under altmode->lock (or
snapshot the fields into the work), as a separate small patch.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: typcrace.c --]
[-- Type: text/plain, Size: 18699 bytes --]
// SPDX-License-Identifier: GPL-2.0
/*
* typcrace.c - dynamic verification module for drivers/usb/typec audit
* (docs/manual-review-drivers-usb-typec-2026-11-02.round3.md)
*
* Statement-faithful replicas of the audited driver state machines (anx7411,
* stusb160x), driven via the exported typec class API on a fake typec_port,
* with one-shot kprobe stalls making the audited interleavings deterministic.
* Kernel source unmodified (build-kasan-typec-7.3).
*
* tc0 - audit Case 3 interleaving 1: anx7411 work uses amode[] pointer
* (check-then-use, anx7411.c:602-613) while remove frees the device.
* Work is parked by a kprobe at its use point
* (typec_altmode_update_active) AFTER it read amode[i]; the remove
* thread unregisters (frees) the altmode during the park; on resume
* the use dereferences freed memory -> KASAN UAF.
* tc0b - audit Case 3 interleaving 2: double device_unregister. Remove is
* parked by a one-shot kprobe at typec_unregister_altmode BEFORE it
* executes; the queued work then unregisters the same altmode
* (device freed); on resume remove's unregister runs on the freed
* device -> UAF inside typec_unregister_altmode/class core.
* tc1 - audit Case 4 (stusb160x form): irq thread parked by one-shot kprobe
* at typec_unregister_partner (having seen chip->partner != NULL);
* remove thread then unregisters the same partner and unregisters the
* port; irq resumes -> unregister on freed partner -> UAF.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/delay.h>
#include <linux/ktime.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>
#include <linux/completion.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/usb/typec.h>
#include <linux/usb/typec_altmode.h>
#define MAX_ALTMODE 4
struct tyc_typec {
struct typec_port *port;
struct typec_partner *partner;
struct typec_altmode *amode[MAX_ALTMODE];
};
/* ---------------- anx7411 replica (Case 3) ---------------- */
struct tyc_anx {
struct tyc_typec typec;
struct mutex lock; /* analog of ctx->lock: work side only */
struct work_struct work;
struct workqueue_struct *wq;
struct completion work_in_use; /* work parked at its use point */
};
static void anx_unregister_altmode_replica(struct tyc_anx *ctx)
{
int i;
/* anx7411.c:535-549 -- NO ctx->lock (audited) */
for (i = 0; i < MAX_ALTMODE; i++)
if (ctx->typec.amode[i]) {
typec_unregister_altmode(ctx->typec.amode[i]);
ctx->typec.amode[i] = NULL;
}
}
static void anx_unregister_partner_replica(struct tyc_anx *ctx)
{
/* anx7411.c:585-591 -- NO ctx->lock (audited) */
if (ctx->typec.partner) {
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
static bool anx_use_mode; /* tc0: work parks at use point */
static struct tyc_anx *g_anx0; /* tc0 target for kprobe lookup */
/*
* tc0 kprobe target: typec_altmode_update_active is only reached from the
* work's use point in this module (the remove thread never calls it).
* Park here AFTER the work has read amode[i] (audited check-then-use gap).
*/
static int kp_use_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
struct tyc_anx *ctx = g_anx0;
if (!anx_use_mode || !ctx)
return 0;
anx_use_mode = false;
pr_info("TC0: work parked at use point (adev=%px), remove frees now\n",
(void *)regs->di);
complete(&ctx->work_in_use);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0: work resumes use on (possibly freed) adev\n");
return 0;
}
static struct kprobe kp_use = {
.symbol_name = "typec_altmode_update_active",
.pre_handler = kp_use_pre,
};
/* work replica of anx7411_work_func (anx7411.c:906-984) */
static void anx_work_func(struct work_struct *w)
{
struct tyc_anx *ctx = container_of(w, struct tyc_anx, work);
int i;
mutex_lock(&ctx->lock); /* :916 */
/* "update_altmode" check-then-use loop (:602-613) */
for (i = 0; i < MAX_ALTMODE; i++) {
if (!ctx->typec.amode[i])
continue;
pr_info("TC0: work read amode[%d]=%px svid=%04x, calling update_active\n",
i, ctx->typec.amode[i], ctx->typec.amode[i]->svid);
typec_altmode_update_active(ctx->typec.amode[i], true);
}
/* POW_OFF branch (:943-946) */
pr_info("TC0: work unregister altmode+partner\n");
anx_unregister_altmode_replica(ctx);
anx_unregister_partner_replica(ctx);
mutex_unlock(&ctx->lock);
}
static int anx_remove_thread(void *data)
{
struct tyc_anx *ctx = data;
pr_info("TC0*: remove enters (audited order: unregister FIRST)\n");
anx_unregister_altmode_replica(ctx); /* :1566 */
anx_unregister_partner_replica(ctx); /* :1567 */
pr_info("TC0*: remove stops work (too late, audited)\n");
cancel_work_sync(&ctx->work); /* :1571 */
pr_info("TC0*: remove done\n");
return 0;
}
/* one-shot stall at typec_unregister_altmode (tc0b): park the FIRST caller */
static bool oneshot_armed;
static struct completion tc_done_wait;
static int kp_unreg_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC0b: remove parked at typec_unregister_altmode (adev=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0b: parked unregister resumes (device likely freed)\n");
return 0;
}
/* ---------------- stusb160x replica (Case 4) ---------------- */
struct tyc_stu {
struct tyc_typec typec;
struct task_struct *irq_thread;
struct completion irq_parked;
};
static int stu_irq_thread(void *data)
{
struct tyc_stu *ctx = data;
while (!kthread_should_stop()) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(msecs_to_jiffies(20));
if (kthread_should_stop())
break;
/* stusb160x_detach(:388-389) check-then-use */
if (ctx->typec.partner) {
pr_info("TC1: irq saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
return 0;
}
static int stu_remove_thread(void *data)
{
struct tyc_stu *ctx = data;
pr_info("TC1: remove enters (devm-irq window open)\n");
/* stusb160x.c:805-808 */
if (ctx->typec.partner) {
pr_info("TC1: remove saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
pr_info("TC1: remove unregisters port\n");
typec_unregister_port(ctx->typec.port);
ctx->typec.port = NULL;
pr_info("TC1: remove done\n");
return 0;
}
/* ---------------- orchestration ---------------- */
static struct tyc_anx *g_anx;
static struct tyc_stu *g_stu;
static struct kprobe kp_unreg;
static int tc_setup_common(struct tyc_typec *t, struct device *parent)
{
struct typec_capability cap = { };
struct typec_partner_desc pdesc = { };
struct typec_altmode_desc adesc = { };
int i;
cap.type = TYPEC_PORT_DRP;
cap.data = TYPEC_PORT_DRD;
cap.revision = USB_TYPEC_REV_1_2;
cap.driver_data = t;
t->port = typec_register_port(parent, &cap);
if (IS_ERR(t->port)) {
pr_err("typec_register_port failed: %pe\n", t->port);
return PTR_ERR(t->port);
}
t->partner = typec_register_partner(t->port, &pdesc);
if (IS_ERR(t->partner)) {
pr_err("typec_register_partner failed: %pe\n", t->partner);
typec_unregister_port(t->port);
t->port = NULL;
return PTR_ERR(t->partner);
}
for (i = 0; i < 2; i++) {
adesc.svid = 0x1234 + i;
adesc.mode = i + 1;
adesc.vdo = 0xdeadbeef;
adesc.roles = TYPEC_PORT_DRD;
t->amode[i] = typec_partner_register_altmode(t->partner, &adesc);
if (IS_ERR(t->amode[i])) {
pr_err("partner_register_altmode[%d] failed: %pe\n",
i, t->amode[i]);
t->amode[i] = NULL;
return -EFAULT;
}
}
return 0;
}
static int run_tc0(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&g_anx->work_in_use);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwq");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
ret = register_kprobe(&kp_use);
if (ret) {
pr_err("TC0: kprobe(use) failed %d\n", ret);
goto out;
}
g_anx0 = g_anx;
anx_use_mode = true;
/* 1. queue work; it parks inside kprobe at its first use, having
* already read amode[0] (audited check-then-use gap). */
queue_work(g_anx->wq, &g_anx->work);
/* 2. wait until parked */
if (!wait_for_completion_timeout(&g_anx->work_in_use, msecs_to_jiffies(2000))) {
pr_err("TC0: work never parked; abort\n");
goto out;
}
/* 3. remove thread frees altmodes+partner under the parked work */
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm");
if (!IS_ERR(rm))
kthread_stop(rm); /* joins: remove completes inside window */
/* 4. work auto-resumes after 400ms -> UAF */
pr_info("TC0: RESULT see KASAN reports above\n");
msleep(300);
anx_unregister_altmode_replica(g_anx);
anx_unregister_partner_replica(g_anx);
out:
unregister_kprobe(&kp_use);
g_anx0 = NULL;
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int run_tc0b(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0b");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&tc_done_wait);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwqb");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_altmode",
.pre_handler = kp_unreg_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC0b: kprobe failed %d\n", ret);
goto out;
}
/* 1. remove thread parks inside kprobe BEFORE its unregister */
oneshot_armed = true;
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm0b");
if (IS_ERR(rm)) {
pr_err("TC0b: kthread failed\n");
goto out;
}
/* 2. wait parked */
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC0b: remove never parked\n");
kthread_stop(rm);
goto out;
}
/* 3. now let the work run: it unregisters the same altmodes+partner
* (device freed) while remove is parked mid-unregister */
queue_work(g_anx->wq, &g_anx->work);
msleep(100);
/* 4. remove resumes -> unregister on freed device */
pr_info("TC0b: remove resumes at ~300ms\n");
msleep(700); /* let everything settle */
cancel_work_sync(&g_anx->work);
pr_info("TC0b: RESULT see KASAN reports above\n");
out:
unregister_kprobe(&kp_unreg);
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int kp_park_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC1: irq thread parked at typec_unregister_partner (partner=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC1: irq resumes unregister (partner likely freed)\n");
return 0;
}
static int run_tc1(void)
{
struct device *root_dev;
int ret;
root_dev = root_device_register("tycroot1");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_stu = kzalloc(sizeof(*g_stu), GFP_KERNEL);
if (!g_stu) {
root_device_unregister(root_dev);
return -ENOMEM;
}
init_completion(&tc_done_wait);
init_completion(&g_stu->irq_parked);
ret = tc_setup_common(&g_stu->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_partner",
.pre_handler = kp_park_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC1: kprobe failed %d\n", ret);
goto out;
}
/* 1. irq thread parks at its unregister (partner still alive) */
oneshot_armed = true;
g_stu->irq_thread = kthread_run(stu_irq_thread, g_stu, "tyc-stuirq");
if (IS_ERR(g_stu->irq_thread)) {
g_stu->irq_thread = NULL;
goto out;
}
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC1: irq never parked\n");
goto out;
}
/* 2. remove sequence inline on the insmod thread (partner still
* alive -> unregister frees it, then port); parked irq thread is
* on another CPU */
stu_remove_thread(g_stu);
/* 3. irq resumes -> unregister on freed partner */
pr_info("TC1: RESULT see KASAN reports above\n");
msleep(300);
out:
unregister_kprobe(&kp_unreg);
if (g_stu) {
if (g_stu->irq_thread)
kthread_stop(g_stu->irq_thread);
kfree(g_stu);
g_stu = NULL;
}
root_device_unregister(root_dev);
return 0;
}
/* ---------------- tc2: Case 1-B deadlock structure replica ----------------
* Faithful structure replica of the audited UCSI unbind deadlock:
* - unbind thread holds "con->lock" and calls cancel_delayed_work_sync()
* (ucsi.c:2003-2004 -> mode_selection.c:277);
* - the mode-selection work, inside its activate window, must take the
* same lock via the ucsi_con_mutex_lock() trylock loop whose exit
* condition is the STALE con->status CONNECTED snapshot
* (ucsi.c:2272-2277; ucsi.h:457 reads ->status, never refreshed on the
* unbind path).
* Expected result: both sides park forever; the VM hangs until timeout.
*/
struct tyc_dead {
struct mutex lock; /* con->lock analog */
bool connected_snapshot; /* con->status CONNECTED bit (stale) */
struct delayed_work work;
struct completion in_loop; /* handshake: work inside trylock loop */
bool ctl; /* ctl=true: lock first, queue later */
};
static void dead_work_fn(struct work_struct *w)
{
struct tyc_dead *d = container_of(w, struct tyc_dead, work.work);
bool locked = false;
pr_info("TC2: work entered activate window, needs con->lock\n");
/* first trylock fails: unbind already holds the lock */
locked = mutex_trylock(&d->lock) != 0;
if (!locked)
complete(&d->in_loop);
/* ucsi_con_mutex_lock() loop, ucsi.c:2272-2277, verbatim shape */
while (d->connected_snapshot && !locked) {
locked = mutex_trylock(&d->lock) != 0;
if (d->connected_snapshot && !locked) {
msleep(20);
}
}
if (locked)
mutex_unlock(&d->lock);
pr_info("TC2: work finished\n");
}
/* audited interleaving: work enters the loop first, unbind then takes the
* lock and cancels -- expected permanent hang (VM killed by timeout). */
static int run_tc2(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true; /* cable connected snapshot */
d->ctl = false;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
/* audited order: unbind holds con->lock FIRST, work starts and
* fails its trylocks while the lock stays held */
mutex_lock(&d->lock);
pr_info("TC2: unbind holds con->lock; scheduling work\n");
schedule_delayed_work(&d->work, 0);
if (!wait_for_completion_timeout(&d->in_loop, msecs_to_jiffies(2000))) {
pr_err("TC2: work never entered loop\n");
mutex_unlock(&d->lock);
kfree(d);
return -ETIMEDOUT;
}
pr_info("TC2: work looping on failed trylocks; unbind calls cancel (expect hang)\n");
cancel_delayed_work_sync(&d->work); /* audited hang */
pr_info("TC2: cancel returned (should never print)\n");
mutex_unlock(&d->lock);
kfree(d);
return 0;
}
/* control: unbind takes the lock BEFORE the work ever runs; cancel() kills
* the pending timer -- no deadlock (audited escape-branch 1). */
static int run_tc2ctl(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true;
d->ctl = true;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
mutex_lock(&d->lock);
pr_info("TC2CTL: unbind holds lock first, queueing work\n");
schedule_delayed_work(&d->work, msecs_to_jiffies(50));
cancel_delayed_work_sync(&d->work); /* pending -> killed */
pr_info("TC2CTL: cancel returned (expected, pending branch)\n");
mutex_unlock(&d->lock);
msleep(200);
kfree(d);
return 0;
}
/* ---------------- tcnat: natural hammering (no probes) ---------------- */
static int tcnat_rounds = 1000;
module_param(tcnat_rounds, int, 0444);
static int run_tcnat(void)
{
struct device *root_dev;
int round;
for (round = 0; round < tcnat_rounds; round++) {
root_dev = root_device_register("tycrootnat");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycnatwq");
if (tc_setup_common(&g_anx->typec, root_dev)) {
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
return -EFAULT;
}
/* natural race: queue work, then immediately remove */
queue_work(g_anx->wq, &g_anx->work);
anx_remove_thread(g_anx);
cancel_work_sync(&g_anx->work);
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
if (!(round % 100))
pr_info("TCNAT: round %d done\n", round);
}
pr_info("TCNAT: %d rounds done (check dmesg for KASAN reports)\n",
tcnat_rounds);
return 0;
}
/* ---------------- module params & entry ---------------- */
static char *action = "status";
module_param(action, charp, 0444);
MODULE_PARM_DESC(action, "tc0|tc0b|tc1|tc2|tcnat|status");
static int tyc_run_action(void)
{
if (!strcmp(action, "tc0"))
return run_tc0();
if (!strcmp(action, "tc0b"))
return run_tc0b();
if (!strcmp(action, "tc1"))
return run_tc1();
if (!strcmp(action, "tc2"))
return run_tc2();
if (!strcmp(action, "tc2ctl"))
return run_tc2ctl();
if (!strcmp(action, "tcnat"))
return run_tcnat();
pr_info("no action, done\n");
return 0;
}
static int __init tyc_init(void)
{
pr_info("action=%s\n", action);
return tyc_run_action();
}
module_init(tyc_init);
static void __exit tyc_exit(void)
{
pr_info("exit\n");
}
module_exit(tyc_exit);
MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 10+ messages in thread
* usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove
2026-10-27 14:12 ` Greg KH
2026-11-04 12:54 ` soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work) 徐晗翔
@ 2026-11-04 12:57 ` 徐晗翔
2026-11-04 13:28 ` Greg KH
2026-11-04 12:58 ` usb: typec: ucsi: deadlock on cros_ec unbind -- connector lock held across cancel_delayed_work_sync vs the mode-selection work's unbounded trylock polling 徐晗翔
` (3 subsequent siblings)
5 siblings, 1 reply; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 12:57 UTC (permalink / raw)
To: heikki.krogerus; +Cc: linux-usb, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 5160 bytes --]
anx7411_i2c_remove() does its teardown in exactly the wrong order:
it first unregisters the partner and its altmodes
(anx7411_partner_unregister_altmode at anx7411.c:1566 and
anx7411_unregister_partner at :1567 -- both UNLOCKED walks of
amode[]/partner) and only afterwards disables the IRQ (:1570) and
cancels the work (:1571). The IRQ thread (devm-requested at
:1527-1531) can enqueue anx7411_work_func() (which runs holding
ctx->lock, :916) at any moment before that, and the remove side does
not take ctx->lock -- so the two sides race on the check-then-use of
amode[] and partner with no common synchronization. Three concrete
interleavings, all verified on a pristine Linux 7.3 KASAN kernel
against the NATIVE typec class and device core with a statement-level
replica of this driver's worker/remove pair: (1) the work
dereferences an altmode already freed by the remove-side unregister
-- 14 KASAN slab-use-after-free reports across the six access points
of typec_altmode_update_active plus sysfs_notify and the whole
kobject_uevent chain, ending in a strlen wild-pointer Oops that kills
the worker; (2) a double typec_unregister_altmode (the work's
POW_OFF branch unregistering at :943/:946 against the remove-side
unregister) -- 24 KASAN reports of the device core executing on a
freed device (device_del internals, dpm_sysfs_remove, freed locks)
and two Oopses; (3) the work re-registers a partner on the already
unregistered port (orphan). The natural, uninstrumented variant of
interleaving 2 hit on its own after ~138 s of create/work/remove
hammering: an Oops in klist_del <- device_del <- device_unregister
<- the work function. The fix is to reorder the remove (stop
IRQ/work first) and take ctx->lock around the unregister walks.
The detailed description:
* The wrong order (Linux 7.3, drivers/usb/typec/anx7411.c)
anx7411_i2c_remove():
:1566 anx7411_partner_unregister_altmode() /* :535-549, NO
lock: walks amode[], typec_unregister_altmode + NULL */
:1567 anx7411_unregister_partner() /* :585-591, NO
lock */
:1570 disable_irq(...)
:1571 cancel_work_sync(&ctx->work) /* too late */
The IRQ thread (:986-993; devm IRQ :1527-1531) queues
anx7411_work_func (:906-984) at any time before :1570; the worker
holds ctx->lock, the remove side does not -- the "check then use"
of amode[i] (:603 -> :606-610), partner (:625) and the
unregister/NULL pairs (:545/:589, :943/:946) have no atomicity
across the two contexts.
* The three interleavings (each dynamically verified; module attached)
1. Work uses an altmode freed by remove: the remove side's
unregister drops the last reference (class.c:606-616 kfree);
the worker's stale amode[i] feeds typec_altmode_update_active/
notify (:606-610) -> UAF, and the resulting update cascade
(sysfs_notify -> kobject_uevent_env -> dev_uevent ->
kobject_get_path -> strlen) Oopses the worker. 14 KASAN
reports; three stacks closed:
alloc: typec_register_altmode <- probe (1024B altmode)
free: kfree <- device_release <- kobject_put <-
unregister (the audited :544 shape)
use: the parked work's continuation
2. Double unregister: the worker's POW_OFF branch (:943/:946) and
the remove side (:1566/:1567) unregister the same altmode/partner
concurrently: the device core (device_del's internals,
dpm_sysfs_remove, sysfs unmerge, bus_notify, freed mutexes,
kernfs lookups) executes on the freed device -- 24 KASAN reports
and two Oopses.
3. Orphan re-registration: the worker's register_altmode (:618-645)
uses the stale partner pointer (:625 check is TOCTOU) after the
remove side already unregistered it.
Natural reachability: with no instrumentation, a create -> queue
work -> immediate remove loop hit interleaving 2's natural shape
after ~138 s: Oops at klist_del+0x22 <- device_del+0x168 <-
device_unregister <- the work function; the worker died (exited
with irqs disabled) and the module hung. So under plain admin
unbind pressure the hit rate is nontrivial, no debugging tools
needed.
Fidelity: the class layer (typec_register/unregister_altmode/
partner, device core, kobject/klist, sysfs/uevent) is 100% native
kernel code; anx7411's worker state machine and remove sequence are
replicated statement-by-statement in the module (anx_work_func/
anx_unregister_*_replica) per the audited line numbers.
Module usage: insmod typcrace.ko; then select the action from the
evidence section's plans (tc0/tc0b/tcnat for this report's three
forms); a parked-worker kprobe (in the module) turns the natural
windows deterministic.
* Suggested fix
Move the stop/wait block (:1569-1573: disable_irq + cancel_work_
sync) BEFORE the unregister side (:1566/:1567), and take ctx->lock
around the amode[]/partner unregister walks. The mux/switch
unregister at :1577-1579 is already after the cancel and correct --
the impact is confined to partner/altmode.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: typcrace.c --]
[-- Type: text/plain, Size: 18699 bytes --]
// SPDX-License-Identifier: GPL-2.0
/*
* typcrace.c - dynamic verification module for drivers/usb/typec audit
* (docs/manual-review-drivers-usb-typec-2026-11-02.round3.md)
*
* Statement-faithful replicas of the audited driver state machines (anx7411,
* stusb160x), driven via the exported typec class API on a fake typec_port,
* with one-shot kprobe stalls making the audited interleavings deterministic.
* Kernel source unmodified (build-kasan-typec-7.3).
*
* tc0 - audit Case 3 interleaving 1: anx7411 work uses amode[] pointer
* (check-then-use, anx7411.c:602-613) while remove frees the device.
* Work is parked by a kprobe at its use point
* (typec_altmode_update_active) AFTER it read amode[i]; the remove
* thread unregisters (frees) the altmode during the park; on resume
* the use dereferences freed memory -> KASAN UAF.
* tc0b - audit Case 3 interleaving 2: double device_unregister. Remove is
* parked by a one-shot kprobe at typec_unregister_altmode BEFORE it
* executes; the queued work then unregisters the same altmode
* (device freed); on resume remove's unregister runs on the freed
* device -> UAF inside typec_unregister_altmode/class core.
* tc1 - audit Case 4 (stusb160x form): irq thread parked by one-shot kprobe
* at typec_unregister_partner (having seen chip->partner != NULL);
* remove thread then unregisters the same partner and unregisters the
* port; irq resumes -> unregister on freed partner -> UAF.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/delay.h>
#include <linux/ktime.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>
#include <linux/completion.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/usb/typec.h>
#include <linux/usb/typec_altmode.h>
#define MAX_ALTMODE 4
struct tyc_typec {
struct typec_port *port;
struct typec_partner *partner;
struct typec_altmode *amode[MAX_ALTMODE];
};
/* ---------------- anx7411 replica (Case 3) ---------------- */
struct tyc_anx {
struct tyc_typec typec;
struct mutex lock; /* analog of ctx->lock: work side only */
struct work_struct work;
struct workqueue_struct *wq;
struct completion work_in_use; /* work parked at its use point */
};
static void anx_unregister_altmode_replica(struct tyc_anx *ctx)
{
int i;
/* anx7411.c:535-549 -- NO ctx->lock (audited) */
for (i = 0; i < MAX_ALTMODE; i++)
if (ctx->typec.amode[i]) {
typec_unregister_altmode(ctx->typec.amode[i]);
ctx->typec.amode[i] = NULL;
}
}
static void anx_unregister_partner_replica(struct tyc_anx *ctx)
{
/* anx7411.c:585-591 -- NO ctx->lock (audited) */
if (ctx->typec.partner) {
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
static bool anx_use_mode; /* tc0: work parks at use point */
static struct tyc_anx *g_anx0; /* tc0 target for kprobe lookup */
/*
* tc0 kprobe target: typec_altmode_update_active is only reached from the
* work's use point in this module (the remove thread never calls it).
* Park here AFTER the work has read amode[i] (audited check-then-use gap).
*/
static int kp_use_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
struct tyc_anx *ctx = g_anx0;
if (!anx_use_mode || !ctx)
return 0;
anx_use_mode = false;
pr_info("TC0: work parked at use point (adev=%px), remove frees now\n",
(void *)regs->di);
complete(&ctx->work_in_use);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0: work resumes use on (possibly freed) adev\n");
return 0;
}
static struct kprobe kp_use = {
.symbol_name = "typec_altmode_update_active",
.pre_handler = kp_use_pre,
};
/* work replica of anx7411_work_func (anx7411.c:906-984) */
static void anx_work_func(struct work_struct *w)
{
struct tyc_anx *ctx = container_of(w, struct tyc_anx, work);
int i;
mutex_lock(&ctx->lock); /* :916 */
/* "update_altmode" check-then-use loop (:602-613) */
for (i = 0; i < MAX_ALTMODE; i++) {
if (!ctx->typec.amode[i])
continue;
pr_info("TC0: work read amode[%d]=%px svid=%04x, calling update_active\n",
i, ctx->typec.amode[i], ctx->typec.amode[i]->svid);
typec_altmode_update_active(ctx->typec.amode[i], true);
}
/* POW_OFF branch (:943-946) */
pr_info("TC0: work unregister altmode+partner\n");
anx_unregister_altmode_replica(ctx);
anx_unregister_partner_replica(ctx);
mutex_unlock(&ctx->lock);
}
static int anx_remove_thread(void *data)
{
struct tyc_anx *ctx = data;
pr_info("TC0*: remove enters (audited order: unregister FIRST)\n");
anx_unregister_altmode_replica(ctx); /* :1566 */
anx_unregister_partner_replica(ctx); /* :1567 */
pr_info("TC0*: remove stops work (too late, audited)\n");
cancel_work_sync(&ctx->work); /* :1571 */
pr_info("TC0*: remove done\n");
return 0;
}
/* one-shot stall at typec_unregister_altmode (tc0b): park the FIRST caller */
static bool oneshot_armed;
static struct completion tc_done_wait;
static int kp_unreg_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC0b: remove parked at typec_unregister_altmode (adev=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0b: parked unregister resumes (device likely freed)\n");
return 0;
}
/* ---------------- stusb160x replica (Case 4) ---------------- */
struct tyc_stu {
struct tyc_typec typec;
struct task_struct *irq_thread;
struct completion irq_parked;
};
static int stu_irq_thread(void *data)
{
struct tyc_stu *ctx = data;
while (!kthread_should_stop()) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(msecs_to_jiffies(20));
if (kthread_should_stop())
break;
/* stusb160x_detach(:388-389) check-then-use */
if (ctx->typec.partner) {
pr_info("TC1: irq saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
return 0;
}
static int stu_remove_thread(void *data)
{
struct tyc_stu *ctx = data;
pr_info("TC1: remove enters (devm-irq window open)\n");
/* stusb160x.c:805-808 */
if (ctx->typec.partner) {
pr_info("TC1: remove saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
pr_info("TC1: remove unregisters port\n");
typec_unregister_port(ctx->typec.port);
ctx->typec.port = NULL;
pr_info("TC1: remove done\n");
return 0;
}
/* ---------------- orchestration ---------------- */
static struct tyc_anx *g_anx;
static struct tyc_stu *g_stu;
static struct kprobe kp_unreg;
static int tc_setup_common(struct tyc_typec *t, struct device *parent)
{
struct typec_capability cap = { };
struct typec_partner_desc pdesc = { };
struct typec_altmode_desc adesc = { };
int i;
cap.type = TYPEC_PORT_DRP;
cap.data = TYPEC_PORT_DRD;
cap.revision = USB_TYPEC_REV_1_2;
cap.driver_data = t;
t->port = typec_register_port(parent, &cap);
if (IS_ERR(t->port)) {
pr_err("typec_register_port failed: %pe\n", t->port);
return PTR_ERR(t->port);
}
t->partner = typec_register_partner(t->port, &pdesc);
if (IS_ERR(t->partner)) {
pr_err("typec_register_partner failed: %pe\n", t->partner);
typec_unregister_port(t->port);
t->port = NULL;
return PTR_ERR(t->partner);
}
for (i = 0; i < 2; i++) {
adesc.svid = 0x1234 + i;
adesc.mode = i + 1;
adesc.vdo = 0xdeadbeef;
adesc.roles = TYPEC_PORT_DRD;
t->amode[i] = typec_partner_register_altmode(t->partner, &adesc);
if (IS_ERR(t->amode[i])) {
pr_err("partner_register_altmode[%d] failed: %pe\n",
i, t->amode[i]);
t->amode[i] = NULL;
return -EFAULT;
}
}
return 0;
}
static int run_tc0(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&g_anx->work_in_use);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwq");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
ret = register_kprobe(&kp_use);
if (ret) {
pr_err("TC0: kprobe(use) failed %d\n", ret);
goto out;
}
g_anx0 = g_anx;
anx_use_mode = true;
/* 1. queue work; it parks inside kprobe at its first use, having
* already read amode[0] (audited check-then-use gap). */
queue_work(g_anx->wq, &g_anx->work);
/* 2. wait until parked */
if (!wait_for_completion_timeout(&g_anx->work_in_use, msecs_to_jiffies(2000))) {
pr_err("TC0: work never parked; abort\n");
goto out;
}
/* 3. remove thread frees altmodes+partner under the parked work */
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm");
if (!IS_ERR(rm))
kthread_stop(rm); /* joins: remove completes inside window */
/* 4. work auto-resumes after 400ms -> UAF */
pr_info("TC0: RESULT see KASAN reports above\n");
msleep(300);
anx_unregister_altmode_replica(g_anx);
anx_unregister_partner_replica(g_anx);
out:
unregister_kprobe(&kp_use);
g_anx0 = NULL;
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int run_tc0b(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0b");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&tc_done_wait);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwqb");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_altmode",
.pre_handler = kp_unreg_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC0b: kprobe failed %d\n", ret);
goto out;
}
/* 1. remove thread parks inside kprobe BEFORE its unregister */
oneshot_armed = true;
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm0b");
if (IS_ERR(rm)) {
pr_err("TC0b: kthread failed\n");
goto out;
}
/* 2. wait parked */
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC0b: remove never parked\n");
kthread_stop(rm);
goto out;
}
/* 3. now let the work run: it unregisters the same altmodes+partner
* (device freed) while remove is parked mid-unregister */
queue_work(g_anx->wq, &g_anx->work);
msleep(100);
/* 4. remove resumes -> unregister on freed device */
pr_info("TC0b: remove resumes at ~300ms\n");
msleep(700); /* let everything settle */
cancel_work_sync(&g_anx->work);
pr_info("TC0b: RESULT see KASAN reports above\n");
out:
unregister_kprobe(&kp_unreg);
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int kp_park_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC1: irq thread parked at typec_unregister_partner (partner=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC1: irq resumes unregister (partner likely freed)\n");
return 0;
}
static int run_tc1(void)
{
struct device *root_dev;
int ret;
root_dev = root_device_register("tycroot1");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_stu = kzalloc(sizeof(*g_stu), GFP_KERNEL);
if (!g_stu) {
root_device_unregister(root_dev);
return -ENOMEM;
}
init_completion(&tc_done_wait);
init_completion(&g_stu->irq_parked);
ret = tc_setup_common(&g_stu->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_partner",
.pre_handler = kp_park_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC1: kprobe failed %d\n", ret);
goto out;
}
/* 1. irq thread parks at its unregister (partner still alive) */
oneshot_armed = true;
g_stu->irq_thread = kthread_run(stu_irq_thread, g_stu, "tyc-stuirq");
if (IS_ERR(g_stu->irq_thread)) {
g_stu->irq_thread = NULL;
goto out;
}
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC1: irq never parked\n");
goto out;
}
/* 2. remove sequence inline on the insmod thread (partner still
* alive -> unregister frees it, then port); parked irq thread is
* on another CPU */
stu_remove_thread(g_stu);
/* 3. irq resumes -> unregister on freed partner */
pr_info("TC1: RESULT see KASAN reports above\n");
msleep(300);
out:
unregister_kprobe(&kp_unreg);
if (g_stu) {
if (g_stu->irq_thread)
kthread_stop(g_stu->irq_thread);
kfree(g_stu);
g_stu = NULL;
}
root_device_unregister(root_dev);
return 0;
}
/* ---------------- tc2: Case 1-B deadlock structure replica ----------------
* Faithful structure replica of the audited UCSI unbind deadlock:
* - unbind thread holds "con->lock" and calls cancel_delayed_work_sync()
* (ucsi.c:2003-2004 -> mode_selection.c:277);
* - the mode-selection work, inside its activate window, must take the
* same lock via the ucsi_con_mutex_lock() trylock loop whose exit
* condition is the STALE con->status CONNECTED snapshot
* (ucsi.c:2272-2277; ucsi.h:457 reads ->status, never refreshed on the
* unbind path).
* Expected result: both sides park forever; the VM hangs until timeout.
*/
struct tyc_dead {
struct mutex lock; /* con->lock analog */
bool connected_snapshot; /* con->status CONNECTED bit (stale) */
struct delayed_work work;
struct completion in_loop; /* handshake: work inside trylock loop */
bool ctl; /* ctl=true: lock first, queue later */
};
static void dead_work_fn(struct work_struct *w)
{
struct tyc_dead *d = container_of(w, struct tyc_dead, work.work);
bool locked = false;
pr_info("TC2: work entered activate window, needs con->lock\n");
/* first trylock fails: unbind already holds the lock */
locked = mutex_trylock(&d->lock) != 0;
if (!locked)
complete(&d->in_loop);
/* ucsi_con_mutex_lock() loop, ucsi.c:2272-2277, verbatim shape */
while (d->connected_snapshot && !locked) {
locked = mutex_trylock(&d->lock) != 0;
if (d->connected_snapshot && !locked) {
msleep(20);
}
}
if (locked)
mutex_unlock(&d->lock);
pr_info("TC2: work finished\n");
}
/* audited interleaving: work enters the loop first, unbind then takes the
* lock and cancels -- expected permanent hang (VM killed by timeout). */
static int run_tc2(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true; /* cable connected snapshot */
d->ctl = false;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
/* audited order: unbind holds con->lock FIRST, work starts and
* fails its trylocks while the lock stays held */
mutex_lock(&d->lock);
pr_info("TC2: unbind holds con->lock; scheduling work\n");
schedule_delayed_work(&d->work, 0);
if (!wait_for_completion_timeout(&d->in_loop, msecs_to_jiffies(2000))) {
pr_err("TC2: work never entered loop\n");
mutex_unlock(&d->lock);
kfree(d);
return -ETIMEDOUT;
}
pr_info("TC2: work looping on failed trylocks; unbind calls cancel (expect hang)\n");
cancel_delayed_work_sync(&d->work); /* audited hang */
pr_info("TC2: cancel returned (should never print)\n");
mutex_unlock(&d->lock);
kfree(d);
return 0;
}
/* control: unbind takes the lock BEFORE the work ever runs; cancel() kills
* the pending timer -- no deadlock (audited escape-branch 1). */
static int run_tc2ctl(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true;
d->ctl = true;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
mutex_lock(&d->lock);
pr_info("TC2CTL: unbind holds lock first, queueing work\n");
schedule_delayed_work(&d->work, msecs_to_jiffies(50));
cancel_delayed_work_sync(&d->work); /* pending -> killed */
pr_info("TC2CTL: cancel returned (expected, pending branch)\n");
mutex_unlock(&d->lock);
msleep(200);
kfree(d);
return 0;
}
/* ---------------- tcnat: natural hammering (no probes) ---------------- */
static int tcnat_rounds = 1000;
module_param(tcnat_rounds, int, 0444);
static int run_tcnat(void)
{
struct device *root_dev;
int round;
for (round = 0; round < tcnat_rounds; round++) {
root_dev = root_device_register("tycrootnat");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycnatwq");
if (tc_setup_common(&g_anx->typec, root_dev)) {
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
return -EFAULT;
}
/* natural race: queue work, then immediately remove */
queue_work(g_anx->wq, &g_anx->work);
anx_remove_thread(g_anx);
cancel_work_sync(&g_anx->work);
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
if (!(round % 100))
pr_info("TCNAT: round %d done\n", round);
}
pr_info("TCNAT: %d rounds done (check dmesg for KASAN reports)\n",
tcnat_rounds);
return 0;
}
/* ---------------- module params & entry ---------------- */
static char *action = "status";
module_param(action, charp, 0444);
MODULE_PARM_DESC(action, "tc0|tc0b|tc1|tc2|tcnat|status");
static int tyc_run_action(void)
{
if (!strcmp(action, "tc0"))
return run_tc0();
if (!strcmp(action, "tc0b"))
return run_tc0b();
if (!strcmp(action, "tc1"))
return run_tc1();
if (!strcmp(action, "tc2"))
return run_tc2();
if (!strcmp(action, "tc2ctl"))
return run_tc2ctl();
if (!strcmp(action, "tcnat"))
return run_tcnat();
pr_info("no action, done\n");
return 0;
}
static int __init tyc_init(void)
{
pr_info("action=%s\n", action);
return tyc_run_action();
}
module_init(tyc_init);
static void __exit tyc_exit(void)
{
pr_info("exit\n");
}
module_exit(tyc_exit);
MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 10+ messages in thread
* usb: typec: ucsi: deadlock on cros_ec unbind -- connector lock held across cancel_delayed_work_sync vs the mode-selection work's unbounded trylock polling
2026-10-27 14:12 ` Greg KH
2026-11-04 12:54 ` soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work) 徐晗翔
2026-11-04 12:57 ` usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove 徐晗翔
@ 2026-11-04 12:58 ` 徐晗翔
2026-11-04 13:00 ` usb: typec: stusb160x/rt1719/hd3ss3220: IRQ thread vs remove -- double unregister / use-after-free on the partner and port 徐晗翔
` (2 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 12:58 UTC (permalink / raw)
To: heikki.krogerus; +Cc: linux-usb, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 6320 bytes --]
On cros_ec UCSI platforms (ALT_MODE_OVERRIDE firmware), unbinding the
platform driver deadlocks: ucsi_unregister_port() takes con->lock
(ucsi.c:2003) and then reaches typec_mode_selection_delete() ->
cancel_delayed_work_sync(&sel->work) (mode_selection.c:277), which
blocks waiting for the mode-selection work to return -- but that work
must, in its DP/TBT activate callbacks, acquire the very same
con->lock via ucsi_con_mutex_lock() before it can return; the unbind
path never refreshes the connector status, the polling condition
stays true, and the loop never exits. The polling is unbounded
(msleep(20) retry loop, ucsi.c:2272-2277) and its condition is a
stale in-memory snapshot (UCSI_CONSTAT reads con->status as latched
at the last connector update, ucsi.h:457-458) -- since the unbind
neither releases the lock nor updates the snapshot, the work spins
forever and the unbind thread waits forever. I reproduced the hang
deterministically on a pristine Linux 7.3 KASAN kernel with a
structural replica of the four ingredients (mutex +
cancel_delayed_work_sync + trylock/msleep polling + stale snapshot),
and closed the loop with two control runs: cancelling a merely-pending
work returns immediately, and a work that got its first trylock before
unbind finishes normally -- the deadlock is a genuine race on "the
work is executing its activate chain while unbind takes the lock
first". Consequences: the unbind/rmmod (or init-failure rollback)
hangs permanently, a system_wq worker is lost, and every later
contender for con->lock (connector-change work, poll worker, role
swap) piles up behind it -- USB-C/DP dead until reboot. Not a memory
safety issue; a liveness bug.
The detailed description:
* The deadlock path (Linux 7.3 line numbers)
cros_ucsi_remove (cros_ec_ucsi.c:313-319, :317) -> ucsi_unregister
(ucsi.c:2372-2397; :2390 only cancels the connector-change work) ->
ucsi_unregister_port (:1998-2032): :2003 mutex_lock(&con->lock) ->
:2004 ucsi_unregister_partner -> :1317-1318 -> (cros_ec_ucsi.c:157)
typec_mode_selection_delete() -> mode_selection.c:277
cancel_delayed_work_sync(&sel->work) -- which is cancel_work_sync's
delayed variant (kernel/workqueue.c:4637-4640 -> :4552 -> :4569
__flush_work(work, true), documented "guaranteed to be not pending
or executing ... even if the work re-queues itself"): the unbind
thread now WAITS for the running work.
The work (mode_selection.c:130-174) -> mode_selection_activate()
(:148/:156/:160) -> :95 releases sel->lock -> device_for_each_child
-> the partner altmode's .activate (altmodes/displayport.c:504-523;
altmodes/thunderbolt.c:247-262) -> typec_altmode_enter/exit
(bus.c:126-148/:157-177) -> the port-side ops registered by UCSI
(ucsi.c:449-463) -> ucsi_displayport_enter/exit / ucsi_thunderbolt_
enter/exit, whose FIRST statement is ucsi_con_mutex_lock()
(ucsi/displayport.c:57/:119; ucsi/thunderbolt.c:89/:122) -- the
same con->lock the unbind thread holds. The poll:
while (connected && !mutex_locked) {
mutex_trylock(...);
connected = UCSI_CONSTAT(con, CONNECTED); /* snapshot,
ucsi.h:457 */
if (connected && !mutex_locked) msleep(20);
}
never exits: the snapshot stays CONNECTED=1 (latched at connection
time), the unbind path has no ucsi_get_connector_status() call, and
msleep is not interruptible. (The DP altmode activate's cable
branch is not an escape: UCSI registers no cable_ops, so it falls
through to the same typec_altmode_enter.)
* Control runs (the race boundary, dynamically covered)
- Work merely pending (timer not fired): cancel removes it and
returns instantly -- no deadlock. So the trigger needs the work
EXECUTING its activate chain when unbind takes the lock; the work
runs on 1 s/4 s periods (cros_ec_ucsi.c:39-41) and its
activate-chain start (typec_mux_set to an i2c mux provider) takes
milliseconds -- the exposure window is real, hit probability
moderate per unbind.
- Work's first trylock succeeds before unbind takes the lock: the
work completes normally.
- The non-deadlock paths exist too: a connector-change disconnect
refreshes the status to disconnected before unregistering
(ucsi.c:1464 -> :1513), and the resume-time
ucsi_check_connection (:1387 -> :1402) likewise lets the poll
see CONNECTED=0 and return -ENOTCONN -- the unbind path is the
one that does not.
* Evidence (pristine 7.3 KASAN; module attached)
Structural replica with a handshake orchestration (hold the lock ->
start the work -> first failed trylock observed -> call the
unbind-side cancel):
TC2: work looping on failed trylocks; unbind calls cancel
(expect hang)
... no "cancel returned", no "work finished" ...
(VM killed by timeout, exit=124 -- both threads permanently
waiting)
and its mirror-image control (cancel of a pending-only work)
returned immediately and clean. Fidelity: the four deadlock
ingredients (mutex, cancel_delayed_work_sync, trylock/msleep
polling, stale CONNECTED snapshot) are replicated statement-by-
statement per ucsi.c:2003-2004 / mode_selection.c:277 /
ucsi.c:2272-2277 / ucsi.h:457-458; the workqueue primitive
semantics were verified in-kernel (workqueue.c:4552-4590).
Module usage: insmod typcrace.ko; action=tc2 (hang; expect the VM
to die by timeout) / action=tc2ctl (control; clean). On real
hardware the same shape is reached by `rmmod cros_ec_ucsi` (or
unbind of the platform driver) while a DP/TBT session is active.
* Suggested fix (any of; no patch attached)
1. ucsi_unregister_port(): refresh the connector status (or
explicitly clear the CONNECTED snapshot) BEFORE taking
con->lock, so a concurrently polled work exits its loop;
2. cros side: move typec_mode_selection_delete() out of the
con->lock critical section (detach partner->sel under the lock
against new updaters, then unlock and only then cancel+free);
3. harden ucsi_con_mutex_lock() with a bounded wait (e.g. 1 s then
-ENOTCONN) so this class of inverted lock-hold cannot wedge the
worker permanently.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: typcrace.c --]
[-- Type: text/plain, Size: 18699 bytes --]
// SPDX-License-Identifier: GPL-2.0
/*
* typcrace.c - dynamic verification module for drivers/usb/typec audit
* (docs/manual-review-drivers-usb-typec-2026-11-02.round3.md)
*
* Statement-faithful replicas of the audited driver state machines (anx7411,
* stusb160x), driven via the exported typec class API on a fake typec_port,
* with one-shot kprobe stalls making the audited interleavings deterministic.
* Kernel source unmodified (build-kasan-typec-7.3).
*
* tc0 - audit Case 3 interleaving 1: anx7411 work uses amode[] pointer
* (check-then-use, anx7411.c:602-613) while remove frees the device.
* Work is parked by a kprobe at its use point
* (typec_altmode_update_active) AFTER it read amode[i]; the remove
* thread unregisters (frees) the altmode during the park; on resume
* the use dereferences freed memory -> KASAN UAF.
* tc0b - audit Case 3 interleaving 2: double device_unregister. Remove is
* parked by a one-shot kprobe at typec_unregister_altmode BEFORE it
* executes; the queued work then unregisters the same altmode
* (device freed); on resume remove's unregister runs on the freed
* device -> UAF inside typec_unregister_altmode/class core.
* tc1 - audit Case 4 (stusb160x form): irq thread parked by one-shot kprobe
* at typec_unregister_partner (having seen chip->partner != NULL);
* remove thread then unregisters the same partner and unregisters the
* port; irq resumes -> unregister on freed partner -> UAF.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/delay.h>
#include <linux/ktime.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>
#include <linux/completion.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/usb/typec.h>
#include <linux/usb/typec_altmode.h>
#define MAX_ALTMODE 4
struct tyc_typec {
struct typec_port *port;
struct typec_partner *partner;
struct typec_altmode *amode[MAX_ALTMODE];
};
/* ---------------- anx7411 replica (Case 3) ---------------- */
struct tyc_anx {
struct tyc_typec typec;
struct mutex lock; /* analog of ctx->lock: work side only */
struct work_struct work;
struct workqueue_struct *wq;
struct completion work_in_use; /* work parked at its use point */
};
static void anx_unregister_altmode_replica(struct tyc_anx *ctx)
{
int i;
/* anx7411.c:535-549 -- NO ctx->lock (audited) */
for (i = 0; i < MAX_ALTMODE; i++)
if (ctx->typec.amode[i]) {
typec_unregister_altmode(ctx->typec.amode[i]);
ctx->typec.amode[i] = NULL;
}
}
static void anx_unregister_partner_replica(struct tyc_anx *ctx)
{
/* anx7411.c:585-591 -- NO ctx->lock (audited) */
if (ctx->typec.partner) {
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
static bool anx_use_mode; /* tc0: work parks at use point */
static struct tyc_anx *g_anx0; /* tc0 target for kprobe lookup */
/*
* tc0 kprobe target: typec_altmode_update_active is only reached from the
* work's use point in this module (the remove thread never calls it).
* Park here AFTER the work has read amode[i] (audited check-then-use gap).
*/
static int kp_use_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
struct tyc_anx *ctx = g_anx0;
if (!anx_use_mode || !ctx)
return 0;
anx_use_mode = false;
pr_info("TC0: work parked at use point (adev=%px), remove frees now\n",
(void *)regs->di);
complete(&ctx->work_in_use);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0: work resumes use on (possibly freed) adev\n");
return 0;
}
static struct kprobe kp_use = {
.symbol_name = "typec_altmode_update_active",
.pre_handler = kp_use_pre,
};
/* work replica of anx7411_work_func (anx7411.c:906-984) */
static void anx_work_func(struct work_struct *w)
{
struct tyc_anx *ctx = container_of(w, struct tyc_anx, work);
int i;
mutex_lock(&ctx->lock); /* :916 */
/* "update_altmode" check-then-use loop (:602-613) */
for (i = 0; i < MAX_ALTMODE; i++) {
if (!ctx->typec.amode[i])
continue;
pr_info("TC0: work read amode[%d]=%px svid=%04x, calling update_active\n",
i, ctx->typec.amode[i], ctx->typec.amode[i]->svid);
typec_altmode_update_active(ctx->typec.amode[i], true);
}
/* POW_OFF branch (:943-946) */
pr_info("TC0: work unregister altmode+partner\n");
anx_unregister_altmode_replica(ctx);
anx_unregister_partner_replica(ctx);
mutex_unlock(&ctx->lock);
}
static int anx_remove_thread(void *data)
{
struct tyc_anx *ctx = data;
pr_info("TC0*: remove enters (audited order: unregister FIRST)\n");
anx_unregister_altmode_replica(ctx); /* :1566 */
anx_unregister_partner_replica(ctx); /* :1567 */
pr_info("TC0*: remove stops work (too late, audited)\n");
cancel_work_sync(&ctx->work); /* :1571 */
pr_info("TC0*: remove done\n");
return 0;
}
/* one-shot stall at typec_unregister_altmode (tc0b): park the FIRST caller */
static bool oneshot_armed;
static struct completion tc_done_wait;
static int kp_unreg_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC0b: remove parked at typec_unregister_altmode (adev=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0b: parked unregister resumes (device likely freed)\n");
return 0;
}
/* ---------------- stusb160x replica (Case 4) ---------------- */
struct tyc_stu {
struct tyc_typec typec;
struct task_struct *irq_thread;
struct completion irq_parked;
};
static int stu_irq_thread(void *data)
{
struct tyc_stu *ctx = data;
while (!kthread_should_stop()) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(msecs_to_jiffies(20));
if (kthread_should_stop())
break;
/* stusb160x_detach(:388-389) check-then-use */
if (ctx->typec.partner) {
pr_info("TC1: irq saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
return 0;
}
static int stu_remove_thread(void *data)
{
struct tyc_stu *ctx = data;
pr_info("TC1: remove enters (devm-irq window open)\n");
/* stusb160x.c:805-808 */
if (ctx->typec.partner) {
pr_info("TC1: remove saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
pr_info("TC1: remove unregisters port\n");
typec_unregister_port(ctx->typec.port);
ctx->typec.port = NULL;
pr_info("TC1: remove done\n");
return 0;
}
/* ---------------- orchestration ---------------- */
static struct tyc_anx *g_anx;
static struct tyc_stu *g_stu;
static struct kprobe kp_unreg;
static int tc_setup_common(struct tyc_typec *t, struct device *parent)
{
struct typec_capability cap = { };
struct typec_partner_desc pdesc = { };
struct typec_altmode_desc adesc = { };
int i;
cap.type = TYPEC_PORT_DRP;
cap.data = TYPEC_PORT_DRD;
cap.revision = USB_TYPEC_REV_1_2;
cap.driver_data = t;
t->port = typec_register_port(parent, &cap);
if (IS_ERR(t->port)) {
pr_err("typec_register_port failed: %pe\n", t->port);
return PTR_ERR(t->port);
}
t->partner = typec_register_partner(t->port, &pdesc);
if (IS_ERR(t->partner)) {
pr_err("typec_register_partner failed: %pe\n", t->partner);
typec_unregister_port(t->port);
t->port = NULL;
return PTR_ERR(t->partner);
}
for (i = 0; i < 2; i++) {
adesc.svid = 0x1234 + i;
adesc.mode = i + 1;
adesc.vdo = 0xdeadbeef;
adesc.roles = TYPEC_PORT_DRD;
t->amode[i] = typec_partner_register_altmode(t->partner, &adesc);
if (IS_ERR(t->amode[i])) {
pr_err("partner_register_altmode[%d] failed: %pe\n",
i, t->amode[i]);
t->amode[i] = NULL;
return -EFAULT;
}
}
return 0;
}
static int run_tc0(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&g_anx->work_in_use);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwq");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
ret = register_kprobe(&kp_use);
if (ret) {
pr_err("TC0: kprobe(use) failed %d\n", ret);
goto out;
}
g_anx0 = g_anx;
anx_use_mode = true;
/* 1. queue work; it parks inside kprobe at its first use, having
* already read amode[0] (audited check-then-use gap). */
queue_work(g_anx->wq, &g_anx->work);
/* 2. wait until parked */
if (!wait_for_completion_timeout(&g_anx->work_in_use, msecs_to_jiffies(2000))) {
pr_err("TC0: work never parked; abort\n");
goto out;
}
/* 3. remove thread frees altmodes+partner under the parked work */
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm");
if (!IS_ERR(rm))
kthread_stop(rm); /* joins: remove completes inside window */
/* 4. work auto-resumes after 400ms -> UAF */
pr_info("TC0: RESULT see KASAN reports above\n");
msleep(300);
anx_unregister_altmode_replica(g_anx);
anx_unregister_partner_replica(g_anx);
out:
unregister_kprobe(&kp_use);
g_anx0 = NULL;
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int run_tc0b(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0b");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&tc_done_wait);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwqb");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_altmode",
.pre_handler = kp_unreg_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC0b: kprobe failed %d\n", ret);
goto out;
}
/* 1. remove thread parks inside kprobe BEFORE its unregister */
oneshot_armed = true;
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm0b");
if (IS_ERR(rm)) {
pr_err("TC0b: kthread failed\n");
goto out;
}
/* 2. wait parked */
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC0b: remove never parked\n");
kthread_stop(rm);
goto out;
}
/* 3. now let the work run: it unregisters the same altmodes+partner
* (device freed) while remove is parked mid-unregister */
queue_work(g_anx->wq, &g_anx->work);
msleep(100);
/* 4. remove resumes -> unregister on freed device */
pr_info("TC0b: remove resumes at ~300ms\n");
msleep(700); /* let everything settle */
cancel_work_sync(&g_anx->work);
pr_info("TC0b: RESULT see KASAN reports above\n");
out:
unregister_kprobe(&kp_unreg);
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int kp_park_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC1: irq thread parked at typec_unregister_partner (partner=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC1: irq resumes unregister (partner likely freed)\n");
return 0;
}
static int run_tc1(void)
{
struct device *root_dev;
int ret;
root_dev = root_device_register("tycroot1");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_stu = kzalloc(sizeof(*g_stu), GFP_KERNEL);
if (!g_stu) {
root_device_unregister(root_dev);
return -ENOMEM;
}
init_completion(&tc_done_wait);
init_completion(&g_stu->irq_parked);
ret = tc_setup_common(&g_stu->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_partner",
.pre_handler = kp_park_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC1: kprobe failed %d\n", ret);
goto out;
}
/* 1. irq thread parks at its unregister (partner still alive) */
oneshot_armed = true;
g_stu->irq_thread = kthread_run(stu_irq_thread, g_stu, "tyc-stuirq");
if (IS_ERR(g_stu->irq_thread)) {
g_stu->irq_thread = NULL;
goto out;
}
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC1: irq never parked\n");
goto out;
}
/* 2. remove sequence inline on the insmod thread (partner still
* alive -> unregister frees it, then port); parked irq thread is
* on another CPU */
stu_remove_thread(g_stu);
/* 3. irq resumes -> unregister on freed partner */
pr_info("TC1: RESULT see KASAN reports above\n");
msleep(300);
out:
unregister_kprobe(&kp_unreg);
if (g_stu) {
if (g_stu->irq_thread)
kthread_stop(g_stu->irq_thread);
kfree(g_stu);
g_stu = NULL;
}
root_device_unregister(root_dev);
return 0;
}
/* ---------------- tc2: Case 1-B deadlock structure replica ----------------
* Faithful structure replica of the audited UCSI unbind deadlock:
* - unbind thread holds "con->lock" and calls cancel_delayed_work_sync()
* (ucsi.c:2003-2004 -> mode_selection.c:277);
* - the mode-selection work, inside its activate window, must take the
* same lock via the ucsi_con_mutex_lock() trylock loop whose exit
* condition is the STALE con->status CONNECTED snapshot
* (ucsi.c:2272-2277; ucsi.h:457 reads ->status, never refreshed on the
* unbind path).
* Expected result: both sides park forever; the VM hangs until timeout.
*/
struct tyc_dead {
struct mutex lock; /* con->lock analog */
bool connected_snapshot; /* con->status CONNECTED bit (stale) */
struct delayed_work work;
struct completion in_loop; /* handshake: work inside trylock loop */
bool ctl; /* ctl=true: lock first, queue later */
};
static void dead_work_fn(struct work_struct *w)
{
struct tyc_dead *d = container_of(w, struct tyc_dead, work.work);
bool locked = false;
pr_info("TC2: work entered activate window, needs con->lock\n");
/* first trylock fails: unbind already holds the lock */
locked = mutex_trylock(&d->lock) != 0;
if (!locked)
complete(&d->in_loop);
/* ucsi_con_mutex_lock() loop, ucsi.c:2272-2277, verbatim shape */
while (d->connected_snapshot && !locked) {
locked = mutex_trylock(&d->lock) != 0;
if (d->connected_snapshot && !locked) {
msleep(20);
}
}
if (locked)
mutex_unlock(&d->lock);
pr_info("TC2: work finished\n");
}
/* audited interleaving: work enters the loop first, unbind then takes the
* lock and cancels -- expected permanent hang (VM killed by timeout). */
static int run_tc2(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true; /* cable connected snapshot */
d->ctl = false;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
/* audited order: unbind holds con->lock FIRST, work starts and
* fails its trylocks while the lock stays held */
mutex_lock(&d->lock);
pr_info("TC2: unbind holds con->lock; scheduling work\n");
schedule_delayed_work(&d->work, 0);
if (!wait_for_completion_timeout(&d->in_loop, msecs_to_jiffies(2000))) {
pr_err("TC2: work never entered loop\n");
mutex_unlock(&d->lock);
kfree(d);
return -ETIMEDOUT;
}
pr_info("TC2: work looping on failed trylocks; unbind calls cancel (expect hang)\n");
cancel_delayed_work_sync(&d->work); /* audited hang */
pr_info("TC2: cancel returned (should never print)\n");
mutex_unlock(&d->lock);
kfree(d);
return 0;
}
/* control: unbind takes the lock BEFORE the work ever runs; cancel() kills
* the pending timer -- no deadlock (audited escape-branch 1). */
static int run_tc2ctl(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true;
d->ctl = true;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
mutex_lock(&d->lock);
pr_info("TC2CTL: unbind holds lock first, queueing work\n");
schedule_delayed_work(&d->work, msecs_to_jiffies(50));
cancel_delayed_work_sync(&d->work); /* pending -> killed */
pr_info("TC2CTL: cancel returned (expected, pending branch)\n");
mutex_unlock(&d->lock);
msleep(200);
kfree(d);
return 0;
}
/* ---------------- tcnat: natural hammering (no probes) ---------------- */
static int tcnat_rounds = 1000;
module_param(tcnat_rounds, int, 0444);
static int run_tcnat(void)
{
struct device *root_dev;
int round;
for (round = 0; round < tcnat_rounds; round++) {
root_dev = root_device_register("tycrootnat");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycnatwq");
if (tc_setup_common(&g_anx->typec, root_dev)) {
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
return -EFAULT;
}
/* natural race: queue work, then immediately remove */
queue_work(g_anx->wq, &g_anx->work);
anx_remove_thread(g_anx);
cancel_work_sync(&g_anx->work);
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
if (!(round % 100))
pr_info("TCNAT: round %d done\n", round);
}
pr_info("TCNAT: %d rounds done (check dmesg for KASAN reports)\n",
tcnat_rounds);
return 0;
}
/* ---------------- module params & entry ---------------- */
static char *action = "status";
module_param(action, charp, 0444);
MODULE_PARM_DESC(action, "tc0|tc0b|tc1|tc2|tcnat|status");
static int tyc_run_action(void)
{
if (!strcmp(action, "tc0"))
return run_tc0();
if (!strcmp(action, "tc0b"))
return run_tc0b();
if (!strcmp(action, "tc1"))
return run_tc1();
if (!strcmp(action, "tc2"))
return run_tc2();
if (!strcmp(action, "tc2ctl"))
return run_tc2ctl();
if (!strcmp(action, "tcnat"))
return run_tcnat();
pr_info("no action, done\n");
return 0;
}
static int __init tyc_init(void)
{
pr_info("action=%s\n", action);
return tyc_run_action();
}
module_init(tyc_init);
static void __exit tyc_exit(void)
{
pr_info("exit\n");
}
module_exit(tyc_exit);
MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 10+ messages in thread
* usb: typec: stusb160x/rt1719/hd3ss3220: IRQ thread vs remove -- double unregister / use-after-free on the partner and port
2026-10-27 14:12 ` Greg KH
` (2 preceding siblings ...)
2026-11-04 12:58 ` usb: typec: ucsi: deadlock on cros_ec unbind -- connector lock held across cancel_delayed_work_sync vs the mode-selection work's unbounded trylock polling 徐晗翔
@ 2026-11-04 13:00 ` 徐晗翔
2026-11-04 13:01 ` usb: typec: ucsi: displayport: data race on dp->header/ vdo_data/vdo_size -- work reads them unlocked while writers hold con->lock (VDM mismatch) 徐晗翔
2026-11-04 13:02 ` usb: typec: class: three lifetime/locking gaps around the mux/switch handles and mode_selection (drvdata UAF after provider unbind; no concurrent protocol for set/put; unlocked state_update vs delete) 徐晗翔
5 siblings, 0 replies; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 13:00 UTC (permalink / raw)
To: heikki.krogerus; +Cc: linux-usb, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 5091 bytes --]
Three typec controller drivers share one teardown defect: their IRQ
thread mutates the registered partner (typec_register_partner /
typec_unregister_partner, and data-role setting) with no
synchronization against .remove(), and the IRQ itself is devm-req-
uested -- so it keeps running until AFTER .remove returns and the
port/partner objects may already be gone. stusb160x_remove() even
performs the same unlocked read-then-unregister on chip->partner
(:805-808) that the IRQ thread performs (:388-389): both sides' "see
non-NULL -> unregister" pairs race, producing a double
typec_unregister_partner on a possibly freed partner (use-after-free
writes inside the class layer), and the trailing
typec_unregister_port() frees the port while the IRQ thread may
still hold the stale port pointer. rt1719_remove() does not touch
the partner at all, so its IRQ thread can unregister the partner
after remove and even re-ATTACH one onto the removed port (orphan),
calling typec_set_pwr_opmode() on the freed port (sysfs_notify on a
dead kobject). hd3ss3220 (IRQ mode) races typec_set_data_role()
against the port teardown the same way. I verified the core
interleaving on a pristine Linux 7.3 KASAN kernel with a
statement-level replica of the stusb160x shape over the NATIVE typec
class and device core: an IRQ thread parked right after seeing a
non-NULL partner, then the remove side unregistering partner+port,
then the resumed IRQ unregistering on the freed partner -- four
typec_unregister_partner UAF reports, a freed-mutex wait, a kernfs
NULL-deref and two Oopses, with the timeline showing both sides
reading the SAME partner pointer before the free. The fix is the
ordering the sibling driver wusb3801 already uses: stop/wait the IRQ
(free_irq, which waits for a running ONESHOT thread) before touching
partner/port in remove.
The detailed description:
* The three drivers (Linux 7.3 line numbers)
- stusb160x: IRQ thread stusb160x_irq_handler (:402-429) calls
stusb160x_attach (:416 -> :364 chip->partner = typec_register_
partner(...)) / stusb160x_detach (:418 -> :388-389 unregister +
NULL) directly; stusb160x_remove (:800-815) does the same
unlocked operations at :805-808 and unregisters the port at
:815; devm IRQ at :452.
- rt1719: rt1719_irq_handler (:586-624) calls rt1719_attach
(:611; re-register at :247 after a defensive pre-unregister
:238-240) / rt1719_detach (:614 -> :288); rt1719_remove
(:928-933) only unregisters the port; devm IRQ :645.
- hd3ss3220 (IRQ mode): hd3ss3220_irq_handler (:280-286) ->
hd3ss3220_set_role (:226-241) -> typec_set_data_role (:237/:240);
remove (:522-532) cancels only the polling-mode worker (:527)
and unregisters the port (:529); devm IRQ :438/:489.
Why nothing protects this: devres_release_all runs after .remove
returns (the window covers the whole remove body); IRQF_ONESHOT
only prevents re-entry of the thread, not its concurrency with
remove; typec_unregister_partner()'s port->partner_link_lock
(class.c:1183-1194) orders the class-side list ops but neither
guards the driver's chip->partner pointer nor the port's
lifetime; and after the port is freed the IRQ thread's port
pointer is dangling (typec_port_release kfrees it).
* Evidence (pristine 7.3 KASAN; module attached)
The stusb160x-shape replica (stu_irq/stu_remove in the module)
over the native class layer: the IRQ thread is parked right after
it observed chip->partner != NULL; the remove side then reads the
same non-NULL pointer, unregisters partner and port; on release,
the IRQ thread's typec_unregister_partner() lands on the freed
partner:
irq saw partner ... parked
remove saw partner (same pointer) -> unregistered + port gone
(400 ms later) irq resumes ->
BUG: KASAN: slab-use-after-free in
typec_unregister_partner+0x51/+0xd6 (x4)
+ mutex_lock on freed lock + kernfs_find_and_get_ns NULL-deref
+ Oops x2
Fidelity: typec_register/unregister_partner, the port release and
the whole device core are native; the driver's dual
read-then-unregister pairs are replicated statement-by-statement
per stusb160x.c:388-389/:800-815.
Module usage: insmod typcrace.ko; action=tc1 (the stusb160x
choreography). On real hardware the shape is reached by unbinding
the i2c driver while a cable attach/detach event is in flight --
plain plug/unplug traffic suffices to have the IRQ thread inside
attach/detach at unbind time.
* Suggested fix
Stop the IRQ before mutating partner/port in remove:
stusb160x: free_irq() (or disable+cancel) first, then the
:805-808 pair and the port unregister; rt1719: free_irq() first,
and make remove also detach the partner (or have the IRQ thread
check a removing flag); hd3ss3220: free_irq() in both modes
before the port unregister. The correct-order precedent is
wusb3801.c:402-414, which free_irq()s -- waiting for the in-flight
ONESHOT thread -- before touching partner/port.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: typcrace.c --]
[-- Type: text/plain, Size: 18699 bytes --]
// SPDX-License-Identifier: GPL-2.0
/*
* typcrace.c - dynamic verification module for drivers/usb/typec audit
* (docs/manual-review-drivers-usb-typec-2026-11-02.round3.md)
*
* Statement-faithful replicas of the audited driver state machines (anx7411,
* stusb160x), driven via the exported typec class API on a fake typec_port,
* with one-shot kprobe stalls making the audited interleavings deterministic.
* Kernel source unmodified (build-kasan-typec-7.3).
*
* tc0 - audit Case 3 interleaving 1: anx7411 work uses amode[] pointer
* (check-then-use, anx7411.c:602-613) while remove frees the device.
* Work is parked by a kprobe at its use point
* (typec_altmode_update_active) AFTER it read amode[i]; the remove
* thread unregisters (frees) the altmode during the park; on resume
* the use dereferences freed memory -> KASAN UAF.
* tc0b - audit Case 3 interleaving 2: double device_unregister. Remove is
* parked by a one-shot kprobe at typec_unregister_altmode BEFORE it
* executes; the queued work then unregisters the same altmode
* (device freed); on resume remove's unregister runs on the freed
* device -> UAF inside typec_unregister_altmode/class core.
* tc1 - audit Case 4 (stusb160x form): irq thread parked by one-shot kprobe
* at typec_unregister_partner (having seen chip->partner != NULL);
* remove thread then unregisters the same partner and unregisters the
* port; irq resumes -> unregister on freed partner -> UAF.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/delay.h>
#include <linux/ktime.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>
#include <linux/completion.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/usb/typec.h>
#include <linux/usb/typec_altmode.h>
#define MAX_ALTMODE 4
struct tyc_typec {
struct typec_port *port;
struct typec_partner *partner;
struct typec_altmode *amode[MAX_ALTMODE];
};
/* ---------------- anx7411 replica (Case 3) ---------------- */
struct tyc_anx {
struct tyc_typec typec;
struct mutex lock; /* analog of ctx->lock: work side only */
struct work_struct work;
struct workqueue_struct *wq;
struct completion work_in_use; /* work parked at its use point */
};
static void anx_unregister_altmode_replica(struct tyc_anx *ctx)
{
int i;
/* anx7411.c:535-549 -- NO ctx->lock (audited) */
for (i = 0; i < MAX_ALTMODE; i++)
if (ctx->typec.amode[i]) {
typec_unregister_altmode(ctx->typec.amode[i]);
ctx->typec.amode[i] = NULL;
}
}
static void anx_unregister_partner_replica(struct tyc_anx *ctx)
{
/* anx7411.c:585-591 -- NO ctx->lock (audited) */
if (ctx->typec.partner) {
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
static bool anx_use_mode; /* tc0: work parks at use point */
static struct tyc_anx *g_anx0; /* tc0 target for kprobe lookup */
/*
* tc0 kprobe target: typec_altmode_update_active is only reached from the
* work's use point in this module (the remove thread never calls it).
* Park here AFTER the work has read amode[i] (audited check-then-use gap).
*/
static int kp_use_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
struct tyc_anx *ctx = g_anx0;
if (!anx_use_mode || !ctx)
return 0;
anx_use_mode = false;
pr_info("TC0: work parked at use point (adev=%px), remove frees now\n",
(void *)regs->di);
complete(&ctx->work_in_use);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0: work resumes use on (possibly freed) adev\n");
return 0;
}
static struct kprobe kp_use = {
.symbol_name = "typec_altmode_update_active",
.pre_handler = kp_use_pre,
};
/* work replica of anx7411_work_func (anx7411.c:906-984) */
static void anx_work_func(struct work_struct *w)
{
struct tyc_anx *ctx = container_of(w, struct tyc_anx, work);
int i;
mutex_lock(&ctx->lock); /* :916 */
/* "update_altmode" check-then-use loop (:602-613) */
for (i = 0; i < MAX_ALTMODE; i++) {
if (!ctx->typec.amode[i])
continue;
pr_info("TC0: work read amode[%d]=%px svid=%04x, calling update_active\n",
i, ctx->typec.amode[i], ctx->typec.amode[i]->svid);
typec_altmode_update_active(ctx->typec.amode[i], true);
}
/* POW_OFF branch (:943-946) */
pr_info("TC0: work unregister altmode+partner\n");
anx_unregister_altmode_replica(ctx);
anx_unregister_partner_replica(ctx);
mutex_unlock(&ctx->lock);
}
static int anx_remove_thread(void *data)
{
struct tyc_anx *ctx = data;
pr_info("TC0*: remove enters (audited order: unregister FIRST)\n");
anx_unregister_altmode_replica(ctx); /* :1566 */
anx_unregister_partner_replica(ctx); /* :1567 */
pr_info("TC0*: remove stops work (too late, audited)\n");
cancel_work_sync(&ctx->work); /* :1571 */
pr_info("TC0*: remove done\n");
return 0;
}
/* one-shot stall at typec_unregister_altmode (tc0b): park the FIRST caller */
static bool oneshot_armed;
static struct completion tc_done_wait;
static int kp_unreg_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC0b: remove parked at typec_unregister_altmode (adev=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0b: parked unregister resumes (device likely freed)\n");
return 0;
}
/* ---------------- stusb160x replica (Case 4) ---------------- */
struct tyc_stu {
struct tyc_typec typec;
struct task_struct *irq_thread;
struct completion irq_parked;
};
static int stu_irq_thread(void *data)
{
struct tyc_stu *ctx = data;
while (!kthread_should_stop()) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(msecs_to_jiffies(20));
if (kthread_should_stop())
break;
/* stusb160x_detach(:388-389) check-then-use */
if (ctx->typec.partner) {
pr_info("TC1: irq saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
return 0;
}
static int stu_remove_thread(void *data)
{
struct tyc_stu *ctx = data;
pr_info("TC1: remove enters (devm-irq window open)\n");
/* stusb160x.c:805-808 */
if (ctx->typec.partner) {
pr_info("TC1: remove saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
pr_info("TC1: remove unregisters port\n");
typec_unregister_port(ctx->typec.port);
ctx->typec.port = NULL;
pr_info("TC1: remove done\n");
return 0;
}
/* ---------------- orchestration ---------------- */
static struct tyc_anx *g_anx;
static struct tyc_stu *g_stu;
static struct kprobe kp_unreg;
static int tc_setup_common(struct tyc_typec *t, struct device *parent)
{
struct typec_capability cap = { };
struct typec_partner_desc pdesc = { };
struct typec_altmode_desc adesc = { };
int i;
cap.type = TYPEC_PORT_DRP;
cap.data = TYPEC_PORT_DRD;
cap.revision = USB_TYPEC_REV_1_2;
cap.driver_data = t;
t->port = typec_register_port(parent, &cap);
if (IS_ERR(t->port)) {
pr_err("typec_register_port failed: %pe\n", t->port);
return PTR_ERR(t->port);
}
t->partner = typec_register_partner(t->port, &pdesc);
if (IS_ERR(t->partner)) {
pr_err("typec_register_partner failed: %pe\n", t->partner);
typec_unregister_port(t->port);
t->port = NULL;
return PTR_ERR(t->partner);
}
for (i = 0; i < 2; i++) {
adesc.svid = 0x1234 + i;
adesc.mode = i + 1;
adesc.vdo = 0xdeadbeef;
adesc.roles = TYPEC_PORT_DRD;
t->amode[i] = typec_partner_register_altmode(t->partner, &adesc);
if (IS_ERR(t->amode[i])) {
pr_err("partner_register_altmode[%d] failed: %pe\n",
i, t->amode[i]);
t->amode[i] = NULL;
return -EFAULT;
}
}
return 0;
}
static int run_tc0(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&g_anx->work_in_use);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwq");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
ret = register_kprobe(&kp_use);
if (ret) {
pr_err("TC0: kprobe(use) failed %d\n", ret);
goto out;
}
g_anx0 = g_anx;
anx_use_mode = true;
/* 1. queue work; it parks inside kprobe at its first use, having
* already read amode[0] (audited check-then-use gap). */
queue_work(g_anx->wq, &g_anx->work);
/* 2. wait until parked */
if (!wait_for_completion_timeout(&g_anx->work_in_use, msecs_to_jiffies(2000))) {
pr_err("TC0: work never parked; abort\n");
goto out;
}
/* 3. remove thread frees altmodes+partner under the parked work */
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm");
if (!IS_ERR(rm))
kthread_stop(rm); /* joins: remove completes inside window */
/* 4. work auto-resumes after 400ms -> UAF */
pr_info("TC0: RESULT see KASAN reports above\n");
msleep(300);
anx_unregister_altmode_replica(g_anx);
anx_unregister_partner_replica(g_anx);
out:
unregister_kprobe(&kp_use);
g_anx0 = NULL;
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int run_tc0b(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0b");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&tc_done_wait);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwqb");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_altmode",
.pre_handler = kp_unreg_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC0b: kprobe failed %d\n", ret);
goto out;
}
/* 1. remove thread parks inside kprobe BEFORE its unregister */
oneshot_armed = true;
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm0b");
if (IS_ERR(rm)) {
pr_err("TC0b: kthread failed\n");
goto out;
}
/* 2. wait parked */
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC0b: remove never parked\n");
kthread_stop(rm);
goto out;
}
/* 3. now let the work run: it unregisters the same altmodes+partner
* (device freed) while remove is parked mid-unregister */
queue_work(g_anx->wq, &g_anx->work);
msleep(100);
/* 4. remove resumes -> unregister on freed device */
pr_info("TC0b: remove resumes at ~300ms\n");
msleep(700); /* let everything settle */
cancel_work_sync(&g_anx->work);
pr_info("TC0b: RESULT see KASAN reports above\n");
out:
unregister_kprobe(&kp_unreg);
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int kp_park_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC1: irq thread parked at typec_unregister_partner (partner=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC1: irq resumes unregister (partner likely freed)\n");
return 0;
}
static int run_tc1(void)
{
struct device *root_dev;
int ret;
root_dev = root_device_register("tycroot1");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_stu = kzalloc(sizeof(*g_stu), GFP_KERNEL);
if (!g_stu) {
root_device_unregister(root_dev);
return -ENOMEM;
}
init_completion(&tc_done_wait);
init_completion(&g_stu->irq_parked);
ret = tc_setup_common(&g_stu->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_partner",
.pre_handler = kp_park_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC1: kprobe failed %d\n", ret);
goto out;
}
/* 1. irq thread parks at its unregister (partner still alive) */
oneshot_armed = true;
g_stu->irq_thread = kthread_run(stu_irq_thread, g_stu, "tyc-stuirq");
if (IS_ERR(g_stu->irq_thread)) {
g_stu->irq_thread = NULL;
goto out;
}
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC1: irq never parked\n");
goto out;
}
/* 2. remove sequence inline on the insmod thread (partner still
* alive -> unregister frees it, then port); parked irq thread is
* on another CPU */
stu_remove_thread(g_stu);
/* 3. irq resumes -> unregister on freed partner */
pr_info("TC1: RESULT see KASAN reports above\n");
msleep(300);
out:
unregister_kprobe(&kp_unreg);
if (g_stu) {
if (g_stu->irq_thread)
kthread_stop(g_stu->irq_thread);
kfree(g_stu);
g_stu = NULL;
}
root_device_unregister(root_dev);
return 0;
}
/* ---------------- tc2: Case 1-B deadlock structure replica ----------------
* Faithful structure replica of the audited UCSI unbind deadlock:
* - unbind thread holds "con->lock" and calls cancel_delayed_work_sync()
* (ucsi.c:2003-2004 -> mode_selection.c:277);
* - the mode-selection work, inside its activate window, must take the
* same lock via the ucsi_con_mutex_lock() trylock loop whose exit
* condition is the STALE con->status CONNECTED snapshot
* (ucsi.c:2272-2277; ucsi.h:457 reads ->status, never refreshed on the
* unbind path).
* Expected result: both sides park forever; the VM hangs until timeout.
*/
struct tyc_dead {
struct mutex lock; /* con->lock analog */
bool connected_snapshot; /* con->status CONNECTED bit (stale) */
struct delayed_work work;
struct completion in_loop; /* handshake: work inside trylock loop */
bool ctl; /* ctl=true: lock first, queue later */
};
static void dead_work_fn(struct work_struct *w)
{
struct tyc_dead *d = container_of(w, struct tyc_dead, work.work);
bool locked = false;
pr_info("TC2: work entered activate window, needs con->lock\n");
/* first trylock fails: unbind already holds the lock */
locked = mutex_trylock(&d->lock) != 0;
if (!locked)
complete(&d->in_loop);
/* ucsi_con_mutex_lock() loop, ucsi.c:2272-2277, verbatim shape */
while (d->connected_snapshot && !locked) {
locked = mutex_trylock(&d->lock) != 0;
if (d->connected_snapshot && !locked) {
msleep(20);
}
}
if (locked)
mutex_unlock(&d->lock);
pr_info("TC2: work finished\n");
}
/* audited interleaving: work enters the loop first, unbind then takes the
* lock and cancels -- expected permanent hang (VM killed by timeout). */
static int run_tc2(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true; /* cable connected snapshot */
d->ctl = false;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
/* audited order: unbind holds con->lock FIRST, work starts and
* fails its trylocks while the lock stays held */
mutex_lock(&d->lock);
pr_info("TC2: unbind holds con->lock; scheduling work\n");
schedule_delayed_work(&d->work, 0);
if (!wait_for_completion_timeout(&d->in_loop, msecs_to_jiffies(2000))) {
pr_err("TC2: work never entered loop\n");
mutex_unlock(&d->lock);
kfree(d);
return -ETIMEDOUT;
}
pr_info("TC2: work looping on failed trylocks; unbind calls cancel (expect hang)\n");
cancel_delayed_work_sync(&d->work); /* audited hang */
pr_info("TC2: cancel returned (should never print)\n");
mutex_unlock(&d->lock);
kfree(d);
return 0;
}
/* control: unbind takes the lock BEFORE the work ever runs; cancel() kills
* the pending timer -- no deadlock (audited escape-branch 1). */
static int run_tc2ctl(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true;
d->ctl = true;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
mutex_lock(&d->lock);
pr_info("TC2CTL: unbind holds lock first, queueing work\n");
schedule_delayed_work(&d->work, msecs_to_jiffies(50));
cancel_delayed_work_sync(&d->work); /* pending -> killed */
pr_info("TC2CTL: cancel returned (expected, pending branch)\n");
mutex_unlock(&d->lock);
msleep(200);
kfree(d);
return 0;
}
/* ---------------- tcnat: natural hammering (no probes) ---------------- */
static int tcnat_rounds = 1000;
module_param(tcnat_rounds, int, 0444);
static int run_tcnat(void)
{
struct device *root_dev;
int round;
for (round = 0; round < tcnat_rounds; round++) {
root_dev = root_device_register("tycrootnat");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycnatwq");
if (tc_setup_common(&g_anx->typec, root_dev)) {
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
return -EFAULT;
}
/* natural race: queue work, then immediately remove */
queue_work(g_anx->wq, &g_anx->work);
anx_remove_thread(g_anx);
cancel_work_sync(&g_anx->work);
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
if (!(round % 100))
pr_info("TCNAT: round %d done\n", round);
}
pr_info("TCNAT: %d rounds done (check dmesg for KASAN reports)\n",
tcnat_rounds);
return 0;
}
/* ---------------- module params & entry ---------------- */
static char *action = "status";
module_param(action, charp, 0444);
MODULE_PARM_DESC(action, "tc0|tc0b|tc1|tc2|tcnat|status");
static int tyc_run_action(void)
{
if (!strcmp(action, "tc0"))
return run_tc0();
if (!strcmp(action, "tc0b"))
return run_tc0b();
if (!strcmp(action, "tc1"))
return run_tc1();
if (!strcmp(action, "tc2"))
return run_tc2();
if (!strcmp(action, "tc2ctl"))
return run_tc2ctl();
if (!strcmp(action, "tcnat"))
return run_tcnat();
pr_info("no action, done\n");
return 0;
}
static int __init tyc_init(void)
{
pr_info("action=%s\n", action);
return tyc_run_action();
}
module_init(tyc_init);
static void __exit tyc_exit(void)
{
pr_info("exit\n");
}
module_exit(tyc_exit);
MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 10+ messages in thread
* usb: typec: ucsi: displayport: data race on dp->header/ vdo_data/vdo_size -- work reads them unlocked while writers hold con->lock (VDM mismatch)
2026-10-27 14:12 ` Greg KH
` (3 preceding siblings ...)
2026-11-04 13:00 ` usb: typec: stusb160x/rt1719/hd3ss3220: IRQ thread vs remove -- double unregister / use-after-free on the partner and port 徐晗翔
@ 2026-11-04 13:01 ` 徐晗翔
2026-11-04 13:02 ` usb: typec: class: three lifetime/locking gaps around the mux/switch handles and mode_selection (drvdata UAF after provider unbind; no concurrent protocol for set/put; unlocked state_update vs delete) 徐晗翔
5 siblings, 0 replies; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 13:01 UTC (permalink / raw)
To: heikki.krogerus; +Cc: linux-usb, linux-kernel
The UCSI displayport altmode's reply triple (dp->header / vdo_data /
vdo_size, ucsi/displayport.c:28-30) is written by three writers that
all hold con->lock (enter :97-102, exit :142-147, vdm :234+) but read
by ucsi_displayport_work (:282-295) with NO lock at all -- the work
is scheduled asynchronously (:104/:149/:265) and never acquires
con->lock, so the lock only serializes the writers among themselves
and does not cover the reader. When the worker runs while a writer
updates the triple (the mode_selection activate path entering
ucsi_displayport_enter, or the next EC event arriving while the
worker is inside typec_altmode_vdm -> the partner driver), the VDM
message assembled from the mixed fields is sent to the partner
driver: a stale/mixed header with fresh data or vice versa -- a DP
state machine glitch that the partner sees as a malformed or
misdirected VDM (self-healing on the next event, but still a protocol
violation and a plain KCSAN data race). Lifetime is NOT at risk
(vdo_data only ever points at the persistent &dp->data.status or
NULL; the worker is cancel_work_sync'ed before teardown), so this is
a small locking fix: read/clear the triple under the same lock (or a
dedicated dp->lock) in the worker.
The detailed description:
* The racy pair (Linux 7.3, drivers/usb/typec/ucsi/displayport.c)
Writers (all inside ucsi_con_mutex_lock ... unlock, :57/:119/:209):
- enter path: :97-102 sets dp->header = ENTER_ACK (with
vdo_data/vdo_size) and schedules dp->work (:104);
- exit path: :142-147 + :149;
- vdm path: :234+ (with :183-184 for the ATTENTION-derived
variant) + :265.
Reader (no lock): ucsi_displayport_work (:282-295) reads
dp->header / dp->vdo_data / dp->vdo_size (:287-288) and clears
them (:292-294), then calls typec_altmode_vdm(dp->alt, ...) ->
bus.c:214-235 -> the partner driver's dp_altmode_vdm
(altmodes/displayport.c:372-443, under the driver's own dp->lock).
The worker never takes con->lock, and nothing stops a writer from
taking con->lock while the unlocked worker runs (the mode_selection
activate path enters ucsi_displayport_enter without holding
con->lock -- it takes it inside). schedule_work coalescing does
not close the window.
* Concrete interleavings
1. mode_selection activate vs an in-flight worker: the activate
path (thread A) writes ENTER_ACK and schedules; the previous
round's worker (thread B) is concurrently reading the old
header/vdo_data at :287-288 -- the partner receives a mixed
old/new VDM.
2. back-to-back vdm events: the worker is inside
typec_altmode_vdm (which enters the partner driver and takes
its dp->lock -- nonzero time), the next EC event drives
ucsi_displayport_vdm to overwrite the triple under con->lock --
same mixed-VDM outcome.
* Scope / severity
vdo_data only ever points at the persistent &dp->data.status (or
NULL, :101/:146/:183) and vdo_size is a u8 -- no dangling pointer,
no UAF; the worker is drained by ucsi_displayport_remove_partner's
cancel_work_sync (:297-313, :308) before the altmode teardown
(verified: ucsi.c:834-841 ordering). So the impact is: KCSAN
data-race reports plus transient VDM mismatches (partner-side DP
state machine warnings/retries; self-healing). Severity low; the
fix is small.
* Suggested fix
Have ucsi_displayport_work() snapshot the triple under
ucsi_con_mutex_lock(dp->con) (or a new dedicated dp->lock shared by
writers and the worker), then send the snapshot outside the lock.
Alternatively, writers coalesce under the lock with a
cancel_work_sync before overwriting -- more costly; the snapshot
is the clean minimal fix.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
^ permalink raw reply [flat|nested] 10+ messages in thread
* usb: typec: class: three lifetime/locking gaps around the mux/switch handles and mode_selection (drvdata UAF after provider unbind; no concurrent protocol for set/put; unlocked state_update vs delete)
2026-10-27 14:12 ` Greg KH
` (4 preceding siblings ...)
2026-11-04 13:01 ` usb: typec: ucsi: displayport: data race on dp->header/ vdo_data/vdo_size -- work reads them unlocked while writers hold con->lock (VDM mismatch) 徐晗翔
@ 2026-11-04 13:02 ` 徐晗翔
5 siblings, 0 replies; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 13:02 UTC (permalink / raw)
To: heikki.krogerus; +Cc: linux-usb, linux-kernel
The typec class layer leaves the lifetime of its mux/switch machinery
to caller discipline that nothing enforces or documents, and three
concrete gaps fall out of that. The reportable one is the provider
drvdata gap: a mux/switch handle holder keeps a device reference on
the provider's class device (class_find_device() takes get_device,
drivers/base/class.c:470), so after the PROVIDER driver unbinds,
typec_mux_unregister() only does device_unregister -- the mux_dev
shell (and its ->set callback pointer) survives as long as any owner
holds a handle, while the provider's devm private data is freed the
moment its .remove returns. The next orientation/mode change then
calls mux_dev->set() (mux.c:393-412) into e.g.
qmp_combo_typec_mux_set() (phy-qcom-qmp-combo.c:4804) or
rk_udphy_typec_mux_set() (phy-rockchip-usbdp.c:1346), which fetch
devm drvdata of a freed object -- use-after-free read/write with
KASAN visibility; the same shape applies to the switch/retimer-side
providers with explicit removes (ptn36502, nb7vpq904m). Two more
gaps in the same layer are contract-shaped and currently shielded
only by callers: (a) the aggregate typec_mux/typec_switch handles are
freshly allocated per get with no refcount/lock/RCU, typec_mux_set()
walks the callback list unlocked and typec_mux_put() kfrees on the
final put (the one in-tree violator of the resulting "drain before
put" discipline -- pmic_glink_altmode -- is reported separately); and
(b) mode_selection.c's exported state_update/delete pair has no
common lock or lifetime protocol at all (delete: partner->sel = NULL,
cancel_delayed_work_sync, mode_list_clean -- which does NOT take
sel->lock -- mutex_destroy, kfree; update: unlocked read of
partner->sel then mutex_lock + list access; today's only protection
is that every in-tree caller holds ucsi's con->lock, per the
enumeration below). Fixes: a dead-flag + -ENODEV check in the set
path for (the drvdata gap), a documented drain+put contract (or a
class-layer primitive) for the handle protocol, and a lock/lifetime
contract (or in-delete locking) for mode_selection.
The detailed description:
* Gap 1 (reportable UAF): provider drvdata outlived by the handle
Linux 7.3: the owner takes the handle via class_find_device()
(mux.c:295) which get_device()s the mux_dev (class.c:470); the
final typec_mux_put() (mux.c:376-391) drops it; the provider's
unregister (mux.c:478-483) only device_unregister()s. So:
provider unbind: .remove -> typec_mux_unregister (shell kept)
.remove returns -> devres releases the
provider's private data (qmp/udphy/ptn)
later mode/orientation change on the owner:
typec_mux_set(handle) -> mux_dev->set(...)
-> typec_mux_get_drvdata() -> FREED data
Instances with this devm-drvdata pattern: qcom-qmp-combo (set
:4804-4806, devm qmp, register :4924, unregister devm action
:4894-4898/:4930), rockchip-usbdp (set :1346, register :1413-1415,
unregister :1402-1406); ptn36502/nb7vpq904m have explicit removes
but devm priv/regmap, and their own put does not cover an owner
still holding the handle. Nothing else closes this: module refs
pin code; the parent-device reference pins the provider device
struct, not its devm data; the unregister's device_del has no
kernfs-drain semantics for this path (set is reached via the
owner's private pointer, not sysfs).
Fix: make the class layer mark the mux/switch dead at unregister
(WRITE_ONCE flag) and have typec_mux_set()/typec_switch_set()
return -ENODEV on it (READ_ONCE), or provide a drain primitive;
a transitional provider-side option is to allocate the drvdata
with the class device so the lifetimes coincide.
* Gap 2 (contract): aggregate handles have no concurrent protocol
mux.c allocates a fresh aggregate handle per get (:82-129/:320-368)
with no refcount/lock/RCU; typec_mux_set() (:393-412) walks the
callback list unlocked; typec_mux_put() kfrees on the final put;
and typec_unregister_altmode() even suggests the fragile order
(put at class.c:712 before device_unregister at :713). I audited
all owners: UCSI/cros/tipd/gaokun serialize set-vs-put by their
own construction (work drains, free_irq, notifier unregister --
tabulated in my notes; the one in-tree violator,
pmic_glink_altmode, has no cancel/flush at all and is reported
separately). The retimer class does it right (kref'd device,
retimer.c:65-81) -- the mux/switch aggregate handles are the
historical weak spot. Fix: document the drain+put discipline in
include/linux/usb/typec_mux.h (it currently says nothing about
concurrency) or provide a class-layer drain+put primitive.
* Gap 3 (latent): mode_selection state_update vs delete
mode_selection.c:176-197 (state_update: unlocked read of
partner->sel at :179, then mutex_lock(&sel->lock) + list access)
vs :271-283 (delete: partner->sel = NULL; cancel_delayed_work_sync
(covers only that work); mode_list_clean() at :102-110 which does
NOT take sel->lock and kfrees every node; mutex_destroy(); kfree
(sel)). Any overlap = UAF (locking a destroyed mutex; racing the
list cleanup). Today unreachable: the single update entry
(ucsi_altmode_update_active, ucsi.c:354-384) is only called under
con->lock (ucsi.c:1029/:1522/displayport.c:255 via the locked
ucsi_con_mutex_lock/thunderbolt.c:109), and the only delete
implementation (cros_ec) is likewise under con->lock
(ucsi.c:1317-1318 from :2004/:1513/:1402/:2156) -- the header
(typec_altmode.h:247-285) documents state_update as a generic
altmode-driver entry point but states no locking precondition, so
the first future caller that trusts the header reintroduces the
UAF. Fix: in-delete locking (mutex_lock/unlock(&sel->lock) before
the cleanup) or a kref/RCU anchor, and document the contract.
These three are reported together because they are the same layer
and the same root style: safety outsourced to undocumented caller
discipline. The class layer's own retimer implementation shows
the fix pattern.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work)
2026-11-04 12:54 ` soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work) 徐晗翔
@ 2026-11-04 13:04 ` Greg KH
0 siblings, 0 replies; 10+ messages in thread
From: Greg KH @ 2026-11-04 13:04 UTC (permalink / raw)
To: 徐晗翔
Cc: andersson, konradybcio, linux-arm-msm, linux-usb, linux-kernel
On Wed, Nov 04, 2026 at 08:54:38PM +0800, 徐晗翔 wrote:
> Please keep me in Cc; I am not subscribed to the lists.
Please just send a patch for this issue as obviously you have a
reproducer and can test that it works or not.
thanks,
greg k-h
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove
2026-11-04 12:57 ` usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove 徐晗翔
@ 2026-11-04 13:28 ` Greg KH
0 siblings, 0 replies; 10+ messages in thread
From: Greg KH @ 2026-11-04 13:28 UTC (permalink / raw)
To: 徐晗翔; +Cc: heikki.krogerus, linux-usb, linux-kernel
On Wed, Nov 04, 2026 at 08:57:07PM +0800, 徐晗翔 wrote:
<snip>
Sorry, but none of this is helpful at all. Please send patches, not
walls of LLM-generated text that no one is going to read.
greg k-h
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-11-04 13:28 UTC | newest]
Thread overview: 10+ 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
2026-11-04 12:54 ` soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work) 徐晗翔
2026-11-04 13:04 ` Greg KH
2026-11-04 12:57 ` usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove 徐晗翔
2026-11-04 13:28 ` Greg KH
2026-11-04 12:58 ` usb: typec: ucsi: deadlock on cros_ec unbind -- connector lock held across cancel_delayed_work_sync vs the mode-selection work's unbounded trylock polling 徐晗翔
2026-11-04 13:00 ` usb: typec: stusb160x/rt1719/hd3ss3220: IRQ thread vs remove -- double unregister / use-after-free on the partner and port 徐晗翔
2026-11-04 13:01 ` usb: typec: ucsi: displayport: data race on dp->header/ vdo_data/vdo_size -- work reads them unlocked while writers hold con->lock (VDM mismatch) 徐晗翔
2026-11-04 13:02 ` usb: typec: class: three lifetime/locking gaps around the mux/switch handles and mode_selection (drvdata UAF after provider unbind; no concurrent protocol for set/put; unlocked state_update vs delete) 徐晗翔
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox