mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* usb: devio: usbdev_release() has no barrier against an  in-flight async_completed() callback (use-after-free window on  struct usb_dev_state)gregkh@linuxfoundation.org
@ 2026-10-27 12:35 徐晗翔
  2026-10-27 14:12 ` Greg KH
  0 siblings, 1 reply; 21+ 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] 21+ 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-03 11:21   ` mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN blind spot: slub.o is uninstrumented) 徐晗翔
                     ` (11 more replies)
  0 siblings, 12 replies; 21+ 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] 21+ messages in thread

* mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN  blind spot: slub.o is uninstrumented)
  2026-10-27 14:12 ` Greg KH
@ 2026-11-03 11:21   ` 徐晗翔
  2026-11-04  9:41     ` Qing Wang
  2026-11-03 11:30   ` futex/hrtimer: hrtimer_sleeper::task is read unsynchronized in 16 places on v7.3 -- the accessor series that fixes this is not in 7.3; please make sure it reaches the affected trees (backport nudge + stock-kernel KCSAN evidence) 徐晗翔
                     ` (10 subsequent siblings)
  11 siblings, 1 reply; 21+ messages in thread
From: 徐晗翔 @ 2026-11-03 11:21 UTC (permalink / raw)
  To: vbabka, harry, akpm; +Cc: linux-mm, linux-kernel

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

flush_all_cpus_locked() calls has_pcs_used() for every CPU to decide
which flush work to skip, reading each remote CPU's pcs->spare /
pcs->rcu_free and dereferencing pcs->main->size -- all locklessly
(mm/slub.c:4041-4051, inlined into flush_all_cpus_locked) -- while
the owning CPU's alloc/free fast paths mutate exactly those fields
under only the per-CPU sheaf lock (pcs->main->size++/--, the
pcs->main pointer swap in __pcs_replace_empty_main, etc.).  The
reader runs from kmem_cache_shrink() (sysfs shrink attribute, memory
offline) and is, as far as I can enumerate, the only cross-CPU
consumer of this state outside the sysfs slabinfo walkers (excluded
-- they read other fields under a different protocol).  I verified
the race with KCSAN after discovering why nobody has ever seen it
reported: mm/Makefile sets KCSAN_SANITIZE_slub.o := n, so BOTH sides
of this race are structurally invisible to KCSAN on any stock kernel
(verified at the disassembly level: the four accesses in
flush_all_cpus_locked compile to bare cmp/mov with zero __tsan
calls).  On a diagnostic build that only flips that Makefile switch
(no source change), a shrink+churn workload on a dedicated cache
produces 436 reader-pair reports covering all five writer families,
including an 8-byte pcs->main pointer swap with an explicit "value
changed" line -- the snapshot prerequisite of the (extremely
unlikely, source-argued) use-after-free chain: a stale pcs->main can
outlive its sheaf's retirement (spare -> rcu_free -> call_rcu ->
kfree) if the reader is preempted between load and dereference.  The
practical impact of the race itself is a wrong skip decision in
sfw->skip; the fix is to move the emptiness check into the target
CPU's flush callback, where the state is local and locked.

The detailed description:

* The race (Linux 7.3, mm/slub.c)

  Reader (inline has_pcs_used, from flush_all_cpus_locked, called by
  __kmem_cache_shrink / kmem_cache_shrink -- sysfs "shrink" and
  memory offline, privileged):
      reads pcs->spare, pcs->rcu_free, pcs->main, pcs->main->size of
      REMOTE cpus, holding no lock on that cpu's sheaf.

  Writers (per-CPU fast paths under the sheaf lock of their own cpu):
      alloc/free paths' main->size++/-- (kmem_cache_free's free_to_pcs,
      bulk variants' size += batch, alloc's size--), and
      __pcs_replace_empty_main's pcs->main = full / pcs->spare = old
      pointer stores.

  Sheaves are enabled by default (no slub_debug boot param needed).

* Why KCSAN has never reported it (part of the finding)

  mm/Makefile:15 KCSAN_SANITIZE_slub.o := n excludes the entire file
  from instrumentation on both sides.  nm/disassembly on a stock 7.3
  KCSAN kernel: has_pcs_used is inlined into flush_all_cpus_locked
  and the four accesses at +0x166/+0x173 etc. have no __tsan_* calls
  around them.  So "no KCSAN report" is not evidence of absence for
  anything inside slub.o; this race was invisible by construction.

* Evidence (KCSAN, stock-vs-diagnostic A/B; reproducer attached)

  - Stock kernel: three runs, zero reports (structurally, as above).
  - Diagnostic kernel (ONLY the Makefile switch flipped, source
    untouched): dedicated non-mergeable cache, 3 churn threads
    (single+bulk8 alloc/free) + 2 shrink threads -> 436 reader-pair
    reports, all five writer families:
      * size field (4 bytes, same address across CPUs):
        read by flush_all_cpus_locked+0x173 (shrink, cpu 3) vs
        read-write kmem_cache_free_bulk+0x90a (churn, cpu 1)
      * pcs->main pointer (8 bytes): write by
        __pcs_replace_empty_main+0x5c1 (pcs->main = full) vs read by
        flush_all_cpus_locked+0x166, KCSAN printing
        "value changed: ...0300 -> ...78800" -- the reader observed
        the sheaf pointer being concurrently replaced, i.e. the
        stale-snapshot precondition of the UAF chain, live.
  - Control (same diagnostic kernel, churn-only, shrink=0): zero
    reader-pair reports -- the reports are caused by the shrink-side
    reader, not by the churn load.
  - Honest limits: the (c)-layer UAF read (stale pcs->main surviving
    the retirement chain during a preempted reader) is source-argued,
  not dynamically triggered -- it needs a millisecond-scale preemption
  of the reader landing inside a retirement window; KCSAN demonstrates
  the racing accesses and the pointer-swap visibility, which is the
  right tool's right scope.

  Reproducer (attached fxslub.c): registers a private cache with a
  ctor (non-mergeable, sheaf-enabled) and hammers
  kmem_cache_shrink() from N threads while other threads churn
  single+bulk allocations.  On a KCSAN kernel with slub.o
  instrumented it produces the reports above within minutes; on a
  stock kernel it demonstrably cannot (that is the point).


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

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

// SPDX-License-Identifier: GPL-2.0
/*
 * fxslub.c - KCSAN hammer for Case 1 of the kernel/futex manual audit:
 *
 *   has_pcs_used() (mm/slub.c:4041-4051) reads remote per-CPU sheaf state
 *   (pcs->spare / pcs->rcu_free / pcs->main->size) with no lock and no
 *   annotation from flush_all_cpus_locked() during kmem_cache_shrink(),
 *   racing the owning CPU's local_trylock-protected writers
 *   (free_to_pcs / alloc_from_pcs / __free_to_pcs_batch /
 *    __pcs_replace_empty_main / __pcs_replace_full_main /
 *    __kfree_rcu_sheaf ...).
 *
 * Method: dedicated unmergeable cache (ctor) + churn kthreads pinned on
 * CPUs 0..N-1 driving all sheaf write paths (single fastpath, bulk, and
 * kfree_rcu batching) + shrink kthreads looping kmem_cache_shrink() on
 * other CPUs. No kernel source modification; EXPORTED APIs only.
 *
 * Params:
 *   churn_threads  - pinned per-CPU alloc/free/rcu threads (default 6)
 *   shrink_threads - kmem_cache_shrink() loopers (default 2, cpus 6,7)
 *   shrink         - 1 enable shrink loop (fx1), 0 negative control (fx1ctl)
 *   secs           - self-reported runtime budget hint for /init (default 90)
 */
#define pr_fmt(fmt) "FXSLUB: " fmt

#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/rcupdate.h>
#include <linux/cpu.h>
#include <linux/nodemask.h>
#include <linux/mm.h>

static int churn_threads = 6;
static int shrink_threads = 2;
static bool shrink = true;
static int secs = 90;

module_param(churn_threads, int, 0);
module_param(shrink_threads, int, 0);
module_param(shrink, bool, 0);
module_param(secs, int, 0);

struct fxobj {
	struct rcu_head rcu;
	char pad[512 - sizeof(struct rcu_head)];
};

static struct kmem_cache *fxcache;
static struct task_struct *churn_task[8];
static struct task_struct *shrink_task[4];
static atomic_t stop_flag;

static atomic64_t n_alloc, n_bulk, n_rcu, n_shrink, n_free_single;

/* ctor => unmergeable => dedicated sheaves/barns for this cache */
static void fx_ctor(void *p) { }

static int fx_churn(void *arg)
{
	void *bulk[8];
	struct fxobj *o;
	int n;

	while (!kthread_should_stop() && !atomic_read(&stop_flag)) {
		/* single fastpath: pcs->main->size ++/-- on this cpu */
		o = kmem_cache_alloc(fxcache, GFP_KERNEL);
		if (o) {
			atomic64_inc(&n_alloc);
			kmem_cache_free(fxcache, o);
			atomic64_inc(&n_free_single);
		}

		/* bulk: drives __pcs_replace_empty_main / free_to_pcs_bulk /
		 *       barn traffic (pcs->main/spare pointer stores).
		 * Bulk size 8 (not 32): smaller batches keep the free-side
		 * barn_replace_full_sheaf() rate (whose MAX_FULL_SHEAVES check
		 * is outside barn->lock by design) from stacking unboundedly
		 * under KCSAN-widened check->lock gaps (attempt4: 5.8GB of
		 * freed objects parked in barn sheaves -> OOM). */
		n = kmem_cache_alloc_bulk(fxcache, GFP_KERNEL, 8, bulk);
		if (n > 0) {
			atomic64_add(n, &n_bulk);
			kmem_cache_free_bulk(fxcache, n, bulk);
		}

		/* memory watermark throttle: barn parking of freed objects is
		 * only soft-bounded (see above); do not let the hammer OOM the VM */
		if (si_mem_available() < (512UL << 20 >> PAGE_SHIFT))
			msleep(100);

		/*
		 * No kfree_rcu phase: for a non-kmalloc cache, kfree_rcu() takes
		 * the per-object call_rcu(kvfree_rcu_cb) path (NOT the sheaf
		 * batching), so it contributes nothing to the sheaf-state writers
		 * under test while its RCU backlog grows without bound under
		 * KCSAN taxation (attempt3: 5.8GB unreclaimable, OOM panic).
		 * All target writers (free_to_pcs / alloc_from_pcs /
		 * __pcs_replace_*) are driven by the single+bulk churn above.
		 */

		if (!(atomic64_read(&n_alloc) & 0xfff))
			cond_resched();
	}
	return 0;
}

static int fx_shrink(void *arg)
{
	while (!kthread_should_stop() && !atomic_read(&stop_flag)) {
		kmem_cache_shrink(fxcache);
		atomic64_inc(&n_shrink);
		cond_resched();
	}
	return 0;
}

static int __init fxslub_init(void)
{
	long i;
	int ret;

	fxcache = kmem_cache_create("fxsheaf", sizeof(struct fxobj), 0,
				    SLAB_PANIC, fx_ctor);
	if (!fxcache)
		return -ENOMEM;

	if (churn_threads > 8)
		churn_threads = 8;
	if (shrink_threads > 4)
		shrink_threads = 4;

	pr_info("cache fxsheaf created, churn=%d shrink=%d(%s) secs=%d\n",
		churn_threads, shrink_threads,
		shrink ? "on" : "OFF (negative control)", secs);

	for (i = 0; i < churn_threads; i++) {
		struct task_struct *t = kthread_create(fx_churn, (void *)i,
						       "fxchurn/%ld", i);
		if (IS_ERR(t)) { ret = PTR_ERR(t); goto err; }
		kthread_bind(t, i % nr_cpu_ids);
		wake_up_process(t);
		churn_task[i] = t;
	}

	if (shrink) {
		for (i = 0; i < shrink_threads; i++) {
			struct task_struct *t = kthread_create(fx_shrink, (void *)i,
							       "fxshrink/%ld", i);
			if (IS_ERR(t)) { ret = PTR_ERR(t); goto err; }
			kthread_bind(t, (churn_threads + i) % nr_cpu_ids);
			wake_up_process(t);
			shrink_task[i] = t;
		}
	}

	pr_info("threads running\n");
	return 0;
err:
	atomic_set(&stop_flag, 1);
	for (i = 0; i < 8; i++)
		if (churn_task[i])
			kthread_stop(churn_task[i]);
	kmem_cache_destroy(fxcache);
	return ret;
}

static void __exit fxslub_exit(void)
{
	int i;

	atomic_set(&stop_flag, 1);
	for (i = 0; i < 8; i++)
		if (churn_task[i])
			kthread_stop(churn_task[i]);
	for (i = 0; i < 4; i++)
		if (shrink_task[i])
			kthread_stop(shrink_task[i]);

	pr_info("totals: shrink_calls=%lld allocs=%lld bulk=%lld rcu=%lld single_free=%lld\n",
		atomic64_read(&n_shrink), atomic64_read(&n_alloc),
		atomic64_read(&n_bulk), atomic64_read(&n_rcu),
		atomic64_read(&n_free_single));
	/* see comment below before any future re-addition of destroy */

	/*
	 * The cache is deliberately NOT destroyed: kfree_rcu() from a non-
	 * kmalloc cache queues one call_rcu(kvfree_rcu_cb) per object
	 * (kvfree_call_rcu(), mm/slab_common.c), and those callbacks free back
	 * into per-CPU sheaves; draining ~200k of them under KCSAN taxation
	 * takes minutes and then floods "Objects remaining on
	 * __kmem_cache_shutdown()" warnings (observed: 1441/1604 warnings in
	 * the first two control attempts). That noise is a module-contract
	 * artifact, irrelevant to the race under test; this VM is throwaway,
	 * so we leak the idle cache and keep the log clean.
	 */
	pr_info("leaking idle cache fxsheaf (teardown noise avoided)\n");
	pr_info("cache destroyed, module out\n");
}

module_init(fxslub_init);
module_exit(fxslub_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("KCSAN hammer: has_pcs_used() vs per-CPU sheaf writers (audit Case 1)");

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

* futex/hrtimer: hrtimer_sleeper::task is read unsynchronized  in 16 places on v7.3 -- the accessor series that fixes this is not in 7.3; please make sure it reaches the affected trees (backport  nudge + stock-kernel KCSAN evidence)
  2026-10-27 14:12 ` Greg KH
  2026-11-03 11:21   ` mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN blind spot: slub.o is uninstrumented) 徐晗翔
@ 2026-11-03 11:30   ` 徐晗翔
  2026-11-03 11:48   ` cgroup: fork vs cgroup.procs write race in favordynmods mode -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the stable trees (repro incl. uninstrumented natural hit) 徐晗翔
                     ` (9 subsequent siblings)
  11 siblings, 0 replies; 21+ messages in thread
From: 徐晗翔 @ 2026-11-03 11:30 UTC (permalink / raw)
  To: tglx, mingo, anna-maria, frederic; +Cc: linux-kernel

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

On v7.3, struct hrtimer_sleeper::task is written by the hrtimer
callback in hard interrupt context (hrtimer_wakeup(): t->task = NULL,
plus the start path's assignment) and read without any synchronization
in sixteen places across nine files -- the futex wait/requeue/PI
paths (kernel/futex/waitwake.c:386/542/593/728, requeue.c:747,
rtmutex.c:1647), the nanosleep core (kernel/time/hrtimer.c:2435/2441/
2445, sleep_timeout.c:215/223), wait.h:559, aio, io_uring/rw.c and
pktgen.  The impact is bounded -- misclassification of a return code
(-ERESTARTSYS seen where -ETIMEDOUT is due, or an extra restart) with
no UAF and no lost wakeup -- which matches the analysis in the
upstream series that already fixes this (P. McKenney, 2026-07-31:
mark the field __private and route every access through
hrtimer_sleeper_task_get/set() accessors; nine patches).  That series
is not in v7.3.  I am writing to (a) confirm the analysis with
stock-kernel KCSAN evidence on 7.3 -- 48+ reports within ~20 s of
targeted microsecond-timeout load, exactly at the audited pairs
(__futex_wait vs hrtimer_wakeup, do_nanosleep vs hrtimer_wakeup),
and (b) ask that the series be applied/backported to the trees that
still carry the raw accesses, since every one of them keeps being a
KCSAN report factory (and a correctness smell) until then.

The detailed description:
* The unsynchronized pattern (v7.3)
  Writers (2): hrtimer_sleeper_start_expires()'s task assignment and,
  in hard IRQ context, hrtimer_wakeup() setting t->task = NULL
  (kernel/time/hrtimer.c:2323-2333) when the timer expires.
  Readers (16): each checks "did the timer already fire?" by reading
  sleeper->task -- e.g. __futex_wait's return-code selection
  (waitwake.c:728), futex_wait_requeue_pi paths (:542/:593/:386),
  requeue.c:747, rt_mutex_waiter (rtmutex.c:1647), do_nanosleep's
  restart decision (hrtimer.c:2435/2441/2445), etc.  None holds a
  lock or uses READ_ONCE against the IRQ writer.

* Evidence (KCSAN on a STOCK 7.3 kernel -- unlike slub, these files
  are instrumented; no diagnostic build needed)
  Targeted microsecond-timeout workloads (userspace static binary
  plus a small module for the in-kernel waiters): futex with tiny
  timeouts (146k/141k calls), FUTEX_WAIT_REQUEUE_PI (122k), nanosleep
  (363k), epoll (2.89M), aio (531k), usleep_range /
  wait_event_hrtimeout (581k each).  Within ~20 seconds of load
  start:
    BUG: KCSAN: data-race in __futex_wait / hrtimer_wakeup
    read-write to 0x... of 8 bytes by interrupt on cpu 1:
      hrtimer_wakeup+0x17                     <- t->task = NULL
      __hrtimer_run_queues <- hrtimer_interrupt <- apic timer
    read to 0x... of 8 bytes by task on cpu 3:
      __futex_wait+0xc9                       <- the waitwake.c:728
                                                read, held in KCSAN's
                                                watchpoint delay
      futex_wait -> do_futex -> __x64_sys_futex
  and the same shape for do_nanosleep vs hrtimer_wakeup
  (do_nanosleep+0xbe = hrtimer.c:2435/2441).  48+ targeted reports
  across the two representative read sites; the other audited read
  sites share the identical mechanism (sampling/limiting is why they
  were not individually collected).
  Reproducers attached: fxfut.c (userspace, six thread classes) and
  fxhrt.c (module driving usleep_range/wait_event_hrtimeout hammer).
  Run on any KCSAN kernel with skip_watch/udelay tuned; first hits
  arrive within tens of seconds.



Regards,
Hanxiang Xu
xuhx@hust.edu.cn

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

// SPDX-License-Identifier: GPL-2.0
/*
 * fxhrt.c - KCSAN hammer for Case 2 of the kernel/futex manual audit,
 * kernel-context side:
 *
 *   hrtimer_sleeper::task plain reads racing the hardirq writer
 *   hrtimer_wakeup() (kernel/time/hrtimer.c:2323-2333 t->task = NULL).
 *
 * Sites exercised here:
 *   R10/R11 schedule_hrtimeout_range_clock()  kernel/time/sleep_timeout.c:215/223
 *   R12     __wait_event_hrtimeout            include/linux/wait.h:559
 *   (user-space side fxfut covers futex R1-R5, do_nanosleep R7-R9, epoll/aio)
 *
 * Method: kthreads looping microsecond sleeps so the timer expires within
 * the KCSAN watch window of the immediately-following ->task read.
 * usleep_range() reaches schedule_hrtimeout_range_clock() via
 * do_usleep_range(); wait_event_hrtimeout() expands the wait.h macro.
 */
#define pr_fmt(fmt) "FXHRT: " fmt

#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/slab.h>

static int threads = 4;
static int secs = 90;

module_param(threads, int, 0);
module_param(secs, int, 0);

static DECLARE_WAIT_QUEUE_HEAD(fxwq);
static struct task_struct *htask[8];
static atomic_t stop_flag;
static atomic64_t n_usleep, n_wevt;

static int fx_hrt(void *arg)
{
	unsigned long t;

	while (!kthread_should_stop() && !atomic_read(&stop_flag)) {
		/* R10/R11: schedule_hrtimeout_range_clock() t.task reads */
		usleep_range(1, 2);
		atomic64_inc(&n_usleep);

		/* R12: __wait_event_hrtimeout __t.task read */
		t = atomic64_inc_return(&n_wevt);
		wait_event_hrtimeout(fxwq, (t & 1) == 2, 2);
	}
	return 0;
}

static int __init fxhrt_init(void)
{
	long i;
	int ret;

	if (threads > 8)
		threads = 8;

	for (i = 0; i < threads; i++) {
		struct task_struct *t = kthread_create(fx_hrt, (void *)i,
						       "fxhrt/%ld", i);
		if (IS_ERR(t)) { ret = PTR_ERR(t); goto err; }
		kthread_bind(t, (i + 4) % nr_cpu_ids);
		wake_up_process(t);
		htask[i] = t;
	}
	pr_info("%d hrt sleeper threads running (usleep_range 1-2us + wait_event_hrtimeout 2us)\n",
		threads);
	return 0;
err:
	atomic_set(&stop_flag, 1);
	for (i = 0; i < 8; i++)
		if (htask[i])
			kthread_stop(htask[i]);
	return ret;
}

static void __exit fxhrt_exit(void)
{
	int i;

	atomic_set(&stop_flag, 1);
	for (i = 0; i < 8; i++)
		if (htask[i])
			kthread_stop(htask[i]);
	pr_info("totals: usleep_range=%lld wait_event_hrtimeout=%lld\n",
		atomic64_read(&n_usleep), atomic64_read(&n_wevt));
}

module_init(fxhrt_init);
module_exit(fxhrt_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("KCSAN hammer: hrtimer_sleeper::task readers vs hrtimer_wakeup (audit Case 2)");

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

// SPDX-License-Identifier: GPL-2.0
/* fxfut.c - KCSAN hammer for Case 2, user-space side.
 *
 * Threads hammering microsecond-timeout waits so the armed hrtimer
 * expires inside the KCSAN watch window of the following ->task read:
 *
 *   R1/R4  futex(FUTEX_WAIT_PRIVATE, .., 1-2us)   -> futex_do_wait/__futex_wait
 *   R1     futex(FUTEX_WAIT_BITSET_PRIVATE, ...)   -> futex_do_wait
 *   R5     futex(FUTEX_WAIT_REQUEUE_PI_PRIVATE..)  -> handle_early_requeue_pi_wakeup
 *   R7-R9  nanosleep(1-2us)                        -> do_nanosleep
 *   R10    epoll_wait(1ms)                         -> schedule_hrtimeout_range_clock
 *   R13    io_getevents(1-2us timeout)             -> aio_read_events_ring
 *
 * usage: fxfut <threads-per-kind> <seconds>
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/syscall.h>
#include <sys/epoll.h>
#include <time.h>

#ifndef SYS_futex
#define SYS_futex 202
#endif
#define FUTEX_WAIT		0
#define FUTEX_WAIT_BITSET	9
#define FUTEX_WAIT_REQUEUE_PI	11
#define FUTEX_PRIVATE_FLAG	128

#ifndef SYS_io_setup
#define SYS_io_setup 206
#define SYS_io_getevents 208
#endif

static volatile int running = 1;
static long cnt_futex, cnt_futexb, cnt_rqi, cnt_nano, cnt_epoll, cnt_aio;
static int n_threads = 3, secs = 90;

#define TS_US(nsec) ({ static struct timespec _ts; _ts.tv_sec = 0; _ts.tv_nsec = (nsec); &_ts; })

static void *tf_futex(void *a)
{
	unsigned int word = 0;
	while (running) {
		syscall(SYS_futex, &word, FUTEX_WAIT | FUTEX_PRIVATE_FLAG,
			word, TS_US(1000), NULL, 0);
		cnt_futex++;
	}
	return NULL;
}

static void *tf_futexb(void *a)
{
	unsigned int word = 0;
	while (running) {
		syscall(SYS_futex, &word, FUTEX_WAIT_BITSET | FUTEX_PRIVATE_FLAG,
			word, TS_US(2000), NULL, 0xffffffff);
		cnt_futexb++;
	}
	return NULL;
}

static void *tf_rqi(void *a)
{
	unsigned int u1 = 0, u2 = 0;
	while (running) {
		syscall(SYS_futex, &u1, FUTEX_WAIT_REQUEUE_PI | FUTEX_PRIVATE_FLAG,
			0, TS_US(2000), &u2, 0);
		cnt_rqi++;
	}
	return NULL;
}

static void *tf_nano(void *a)
{
	while (running) {
		nanosleep(TS_US(1000), NULL);
		cnt_nano++;
	}
	return NULL;
}

static void *tf_epoll(void *a)
{
	int fd = epoll_create1(0);
	struct epoll_event ev;
	while (running && fd >= 0) {
		epoll_wait(fd, &ev, 0, 1);
		cnt_epoll++;
	}
	return NULL;
}

static void *tf_aio(void *a)
{
	unsigned long ctx = 0;
	if (syscall(SYS_io_setup, 4, &ctx) < 0)
		return NULL;
	while (running) {
		syscall(SYS_io_getevents, ctx, 0, 0, NULL, TS_US(2000));
		cnt_aio++;
	}
	return NULL;
}

int main(int argc, char **argv)
{
	pthread_t th[64];
	int n = 0, i;

	if (argc > 1) n_threads = atoi(argv[1]);
	if (argc > 2) secs = atoi(argv[2]);
	if (n_threads < 1) n_threads = 1;
	if (n_threads > 8) n_threads = 8;

	for (i = 0; i < n_threads; i++) {
		pthread_create(&th[n++], NULL, tf_futex, NULL);
		pthread_create(&th[n++], NULL, tf_futexb, NULL);
		pthread_create(&th[n++], NULL, tf_rqi, NULL);
		pthread_create(&th[n++], NULL, tf_nano, NULL);
		pthread_create(&th[n++], NULL, tf_epoll, NULL);
		pthread_create(&th[n++], NULL, tf_aio, NULL);
	}
	printf("FXFUT: %d threads x 6 kinds, %ds\n", n_threads, secs);
	fflush(stdout);

	sleep(secs);
	running = 0;
	for (i = 0; i < n; i++)
		pthread_join(th[i], NULL);
	printf("FXFUT: totals futex=%ld futexb=%ld rqi=%ld nano=%ld epoll=%ld aio=%ld\n",
	       cnt_futex, cnt_futexb, cnt_rqi, cnt_nano, cnt_epoll, cnt_aio);
	fflush(stdout);
	return 0;
}

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

* cgroup: fork vs cgroup.procs write race in favordynmods mode  -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the  stable trees (repro incl. uninstrumented natural hit)
  2026-10-27 14:12 ` Greg KH
  2026-11-03 11:21   ` mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN blind spot: slub.o is uninstrumented) 徐晗翔
  2026-11-03 11:30   ` futex/hrtimer: hrtimer_sleeper::task is read unsynchronized in 16 places on v7.3 -- the accessor series that fixes this is not in 7.3; please make sure it reaches the affected trees (backport nudge + stock-kernel KCSAN evidence) 徐晗翔
@ 2026-11-03 11:48   ` 徐晗翔
  2026-11-03 14:51     ` Sasha Levin
  2026-11-03 11:57   ` net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3 徐晗翔
                     ` (8 subsequent siblings)
  11 siblings, 1 reply; 21+ messages in thread
From: 徐晗翔 @ 2026-11-03 11:48 UTC (permalink / raw)
  To: tj, hannes, mkoutny; +Cc: cgroups, linux-kernel, stable

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

In the per-threadgroup-rwsem mode (CONFIG_CGROUP_FAVOR_DYNMODS or the
cgroup2 mount option favordynmods), v7.3 publishes a forked child's
PID before cgroup_post_fork() links it into its css_set
(fork.c:2572 vs cgroup.c:7085->928-929); a concurrent writer to the
destination cgroup's cgroup.procs can migrate the child inside that
window, double-linking task_struct::cg_list and corrupting css_set
accounting -- after the child exits, css_task_iter_advance() walks a
dangling pointer (use-after-free on the child's task_struct; after
SLUB reuse this is a write primitive in principle).  This is fixed
upstream by bf6fbf46b5ff ("cgroup: serialize migrations of newly
forked processes", merged 2026-10-20, already tagged Cc: stable #
6.18+), which v7.3 predates.  I am writing to support that backport
with reproduction evidence on a pristine 7.3 KASAN kernel: a
controlled window hits all three of the kernel's own WARN tripwires
(cgroup.c:2601 in cgroup_migrate_add_task, :7083 in
cgroup_post_fork, :915 in css_set_move_task) and produces 648
slab-use-after-free reports in css_task_iter_advance/_next (victim =
the child's 5896-byte task_struct, alloc copy_process, free
release_task); more importantly, a completely uninstrumented
userspace-only hammer (4 forker threads + 2 cgroup.procs writers,
preempt=full) hits the same three tripwires with identical offsets
within ~40 seconds and then takes a secondary NULL-deref in
css_set_move_task -- the race is naturally reachable by ordinary
userspace, with no KASAN required to notice (the WARNs fire on
stock kernels too).  The migration succeeding is what corrupts;
even a later failed/rolled-back migration leaves the damage.


The detailed description:

* The window (v7.3)
  fork(): the child's PID becomes visible (fork.c:2572 area) before
  cgroup_post_fork() runs and does css_set_move_task/link into
  cg_list (cgroup.c:7085 -> 928-929).  In favordynmods mode nothing
  serializes a cgroup.procs writer against this window (that is what
  bf6fbf46b5ff adds).  The writer's cgroup_migrate_add_task finds
  the "published but unlinked" child (list_empty(cg_list) true --
  WARN at :2601), links it to the destination; cgroup_post_fork then
  links it again (WARN at :7083), and css_set_move_task takes its
  from_cset==NULL branch (WARN at :915): cg_list is on two lists,
  nr_tasks is double-counted.  After the child exits and is reaped,
  the destination's task list retains a dangling pointer that
  css_task_iter walks (the UAF), and unrelated later migrations can
  crash on the corrupted state (observed: NULL-deref at
  css_set_move_task+0x8a).

* Evidence (pristine 7.3 KASAN; reproducer attached)
  Controlled (r1stall.ko parks the forker at cgroup_post_fork's
  entry -- the window's end boundary -- and hands the child PID to
  userspace; r1poc writes it to dst/cgroup.procs inside the window):
    WARNING: cgroup.c:2601 at cgroup_migrate_add_task  (writer side)
    ... write ret=3, dst membership: YES                  (migration
        completed inside the window)
    WARNING: cgroup.c:7083 at cgroup_post_fork         (forker side)
    WARNING: cgroup.c:915 at css_set_move_task         (double link)
    ... child exits ...
    BUG: KASAN: slab-use-after-free in css_task_iter_advance+0xb8
      (x648, css_task_iter_advance/_next family; victim task_struct,
       alloc copy_process, free release_task)
  Natural (NO module loaded, preempt=full, pure userspace): same
  three WARNs with identical function+offset signatures at ~40 s
  into the hammer (writer thread hits :2601, forker thread hits
  :7083/:915 -- the interleaving itself captured), followed by
  BUG: KASAN: null-ptr-deref in css_set_move_task+0x8a as a later
  legitimate migration meets the corrupted state; the VM eventually
  became unstable.  Negative control (favordynmods active, normal
  migration of a fully-linked child): zero warnings, zero KASAN.
  Reproducers attached: r1stall.c (kprobe window module) and r1poc.c
  (static userspace orchestrator with ctl/base/nat modes; the nat
  mode needs no module at all).



Regards,
Hanxiang Xu
xuhx@hust.edu.cn

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

// SPDX-License-Identifier: GPL-2.0
/*
 * r1stall.c - controlled-window instrument for R1 (fork vs cgroup.procs
 * migration race under the favordynmods per-threadgroup rwsem).
 *
 * Audited bug (R1, v7.3, fixed upstream by bf6fbf46b5ff):
 *   copy_process() publishes the child PID (attach_pid + tasklist_unlock,
 *   fork.c) BEFORE cgroup_post_fork() links the child into its css_set
 *   (css_set_move_task, cgroup.c:7085). With cgroup_enable_per_threadgroup_
 *   rwsem active, a cgroup.procs writer only takes the child's own fresh,
 *   uncontended per-threadgroup rwsem (cgroup_attach_lock PER_THREADGROUP),
 *   while the forker holds only the global read lock + the PARENT's per-tg
 *   read lock -- the two lock sets are disjoint, so the migration can run
 *   inside that window: cgroup_migrate_add_task() list_move_tail()s the
 *   still-orphan child->cg_list onto the source css_set's mg_tasks list,
 *   and cgroup_migrate_execute() relinks it into the destination css_set
 *   and rewrites child->cgroups. When the forker then runs
 *   cgroup_post_fork(), css_set_move_task(child, NULL, cset, false) takes
 *   the from_cset==NULL path, which SKIPS list_del_init and directly
 *   list_add_tail()s the already-linked node onto cset->tasks:
 *   double linkage of one cg_list node in two css_set lists.
 *
 * This module stalls the forking thread exactly inside that window by
 * busy-waiting in a kprobe pre_handler at the ENTRY of cgroup_post_fork
 * (the window's end boundary). This enlarges the natural window to an
 * arbitrary controlled duration, following the established controlled-
 * window methodology of this environment (see r4_stall.c precedent).
 * It does not by itself prove natural hit rates; those are exercised by
 * the r1poc natural-hammer mode.
 *
 * Hand-off protocol with userspace (r1poc):
 *   1. userspace writes 1 to /sys/module/r1stall/parameters/arm
 *   2. the NEXT user fork hits the probe: pre_handler claims the arm
 *      (atomic cmpxchg), records the child's pid where userspace can read
 *      it (child_pid), and spins (cpu_relax, IRQs on, preempt off) until
 *      userspace writes 1 to .../release or stall_ms elapses
 *   3. userspace (a helper process, on another CPU) reads child_pid,
 *      performs the cgroup.procs migration of that pid, then releases
 *   4. cgroup_post_fork() executes on the corrupted state
 *
 * Kernel-thread forks are passed through untouched (PF_KTHREAD check
 * BEFORE claiming the arm) so stray kworkers cannot consume the stall.
 */
#define pr_fmt(fmt) "R1STALL: " fmt

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

static int child_pid;
static int stall_ms = 4000;

module_param(stall_ms, int, 0600);
MODULE_PARM_DESC(stall_ms, "safety cap on the stall duration (ms)");

static atomic_t armed = ATOMIC_INIT(0);
static atomic_t release = ATOMIC_INIT(0);
static atomic_t taken = ATOMIC_INIT(0);

static int arm_set(const char *val, const struct kernel_param *kp)
{
	int v, ret;

	ret = kstrtoint(val, 0, &v);
	if (ret)
		return ret;
	if (v) {
		/* fresh arming: clear leftover release and pid */
		atomic_set(&release, 0);
		WRITE_ONCE(child_pid, 0);
		atomic_set(&armed, 1);
	} else {
		atomic_set(&armed, 0);
	}
	return 0;
}

static int arm_get(char *buffer, const struct kernel_param *kp)
{
	return sprintf(buffer, "%d\n", atomic_read(&armed));
}

static const struct kernel_param_ops arm_ops = {
	.set = arm_set,
	.get = arm_get,
};
module_param_cb(arm, &arm_ops, NULL, 0600);
MODULE_PARM_DESC(arm, "write 1 to arm the stall for the next user fork");

static int release_set(const char *val, const struct kernel_param *kp)
{
	int v, ret;

	ret = kstrtoint(val, 0, &v);
	if (ret)
		return ret;
	if (v)
		atomic_set(&release, 1);
	return 0;
}

static int release_get(char *buffer, const struct kernel_param *kp)
{
	return sprintf(buffer, "%d\n", atomic_read(&release));
}

static const struct kernel_param_ops release_ops = {
	.set = release_set,
	.get = release_get,
};
module_param_cb(release, &release_ops, NULL, 0600);
MODULE_PARM_DESC(release, "write 1 to release a stalled forker");

module_param(child_pid, int, 0400);
MODULE_PARM_DESC(child_pid, "pid of the child belonging to the stalled fork (read by userspace)");

static int stall_pre(struct kprobe *p, struct pt_regs *regs)
{
	struct task_struct *child = (struct task_struct *)regs->di;
	unsigned long deadline;
	int h;

	/* pass through when idle */
	if (!atomic_read(&armed))
		return 0;

	/* never stall kernel-thread forks (they are not migratable anyway) */
	if (!child || (child->flags & PF_KTHREAD))
		return 0;

	/* single taker */
	if (atomic_cmpxchg(&armed, 1, 0) != 1)
		return 0;

	h = atomic_inc_return(&taken);
	WRITE_ONCE(child_pid, child->pid);
	pr_warn("stall #%d: forker pid=%d comm=%s stalled at cgroup_post_fork entry, child=%d; waiting for release (cap %d ms)\n",
		h, current->pid, current->comm, child->pid, stall_ms);

	deadline = jiffies + msecs_to_jiffies(stall_ms);
	while (!atomic_read_acquire(&release) && time_before(jiffies, deadline))
		cpu_relax();

	pr_warn("stall #%d released (child=%d, %s)\n", h, child->pid,
		atomic_read(&release) ? "by user" : "by TIMEOUT");
	atomic_set(&release, 0);
	return 0;
}

static struct kprobe kp = {
	.symbol_name = "cgroup_post_fork",
	.pre_handler = stall_pre,
};

static int __init r1stall_init(void)
{
	int ret;

	ret = register_kprobe(&kp);
	if (ret) {
		pr_err("cannot register kprobe on cgroup_post_fork: %d\n", ret);
		return ret;
	}
	pr_info("armed kprobe on cgroup_post_fork (%px); stall_ms=%d\n",
		kp.addr, stall_ms);
	return 0;
}

static void __exit r1stall_exit(void)
{
	unregister_kprobe(&kp);
	pr_info("removed, stalls taken=%d\n", atomic_read(&taken));
}

module_init(r1stall_init);
module_exit(r1stall_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("controlled-window kprobe stall for R1 (fork vs cgroup.procs favordynmods race) verification");

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

// SPDX-License-Identifier: GPL-2.0
/*
 * r1poc.c - userspace choreography for the R1 (fork vs cgroup.procs
 * favordynmods per-threadgroup rwsem race) verification.
 *
 * Modes:
 *   r1poc ctl <dst_cgroup_procs>
 *       Controlled-window run (requires /r1stall.ko loaded).
 *       - forks a helper BEFORE arming; helper spins on the module's
 *         child_pid parameter, migrates the stalled child by writing its
 *         pid to <dst_cgroup_procs>, verifies dst membership, releases
 *       - main arms the stall and forks the victim child; the fork is
 *         stalled inside cgroup_post_fork() while the helper migrates
 *       - afterwards the child exits and main hammers reads of the dst
 *         cgroup.procs / cgroup.tasks (css_task_iter walks the corrupted
 *         list -> KASAN use-after-free if the freed task is hit)
 *
 *   r1poc nat <dst_cgroup_procs> <secs> <n_forkers> <n_writers>
 *       Natural hammer, no instrumentation: forkers fork+reap in a tight
 *       loop; writers predict the next pid via /proc/sys/kernel/ns_last_pid
 *       and hammer writes into <dst_cgroup_procs>. Races the natural window
 *       between attach_pid() publication and cgroup_post_fork() linking
 *       (widened by proc connector broadcast, CONFIG_PROC_EVENTS=y).
 *
 *   r1poc base <dst_cgroup_procs>
 *       Negative baseline: migrate a fully-forked (properly linked) child
 *       and read the dst lists afterwards. Validates the plumbing; must
 *       NOT produce the cgroup WARNs or any KASAN report.
 *
 * All output is prefixed "R1POC:" for log harvesting.
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <time.h>
#include <pthread.h>

#define P "/sys/module/r1stall/parameters/"
#define ARM_PATH        P "arm"
#define RELEASE_PATH    P "release"
#define CHILD_PID_PATH  P "child_pid"

static void say(const char *fmt, ...)
{
	va_list ap;

	fputs("R1POC: ", stdout);
	va_start(ap, fmt);
	vprintf(fmt, ap);
	va_end(ap);
	fputc('\n', stdout);
	fflush(stdout);
}

static int write_file(const char *path, const char *buf)
{
	int fd = open(path, O_WRONLY);
	int n = -1;

	if (fd < 0)
		return -1;
	n = write(fd, buf, strlen(buf));
	close(fd);
	return n;
}

static int read_file(const char *path, char *buf, int len)
{
	int fd = open(path, O_RDONLY);
	int n = -1;

	if (fd < 0)
		return -1;
	n = read(fd, buf, len - 1);
	if (n >= 0)
		buf[n] = '\0';
	close(fd);
	return n;
}

static int write_int(const char *path, int v)
{
	char buf[32];

	snprintf(buf, sizeof(buf), "%d", v);
	return write_file(path, buf);
}

static int read_int(const char *path, int *out)
{
	char buf[64];
	int n = read_file(path, buf, sizeof(buf));

	if (n <= 0)
		return -1;
	*out = atoi(buf);
	return 0;
}

static void dump_first_line(const char *tag, const char *path)
{
	char buf[4096];
	int n = read_file(path, buf, sizeof(buf));

	if (n <= 0) {
		say("%s read(%s) failed errno=%d", tag, path, errno);
		return;
	}
	buf[200] = '\0';
	for (char *p = buf; *p; p++)
		if (*p == '\n') { *p = '|'; }
	say("%s %s -> %.200s", tag, path, buf);
}

/* ------------------------------------------------------------------ */
/* controlled mode                                                     */
/* ------------------------------------------------------------------ */

static void ctl_helper(const char *dst, int mypid)
{
	char buf[64];
	int pid = 0, i, n, fd;
	char rb[4096];

	for (i = 0; i < 800; i++) {	/* up to ~8s */
		if (read_int(CHILD_PID_PATH, &pid) == 0 && pid > 0)
			break;
		usleep(10000);
	}
	if (pid <= 0) {
		say("helper(pid=%d): TIMEOUT waiting for child_pid, releasing", mypid);
		write_int(RELEASE_PATH, 1);
		_exit(9);
	}
	say("helper(pid=%d): saw child_pid=%d, migrating it now", mypid, pid);

	n = snprintf(buf, sizeof(buf), "%d", pid);
	fd = open(dst, O_WRONLY);
	if (fd < 0) {
		say("helper: open(%s) FAILED errno=%d", dst, errno);
		write_int(RELEASE_PATH, 1);
		_exit(10);
	}
	errno = 0;
	n = write(fd, buf, n);
	close(fd);
	say("helper: write(pid=%d) to %s -> ret=%d errno=%d", pid, dst, n, errno);

	/* migration effect readback: the stalled child must now be listed */
	n = read_file(dst, rb, sizeof(rb));
	if (n > 0) {
		char want[32];
		int found = 0;

		snprintf(want, sizeof(want), "%d", pid);
		/* crude token scan */
		for (char *p = strtok(rb, " \t\n"); p; p = strtok(NULL, " \t\n"))
			if (!strcmp(p, want))
				found = 1;
		say("helper: dst membership of pid=%d after migration: %s",
		    pid, found ? "YES (child now belongs to dst)" : "NO");
	} else {
		say("helper: dst readback failed errno=%d", errno);
	}

	say("helper: releasing stalled forker");
	write_int(RELEASE_PATH, 1);
	_exit(0);
}

static int ctl_mode(const char *dst)
{
	pid_t helper, child;
	int i, st;

	say("CTL-MODE-START dst=%s", dst);

	helper = fork();
	if (helper == 0) {
		ctl_helper(dst, getpid());
		_exit(0);	/* not reached */
	}
	usleep(200000);		/* helper is up and spinning */

	say("main: arming stall, then forking victim child");
	if (write_int(ARM_PATH, 1) < 0) {
		say("main: arm write FAILED errno=%d (module loaded?)", errno);
		return 1;
	}

	child = fork();		/* stalls inside cgroup_post_fork */
	if (child == 0) {
		say("child(pid=%d): alive after cgroup_post_fork, exiting now", getpid());
		fflush(stdout);
		_exit(42);
	}
	say("main: fork returned child=%d (stall released)", child);

	if (waitpid(child, &st, 0) < 0)
		say("main: waitpid errno=%d", errno);
	else
		say("main: child reaped, exit status=%d", WEXITSTATUS(st));
	kill(helper, SIGKILL);
	waitpid(helper, &st, 0);

	say("main: sleeping 3s (RCU GP + KASAN quarantine) then hammering dst list reads");
	sleep(3);
	for (i = 0; i < 100; i++) {
		dump_first_line("walk", dst);
		usleep(20000);
	}
	dump_first_line("walk", "/sys/fs/cgroup/cgroup.procs");
	say("CTL-MODE-DONE");
	return 0;
}

/* ------------------------------------------------------------------ */
/* baseline (negative control)                                         */
/* ------------------------------------------------------------------ */

static int base_mode(const char *dst)
{
	char buf[64], rb[4096];
	pid_t child;
	int n, fd, st, i, found;
	char want[32];

	say("BASE-MODE-START dst=%s (no arming, plain post-fork migration)", dst);

	child = fork();
	if (child == 0) {
		sleep(1);
		_exit(7);
	}
	usleep(100000);		/* child fully forked & linked */

	n = snprintf(buf, sizeof(buf), "%d", child);
	fd = open(dst, O_WRONLY);
	if (fd < 0) {
		say("base: open(%s) FAILED errno=%d", dst, errno);
		return 1;
	}
	errno = 0;
	n = write(fd, buf, n);
	close(fd);
	say("base: write(pid=%d) ret=%d errno=%d", child, n, errno);

	n = read_file(dst, rb, sizeof(rb));
	found = 0;
	snprintf(want, sizeof(want), "%d", child);
	if (n > 0)
		for (char *p = strtok(rb, " \t\n"); p; p = strtok(NULL, " \t\n"))
			if (!strcmp(p, want))
				found = 1;
	say("base: dst membership of pid=%d: %s", child, found ? "YES" : "NO");

	waitpid(child, &st, 0);
	say("base: child reaped (%d), sleeping 2s then walking dst lists", WEXITSTATUS(st));
	sleep(2);
	for (i = 0; i < 20; i++) {
		dump_first_line("walk", dst);
		usleep(50000);
	}
	say("BASE-MODE-DONE");
	return 0;
}

/* ------------------------------------------------------------------ */
/* natural hammer                                                      */
/*                                                                      */
/* NOTE: /proc/sys/kernel/ns_last_pid needs CONFIG_CHECKPOINT_RESTORE  */
/* which this kernel does not set; prediction is done cooperatively    */
/* instead: forker threads publish the pid of the fork that just       */
/* returned, writer threads hammer last_pid+1 / +2 (pids are           */
/* sequential in this quiet VM, so the next fork's child is almost     */
/* certainly one of those two numbers).                                */
/* ------------------------------------------------------------------ */

static volatile sig_atomic_t stop_flag;
static volatile long last_forked_pid;

static void onTerm(int sig) { stop_flag = 1; }

static void *nat_forker(void *arg)
{
	(void)arg;
	while (!stop_flag) {
		pid_t c = fork();

		if (c == 0)
			_exit(0);	/* immediate exit: highest pid churn */
		if (c > 0) {
			last_forked_pid = c;
			waitpid(c, NULL, 0);
		}
	}
	return NULL;
}

static void *nat_writer(void *arg)
{
	char *dst = arg;
	char b1[16], b2[16];
	int n1, n2, fd;

	fd = open(dst, O_WRONLY);
	if (fd < 0)
		return NULL;
	while (!stop_flag) {
		long t = last_forked_pid + 1;

		/* hammer the two most likely next child pids; attempts
		 * before the child's attach_pid() fail fast with ESRCH,
		 * a success landing between attach_pid() publication and
		 * cgroup_post_fork() linking wins the race */
		n1 = snprintf(b1, sizeof(b1), "%ld", t);
		n2 = snprintf(b2, sizeof(b2), "%ld", t + 1);
		write(fd, b1, n1);
		write(fd, b2, n2);
	}
	close(fd);
	return NULL;
}

static int nat_mode(const char *dst, int secs, int nf, int nw)
{
	pthread_t ft[16], wt[16];
	int i, r, w;

	if (nf > 16) nf = 16;
	if (nw > 16) nw = 16;
	say("NAT-MODE-START dst=%s secs=%d fork_threads=%d writer_threads=%d",
	    dst, secs, nf, nw);
	signal(SIGTERM, onTerm);

	for (i = 0; i < nf; i++)
		if ((r = pthread_create(&ft[i], NULL, nat_forker, NULL)) != 0)
			say("NAT: forker thread %d create failed: %d", i, r);
	for (i = 0; i < nw; i++)
		if ((r = pthread_create(&wt[i], NULL, nat_writer, (void *)dst)) != 0)
			say("NAT: writer thread %d create failed: %d", i, r);

	for (i = 0; i < secs && !stop_flag; i++)
		sleep(1);
	say("NAT: hammer window over (last_forked_pid=%ld), joining", last_forked_pid);
	stop_flag = 1;
	for (i = 0; i < nf; i++)
		pthread_join(ft[i], NULL);
	for (i = 0; i < nw; i++)
		pthread_join(wt[i], NULL);
	w = 0;
	say("NAT: sleeping 2s then walking dst lists");
	sleep(2);
	for (i = 0; i < 40 && !stop_flag; i++) {
		dump_first_line("walk", dst);
		usleep(50000);
	}
	say("NAT-MODE-DONE");
	return w;
}

int main(int argc, char **argv)
{
	if (argc < 3) {
		say("usage: %s ctl|base|nat <dst_cgroup_procs> [secs nf nw]", argv[0]);
		return 2;
	}
	setvbuf(stdout, NULL, _IONBF, 0);
	if (!strcmp(argv[1], "ctl"))
		return ctl_mode(argv[2]);
	if (!strcmp(argv[1], "base"))
		return base_mode(argv[2]);
	if (!strcmp(argv[1], "nat")) {
		int secs = argc > 3 ? atoi(argv[3]) : 30;
		int nf = argc > 4 ? atoi(argv[4]) : 4;
		int nw = argc > 5 ? atoi(argv[5]) : 4;
		return nat_mode(argv[2], secs, nf, nw);
	}
	say("unknown mode %s", argv[1]);
	return 2;
}

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

* net: rtnetlink: rtnl_link_unregister() vs in-flight  RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix  (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3
  2026-10-27 14:12 ` Greg KH
                     ` (2 preceding siblings ...)
  2026-11-03 11:48   ` cgroup: fork vs cgroup.procs write race in favordynmods mode -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the stable trees (repro incl. uninstrumented natural hit) 徐晗翔
@ 2026-11-03 11:57   ` 徐晗翔
  2026-11-03 15:49     ` Eric Dumazet
  2026-11-04 15:03     ` Sasha Levin
  2026-11-03 12:07   ` wifi: wilc1000: SDIO/SPI remove destroys the srcu_struct before wiphy_unregister() -- late cfg80211 callbacks write through dangling srcu state (unfixed upstream; controlled + natural Oops) 徐晗翔
                     ` (7 subsequent siblings)
  11 siblings, 2 replies; 21+ messages in thread
From: 徐晗翔 @ 2026-11-03 11:57 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni; +Cc: netdev, linux-kernel, stable

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

On v7.3, rtnl_link_unregister() removes the rtnl_link_ops from the
global list and immediately destroys the embedded srcu_struct and
frees the ops memory -- a concurrent RTM_GETLINK dump that already
found the ops (rtnl_link_ops_get(), rtnetlink.c:599 hand-off inside
an RCU read critical section at :595) but has not yet taken
ops->srcu will, after the free, execute __srcu_read_lock() /
__srcu_read_unlock() on freed memory: two reads of the destroyed
object (ssp->srcu_ctrp, ssp->sda) whose subsequent percpu counter
writes land wherever the freed percpu pointers point -- for a module
rtnl_link_ops, the ops (and its srcu) live in module memory, so
module_memfree() is the freeing side.  Upstream already fixed this
with a synchronize_rcu() in rtnl_link_unregister() (the patch with
Fixes: 6b57ff21a310; its own comment: "Wait for rtnl_link_ops_get()
readers that found ops but have not yet taken ops->srcu").  v7.3
does not contain it.  I verified both sides on a stock 7.3 KASAN
kernel: parking a real dump reader (driven by an ordinary
RTM_GETLINK + IFLA_LINKINFO dump request) exactly at the :599
hand-off and unregistering+freeing underneath produces three KASAN
slab-use-after-free reports (__srcu_read_lock+0xe, +0x25,
__srcu_read_unlock+0x1a; bad address = ops+0x10 = ops->srcu); and
rebuilding the SAME kernel with ONLY the upstream one-line fix
flips the outcome to zero reports, with the kfree demonstrably
delayed until the parked reader left its RCU section.  Natural
hammering did not hit (the hand-off window is ~100 ns against a
~4 ms unregister -- it needs a preempted/preemptible-RCU reader),
which matches the fix's own reasoning; the controlled run models
exactly that preemption.  The purpose of this mail is to support
the fix's propagation: trees still carrying the unfixed
rtnl_link_unregister() keep a cross-CPU UAF (write) plus potential
freed-module execution reachable by unprivileged netlink dumps
racing a module unload.


The detailed description:

* The gap (v7.3, net/core/rtnetlink.c)
  rtnl_dump_ifinfo() runs lockless for dumps
  (RTNL_FLAG_DUMP_UNLOCKED), takes rcu_read_lock() at :595, calls
  linkinfo_to_kind_ops() -> rtnl_link_ops_get() which returns the
  ops pointer at :599, and only then takes ops->srcu
  (srcu_read_lock) to call ops->dumpit.  rtnl_link_unregister()
  holds rtnl_mutex, removes the ops from the list, calls
  cleanup_srcu_struct(&ops->srcu) and returns to the module, whose
  exit then frees the module memory.  Nothing between the :599
  hand-off and the srcu_read_lock is waited for on the unregister
  side in 7.3 -- hence the fix's synchronize_rcu().

* Evidence (stock 7.3 KASAN, QEMU; A/B on the one-line fix;
  reproducers attached)
  rtnlrace.c registers a kzalloc'd rtnl_link_ops (kind "rtnlrace"),
  parks a dump reader by kprobe at the exact hand-off (return
  address rtnl_dump_ifinfo+0x374, ENDBR-adjusted), and unregisters
  + kfrees from another thread (modeling module_exit ->
  module_memfree).  nlr (userspace) sends the IFLA_LINKINFO dump
  request that drives the reader path.
  Defect kernel (stock 7.3):
    park t=127041 (1004 ms) -> unregister+kfree at t=127249 (park
    +208 ms, reader still parked 792 ms) -> park ends t=128045:
    BUG: KASAN: slab-use-after-free in __srcu_read_lock+0xe
    BUG: KASAN: slab-use-after-free in __srcu_read_lock+0x25
    BUG: KASAN: slab-use-after-free in __srcu_read_unlock+0x1a
      read stack: __srcu_read_lock <- rtnl_dump_ifinfo+0x374 <-
        rtnl_dumpit <- netlink_dump <- rtnetlink_rcv_msg (task nlr)
      bad address = object+0x10 == ops->srcu (matches the
      disassembly lea 0x10(%rbx))
      alloc: insmod kzalloc;  free: unregister thread kfree
  Fixed kernel (same sources, ONLY the upstream synchronize_rcu()
  added to rtnl_link_unregister()):
    park t=191305 -> park ENDS t=192306 -> unregister+kfree
    t=192307 -- the order literally inverts -- zero KASAN reports,
    dump completes with zero errors.  Same module, same parking.
  Natural runs (three samplings, up to ~267k dumps/reader plus
  6-CPU preemption spinners): zero hits; the window arithmetic
  (~100 ns hand-off vs ~4 ms unregister) explains it, and the
  controlled park models precisely the preemption the upstream
  fix's comment describes.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

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

// SPDX-License-Identifier: GPL-2.0+
/*
 * nlr.c - userspace RTM_GETLINK dump hammer for the rtnl_link_ops_get()
 * verification (audit-report Case 1).
 *
 * Sends NLM_F_DUMP RTM_GETLINK requests carrying a nested
 * IFLA_LINKINFO{ IFLA_INFO_KIND=<kind> } filter, which drives exactly:
 *
 *   rtnl_dump_ifinfo -> linkinfo_to_kind_ops -> rtnl_link_ops_get(kind)
 *     -> (inlined) list_for_each_entry_rcu + strcmp + srcu_read_lock(&ops->srcu)
 *
 * i.e. the audited reader path. The dump walk itself runs without RTNL
 * (RTM_GETLINK is registered RTNL_FLAG_DUMP_UNLOCKED), so it is fully
 * concurrent with rtnl_link_unregister().
 *
 * Usage: nlr <kind> [seconds]
 */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>

#ifndef RTNL_FAMILY_MAX
#define RTNL_FAMILY_MAX 33
#endif

#define NLR_BUF 65536

static int mk_dump_req(char *b, int blen, const char *kind, uint32_t seq)
{
	struct nlmsghdr *nh = (struct nlmsghdr *)b;
	struct ifinfomsg *ifi;
	struct rtattr *linfo, *ikind;
	int klen = strlen(kind) + 1;

	memset(b, 0, blen);
	nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
	nh->nlmsg_type = RTM_GETLINK;
	nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
	nh->nlmsg_seq = seq;
	nh->nlmsg_pid = 0;

	ifi = NLMSG_DATA(nh);
	ifi->ifi_family = AF_UNSPEC;

	/* nested IFLA_LINKINFO { IFLA_INFO_KIND = kind } */
	linfo = (struct rtattr *)((char *)b + NLMSG_ALIGN(nh->nlmsg_len));
	linfo->rta_type = IFLA_LINKINFO;
	linfo->rta_len = RTA_LENGTH(0);

	ikind = (struct rtattr *)((char *)linfo + RTA_ALIGN(linfo->rta_len));
	ikind->rta_type = IFLA_INFO_KIND;
	ikind->rta_len = RTA_LENGTH(klen);
	memcpy(RTA_DATA(ikind), kind, klen);

	linfo->rta_len = (char *)ikind + RTA_ALIGN(ikind->rta_len) - (char *)linfo;
	nh->nlmsg_len = (char *)ikind + RTA_ALIGN(ikind->rta_len) - b;
	return nh->nlmsg_len;
}

int main(int argc, char **argv)
{
	const char *kind = argc > 1 ? argv[1] : "rtnlrace";
	int secs = argc > 2 ? atoi(argv[2]) : 30;
	int fd, iters = 0, errs = 0;
	uint32_t seq = 1;
	char req[NLR_BUF], buf[NLR_BUF];
	struct timeval tv = { .tv_sec = 1, .tv_usec = 0 };
	time_t t0 = time(NULL);

	fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
	if (fd < 0) {
		perror("socket");
		return 1;
	}
	setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

	fprintf(stderr, "nlr: hammering RTM_GETLINK dumps with kind=%s for %ds\n",
		kind, secs);

	while (time(NULL) - t0 < secs) {
		struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
		struct iovec iov = { buf, sizeof(buf) };
		struct msghdr msg = {
			.msg_name = &sa, .msg_namelen = sizeof(sa),
			.msg_iov = &iov, .msg_iovlen = 1,
		};
		int len = mk_dump_req(req, sizeof(req), kind, seq++);
		ssize_t n;

		if (sendto(fd, req, len, 0, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
			errs++;
			continue;
		}
		/* drain until DONE or timeout/error */
		for (;;) {
			n = recvmsg(fd, &msg, 0);
			if (n <= 0) {
				errs++;
				break;
			}
			{
				struct nlmsghdr *nh = (struct nlmsghdr *)buf;

				for (; NLMSG_OK(nh, (unsigned)n);
				     nh = NLMSG_NEXT(nh, n)) {
					if (nh->nlmsg_type == NLMSG_DONE ||
					    nh->nlmsg_type == NLMSG_ERROR)
						goto next_iter;
				}
			}
		}
next_iter:
		iters++;
		if ((iters & 0x3ff) == 0)
			fprintf(stderr, "nlr: %d iters (%d errs)\n", iters, errs);
	}
	fprintf(stderr, "nlr: done: %d iters, %d errs\n", iters, errs);
	return 0;
}

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

// SPDX-License-Identifier: GPL-2.0+
/*
 * rtnlrace.c - dynamic verification for the audit-report Case 1:
 *
 *   rtnl_link_unregister() (net/core/rtnetlink.c:701-729) is missing the
 *   synchronize_rcu() that upstream "rtnetlink: wait for RCU readers in
 *   rtnl_link_unregister()" (Fixes: 6b57ff21a310) added.
 *
 *   Reader window under test (rtnetlink.c:595-606, rtnl_link_ops_get inlined):
 *        rcu_read_lock();                       // :595
 *        list_for_each_entry_rcu(...)           // :597  found ops via RCU
 *          strcmp(ops->kind, kind)              // :598  matched
 *          *srcu_index = srcu_read_lock(&ops->srcu);   // :599  <-- handoff
 *        rcu_read_unlock();                     // :606
 *
 * Controlled mode (stall_us > 0):
 *   - registers a kzalloc()'d rtnl_link_ops with kind "rtnlrace"
 *     (rtnl_link_register is EXPORT_SYMBOL_GPL), so a userspace RTM_GETLINK
 *     dump with IFLA_INFO_KIND=rtnlrace walks exactly the audited path.
 *   - a kprobe on __srcu_read_lock with return-address filter parks the dump
 *     reader exactly at the :599 handoff (inside the RCU read-side critical
 *     section, before any SRCU state is touched): the audited window, enlarged
 *     for deterministic observation (same technique as kmod/r4_stall.c).
 *   - the unregister kthread then performs what module_exit does:
 *     rtnl_link_unregister(ops) -> list_del_rcu + synchronize_srcu (returns
 *     immediately: the parked reader has NOT entered the SRCU region) +
 *     cleanup_srcu_struct (frees srcu_usage and the percpu sda, sets
 *     ssp->srcu_sup/sda to NULL, leaves srcu_ctrp dangling), then kfree(ops)
 *     (models module_memfree of the unloaded module's static storage; KASAN
 *     slab poisoning makes the post-free read observable).
 *   - the parked reader resumes: READ_ONCE(ssp->srcu_ctrp) on freed memory
 *     => KASAN use-after-free Read in __srcu_read_lock.
 *
 * Natural mode (stall_us == 0): no kprobe, no init registration. A kthread
 * hammers kzalloc/rtnl_link_register/rtnl_link_unregister/kfree cycles of a
 * kind-"rtnlrace" ops while userspace hammers dumps; the natural
 * strcmp->srcu_read_lock window is the only interleaving (rmmod shape).
 *
 * Offsets for build-kasan-7.3/vmlinux (rtnl_link_ops_get fully inlined):
 *   rtnl_dump_ifinfo @ 0xffffffff826e8030 (size 0x925)
 *     call __srcu_read_lock @ +0x36f -> return address +0x374 (kprobe base skips ENDBR, live filter +0x370)
 */
#define pr_fmt(fmt) "RTNLRACE: " fmt

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

static int stall_ms = 1000;	/* 0 = natural mode */
module_param(stall_ms, int, 0600);
MODULE_PARM_DESC(stall_ms, "park the dump reader this long at the handoff (0=natural); implemented as udelay(10000) x N to dodge x86 __bad_udelay truncation for >20ms values");

static int unreg_delay_ms = 120;
module_param(unreg_delay_ms, int, 0600);

static int natural_secs = 90;
module_param(natural_secs, int, 0600);

static ulong ret_off = 0x370;
module_param(ret_off, ulong, 0600);
MODULE_PARM_DESC(ret_off, "return-address offset of the __srcu_read_lock call inside rtnl_dump_ifinfo");

static struct rtnl_link_ops *rops;	/* controlled mode only */
static atomic_t hits = ATOMIC_INIT(0);
static atomic_t parked = ATOMIC_INIT(0);
static atomic_t freed = ATOMIC_INIT(0);
static atomic_t rounds = ATOMIC_INIT(0);
static unsigned long base_addr;
static struct kprobe kp;
static int probe_live;
static struct task_struct *uthr;

static int stall_pre(struct kprobe *p, struct pt_regs *regs)
{
	unsigned long ra = *(unsigned long *)regs->sp;

	if (ra == base_addr + ret_off) {
		int h = atomic_inc_return(&hits);
		u64 t0 = ktime_to_ms(ktime_get());
		int i;

		atomic_set(&parked, 1);
		if (h <= 3)
			pr_warn("stall #%d: parking dump reader %d ms at handoff (ra=%lx == rtnl_dump_ifinfo+0x%lx) t=%llu\n",
				h, stall_ms, ra, ret_off, t0);
		for (i = 0; i < stall_ms / 10; i++)
			udelay(10000);
		if (h <= 3)
			pr_warn("stall #%d: park END t=%llu (held %llu ms)\n",
				h, ktime_to_ms(ktime_get()),
				ktime_to_ms(ktime_get()) - t0);
	} else if (ra > base_addr && ra < base_addr + 0x2000) {
		static atomic_t dbg;

		int d = atomic_inc_return(&dbg);

		if (d <= 8)
			pr_warn("near-caller #%d ra-off=0x%lx\n", d, ra - base_addr);
	}
	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 unreg_thr(void *unused)
{
	if (stall_ms > 0) {
		/* controlled: wait until the kprobe parked the dump reader */
		while (!atomic_read(&parked) && !kthread_should_stop())
			msleep(5);
		if (kthread_should_stop())
			return 0;
		msleep(unreg_delay_ms);

		rtnl_link_unregister(rops);
		/* protocol evidence: what did cleanup_srcu_struct leave behind?
		 * (rops itself is still allocated here, so these reads are safe) */
		pr_warn("post-unregister t=%llu: ops=%px srcu.srcu_sup=%px srcu.sda=%px srcu.srcu_ctrp=%px\n",
			ktime_to_ms(ktime_get()), rops, rops->srcu.srcu_sup, rops->srcu.sda,
			rops->srcu.srcu_ctrp);
		kfree(rops);
		atomic_set(&freed, 1);
		pr_warn("ops kfree()d (models module_memfree of the unloaded module); parked reader will resume on freed memory\n");
	} else {
		/* natural: hammer kzalloc/register/unregister/kfree (rmmod shape) */
		u64 t0 = ktime_to_ms(ktime_get());

		while (!kthread_should_stop() &&
		       ktime_to_ms(ktime_get()) - t0 < (u64)natural_secs * 1000) {
			struct rtnl_link_ops *o = kzalloc(sizeof(*o), GFP_KERNEL);

			if (!o)
				break;
			o->kind = "rtnlrace";
			if (rtnl_link_register(o) == 0) {
				rtnl_link_unregister(o);
				kfree(o);
			} else {
				kfree(o);
			}
			atomic_inc(&rounds);
			if ((atomic_read(&rounds) & 0x3ff) == 0)
				pr_warn("natural round %d\n", atomic_read(&rounds));
		}
		pr_warn("natural done: %d register/unregister/kfree rounds\n",
			atomic_read(&rounds));
	}
	return 0;
}

static int __init rtnlrace_init(void)
{
	int ret;

	if (stall_ms > 0) {
		rops = kzalloc(sizeof(*rops), GFP_KERNEL);
		if (!rops)
			return -ENOMEM;
		rops->kind = "rtnlrace";
		ret = rtnl_link_register(rops);
		if (ret) {
			pr_err("rtnl_link_register failed: %d\n", ret);
			kfree(rops);
			rops = NULL;
			return ret;
		}
		pr_warn("registered rtnl_link_ops kind=rtnlrace (%px); start: /nlr rtnlrace\n", rops);

		base_addr = sym_addr("rtnl_dump_ifinfo");
		if (!base_addr) {
			pr_err("cannot resolve rtnl_dump_ifinfo\n");
			return -EINVAL;
		}
		kp.symbol_name = "__srcu_read_lock";
		kp.pre_handler = stall_pre;
		ret = register_kprobe(&kp);
		if (ret) {
			pr_err("cannot register kprobe: %d\n", ret);
			return ret;
		}
		probe_live = 1;
		pr_warn("kprobe on __srcu_read_lock, park ra=rtnl_dump_ifinfo+0x%lx (%px)\n",
			ret_off, (void *)(base_addr + ret_off));
	}

	uthr = kthread_run(unreg_thr, NULL, "rtnlrace_unreg");
	if (IS_ERR(uthr))
		return PTR_ERR(uthr);
	pr_warn("armed: stall_ms=%d unreg_delay_ms=%d mode=%s\n", stall_ms,
		unreg_delay_ms, stall_ms > 0 ? "controlled" : "natural");
	return 0;
}

static void __exit rtnlrace_exit(void)
{
	if (uthr)
		kthread_stop(uthr);
	if (probe_live)
		unregister_kprobe(&kp);
	if (stall_ms > 0 && atomic_read(&freed) == 0 && rops) {
		rtnl_link_unregister(rops);
		kfree(rops);
	}
	pr_warn("module exit (hits=%d rounds=%d)\n", atomic_read(&hits),
		atomic_read(&rounds));
}

module_init(rtnlrace_init);
module_exit(rtnlrace_exit);
MODULE_LICENSE("GPL");

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

* wifi: wilc1000: SDIO/SPI remove destroys the srcu_struct  before wiphy_unregister() -- late cfg80211 callbacks write through  dangling srcu state (unfixed upstream; controlled + natural Oops)
  2026-10-27 14:12 ` Greg KH
                     ` (3 preceding siblings ...)
  2026-11-03 11:57   ` net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3 徐晗翔
@ 2026-11-03 12:07   ` 徐晗翔
  2026-11-03 12:10   ` gpio: gpiolib: final put of the gpio_device happens inside the SRCU read critical section on the setup-failure path -- deterministic use-after-free read at guard release (unfixed upstream) 徐晗翔
                     ` (6 subsequent siblings)
  11 siblings, 0 replies; 21+ messages in thread
From: 徐晗翔 @ 2026-11-03 12:07 UTC (permalink / raw)
  To: ajay.kathat, claudiu.beznea; +Cc: linux-wireless, linux-kernel, netdev

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

In wilc1000's SDIO/SPI remove path, the driver destroys its
srcu_struct (cleanup_srcu_struct at sdio.c:228-229, via
wlan_deinit_locks) BEFORE wiphy_unregister() tears down the wiphy --
so any cfg80211 callback that arrives late and holds only the wiphy
lock (which nl80211 releases before dispatching, nl80211.c:4303)
will execute srcu_read_lock()/srcu_read_unlock() on an already
destroyed srcu_struct whose backing percpu memory is freed: the
counter writes in __srcu_read_lock/unlock land on
__per_cpu_offset[cpu]+{0,16} -- the first bytes of the current
CPU's percpu area, corrupting whatever lives there.  I verified the
mechanism on a stock 7.3 KASAN kernel with a field/lock-faithful
simulator of the two sides: the controlled run pins the reader
inside the callback (holding only the simulated wiphy_lock), runs
the real cleanup_srcu_struct() underneath, then deliberately
re-allocates percpu memory at the freed sda address -- the reader's
subsequent srcu_read_lock increments a counter inside that
replacement object (read back as +1: cross-object percpu corruption
demonstrated); the natural run, with no instrumentation at all,
crashes the kernel in under one second of reader/init/cleanup churn
with a page-fault write Oops in __srcu_read_unlock+0x25 whose
disassembly state matches the dangling-state chain instruction for
instruction (RAX=0 -> sda==NULL, RSI=0x10, incq %gs:0x8(%rax,%rsi)
-> a write into the first 32 bytes of the CPU's percpu area).  The
fix is the ordering: wiphy_unregister() (which waits for the wiphy
srcu readers) must happen before cleanup_srcu_struct().  As far as I
can tell this is unfixed in mainline.

The detailed description:

* The ordering bug (Linux 7.3)
  drivers/net/wireless/microchip/wilc1000/sdio.c remove path (SPI
  variant identical): ... -> wilc_netdev_cleanup ->
  wlan_deinit_locks -> cleanup_srcu_struct(&wilc->srcu) (sdio.c:
  228-229), while the wiphy is still registered.  cfg80211 dispatch
  (e.g. set_wiphy_params, whose first statement is
  srcu_read_lock(cfg80211.c:803); likewise wilc_set_wakeup) holds
  only wiphy_lock -- rtnl is already released at nl80211.c:4303 --
  so the driver's remove path and a late callback share no lock.

* What the destroyed-srcu accesses do
  cleanup_srcu_struct() frees the sda percpu arrays (or leaks them
  into the WARN branch when a reader is caught inside, which is
  itself observable) and leaves srcu_ctrp dangling or the unlock
  path reading sda==NULL: __srcu_read_unlock's
  this_cpu_inc(&sdp->locks[...]) then computes GS-base+0x8(0,16) --
  a blind percpu write.  Both jumps of the hazard chain were
  demonstrated (see below).

* Evidence (stock 7.3 KASAN; simulator attached)
  wilcsim.c mirrors the real field/lock structure
  (sim wiphy_lock <-> wiphy_lock; reader parked at the callback's
  srcu_read_lock; remover executing the real cleanup_srcu_struct()
  without any of the reader's locks -- faithful to "no common lock"
  in the real paths):
  Controlled: cleanup completes with sda freed and ctrp dangling;
  a same-size alloc_percpu lands at the EXACT freed sda address
  (zero retries) carrying markers; the released reader's
  srcu_read_lock executes and the marker on cpu3 reads back
  ctr[0].locks=+1 -- the dangling counter write, captured inside
  the replacement object.
  Natural (no kprobe, reader lock/unlock loop vs init/cleanup
  churn): by round ~768 an interleaving hits: Oops 0002 (write
  fault), RIP __srcu_read_unlock+0x25, RAX=0, RSI=0x10, instruction
  65 48 ff 44 30 08 -- writing GS-base+0x18, i.e. inside the first
  32 bytes of the CPU's percpu area.  Kernel dead <1 s into the
  churn.  (Three WARNING: srcutree.c:696 events during the churn
  additionally show the remover catching readers mid-section -- the
  defensive leak branch.)
  Fidelity note: real wilc1000 hardware was not available; the
  simulator reproduces the mechanism (the exact srcu API calls and
  lock topology of the real two paths) on the real library code.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

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

// SPDX-License-Identifier: GPL-2.0+
/*
 * wilcsim.c - dynamic verification for the audit-report Case 2 (wilc1000):
 *
 *   wilc_sdio_remove()/wilc_bus_remove() run wilc_netdev_cleanup() (which
 *   ends in cleanup_srcu_struct(&wilc->srcu), cfg80211.c:1757) BEFORE
 *   wiphy_unregister() (sdio.c:228-229). While the wiphy is still
 *   registered, cfg80211 callbacks can still enter and several of them
 *   (set_wiphy_params cfg80211.c:803, wilc_set_wakeup :1626) take
 *   wl->srcu as their first statement, dispatched under wiphy_lock only
 *   (nl80211_set_wiphy releases rtnl at nl80211.c:4303 before the callback;
 *   SET_WOWLAN has no NEED_RTNL) - the remove path holds neither.
 *
 * This module reproduces the exact interleaving shape with a simulated
 * private struct (struct wilc equivalent) and a simulated wiphy_lock,
 * and makes the otherwise-silent post-cleanup writes OBSERVABLE:
 *
 * Controlled mode (default):
 *   reader kthread   = the cfg80211 callback: takes sim_wiphy_lock, "parks"
 *                      at the audited point (before srcu_read_lock), then
 *                      resumes on the destroyed srcu.
 *   remover kthread  = wilc_sdio_remove: with the reader parked (and NOT
 *                      holding any lock the remover needs - faithful),
 *                      cleanup_srcu_struct() frees srcu_usage + percpu sda.
 *   Then the percpu allocator is asked for a same-size allocation until it
 *   hands back EXACTLY the freed sda address (percpu block reuse), and the
 *   reader resumes: its stale srcu_ctrp this_cpu_inc lands inside the new
 *   object => directly readable cross-object percpu corruption (the
 *   audited harm chain link 1). The unlock path additionally writes at
 *   percpu offset 0/16 (link 2) because ssp->sda == NULL.
 *
 * Natural mode (natural_secs>0 & controlled=0): reader loops
 * lock/unlock and remover loops init/cleanup without any parking; the
 * post-cleanup unlock writes at percpu offset 0/16 corrupt the first
 * percpu symbol of the executing CPU -> expected rapid crash (severity
 * demonstration).
 */
#define pr_fmt(fmt) "WILCSIM: " fmt

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/srcu.h>
#include <linux/mutex.h>
#include <linux/kthread.h>
#include <linux/completion.h>
#include <linux/atomic.h>
#include <linux/delay.h>
#include <linux/cpumask.h>

struct sim_wl {
	struct mutex	wiphy_lock_sim;	/* models wiphy->mtx (nl80211 dispatch) */
	struct srcu_struct srcu;	/* models wl->srcu */
};

static int controlled = 1;
module_param(controlled, int, 0600);
MODULE_PARM_DESC(controlled, "1=controlled parking interleave, 0=natural hammer");

static int natural_secs = 30;
module_param(natural_secs, int, 0600);

static int reuse_tries = 128;
module_param(reuse_tries, int, 0600);

static struct sim_wl *wl;
static struct srcu_data __percpu *marker;	/* reallocated over freed sda */
static struct completion parked;
static struct completion go;
static struct completion reader_done;
static atomic_t natural_rounds = ATOMIC_INIT(0);
static atomic_t natural_warns = ATOMIC_INIT(0);

static long ctr_read(int cpu, int slot, int which)
{
	struct srcu_data *sd = per_cpu_ptr(marker, cpu);

	if (slot == 0 && which == 0)
		return atomic_long_read(&sd->srcu_ctrs[0].srcu_locks);
	if (slot == 0 && which == 1)
		return atomic_long_read(&sd->srcu_ctrs[0].srcu_unlocks);
	if (slot == 1 && which == 0)
		return atomic_long_read(&sd->srcu_ctrs[1].srcu_locks);
	return atomic_long_read(&sd->srcu_ctrs[1].srcu_unlocks);
}

static int reader_thr(void *unused)
{
	int cpu;

	if (controlled) {
		mutex_lock(&wl->wiphy_lock_sim);
		pr_info("reader parked inside callback (holds sim wiphy_lock; remove path needs neither lock)\n");
		complete(&parked);
		wait_for_completion(&go);

		cpu = smp_processor_id();
		{
			struct srcu_ctr __percpu *ctrp =
				READ_ONCE(wl->srcu.srcu_ctrp);
			int idx;
			long b00 = ctr_read(cpu, 0, 0), b01 = ctr_read(cpu, 0, 1);
			long b10 = ctr_read(cpu, 1, 0), b11 = ctr_read(cpu, 1, 1);

			pr_info("reader resumed: stale srcu_ctrp=%px (sda freed), executing srcu_read_lock on DESTROYED srcu\n",
				ctrp);
			idx = srcu_read_lock(&wl->srcu);
			/* NOTE: srcu_read_unlock() deliberately NOT executed in the
			 * controlled run: with ssp->sda == NULL it would write at
			 * percpu offset 0/16 (the first percpu symbol of this CPU)
			 * and could kill the evidence path itself; that write is
			 * demonstrated live by the wilcnat natural run instead. */
			pr_info("reader after lock (idx=%d): marker[cpu%d] deltas ctr[0].locks=%ld ctr[0].unlocks=%ld ctr[1].locks=%ld ctr[1].unlocks=%ld\n",
				idx, cpu,
				ctr_read(cpu, 0, 0) - b00,
				ctr_read(cpu, 0, 1) - b01,
				ctr_read(cpu, 1, 0) - b10,
				ctr_read(cpu, 1, 1) - b11);
			if (ctr_read(cpu, 0, 0) != b00 || ctr_read(cpu, 1, 0) != b10)
				pr_warn("CROSS-OBJECT PERCPU CORRUPTION OBSERVED: stale srcu_ctrp write landed inside the reallocated percpu object\n");
		}
		mutex_unlock(&wl->wiphy_lock_sim);
		complete(&reader_done);
	} else {
		while (!kthread_should_stop()) {
			int idx;

			mutex_lock(&wl->wiphy_lock_sim);
			idx = srcu_read_lock(&wl->srcu);
			srcu_read_unlock(&wl->srcu, idx);
			mutex_unlock(&wl->wiphy_lock_sim);
			cond_resched();
		}
	}
	return 0;
}

static int remove_thr(void *unused)
{
	struct srcu_data __percpu *old_sda;
	int i;

	if (controlled) {
		wait_for_completion(&parked);

		old_sda = wl->srcu.sda;
		pr_info("remove path: cleanup_srcu_struct while reader parked in callback (pre sda=%px ctrp=%px)\n",
			old_sda, wl->srcu.srcu_ctrp);
		cleanup_srcu_struct(&wl->srcu);
		pr_info("cleanup done: srcu.sda=%px srcu.srcu_sup=%px (NULL=NULL => freed) srcu_ctrp=%px (stale, never reset)\n",
			wl->srcu.sda, wl->srcu.srcu_sup, wl->srcu.srcu_ctrp);

		/* reallocate over the freed percpu block: same size, retry until
		 * the allocator hands back the exact address */
		for (i = 0; i < reuse_tries; i++) {
			marker = alloc_percpu(struct srcu_data);
			if (!marker) {
				pr_err("alloc_percpu failed\n");
				break;
			}
			if (marker == old_sda)
				break;
			free_percpu(marker);
			marker = NULL;
		}
		if (marker == old_sda && marker)
			pr_info("marker reallocated at EXACT freed sda address %px after %d tries\n",
				marker, i);
		else
			pr_warn("exact reuse not achieved (marker=%px old=%px tries=%d); chunk-level overlap only\n",
				marker, old_sda, i);
		complete(&go);
		wait_for_completion(&reader_done);

		/* full per-cpu marker scan */
		for_each_possible_cpu(i) {
			long v0 = ctr_read(i, 0, 0), v1 = ctr_read(i, 1, 0);

			if (v0 || v1)
				pr_warn("marker cpu%d: ctr[0].locks=%ld ctr[1].locks=%ld\n", i, v0, v1);
		}
		pr_info("controlled interleave complete\n");
	} else {
		u64 t0 = ktime_to_ms(ktime_get());

		while (ktime_to_ms(ktime_get()) - t0 < (u64)natural_secs * 1000 &&
		       !kthread_should_stop()) {
			cleanup_srcu_struct(&wl->srcu);	/* may WARN+leak if reader inside */
			if (atomic_read(&natural_warns) > 4096)
				break;
			init_srcu_struct(&wl->srcu);
			atomic_inc(&natural_rounds);
			if ((atomic_read(&natural_rounds) & 0xff) == 0)
				pr_warn("natural round %d\n", atomic_read(&natural_rounds));
		}
		pr_warn("natural done: %d init/cleanup rounds\n",
			atomic_read(&natural_rounds));
	}
	return 0;
}

static int __init wilcsim_init(void)
{
	struct task_struct *r, *m;
	int ret;

	wl = kzalloc(sizeof(*wl), GFP_KERNEL);
	if (!wl)
		return -ENOMEM;
	mutex_init(&wl->wiphy_lock_sim);
	ret = init_srcu_struct(&wl->srcu);
	if (ret) {
		kfree(wl);
		return ret;
	}
	init_completion(&parked);
	init_completion(&go);
	init_completion(&reader_done);

	r = kthread_run(reader_thr, NULL, "wilcsim_reader");
	m = kthread_run(remove_thr, NULL, "wilcsim_remove");
	if (IS_ERR(r) || IS_ERR(m))
		return IS_ERR(r) ? PTR_ERR(r) : PTR_ERR(m);
	pr_warn("armed: mode=%s\n", controlled ? "controlled" : "natural");
	return 0;
}

static void __exit wilcsim_exit(void)
{
	/* natural mode: threads self-terminate after natural_secs; controlled:
	 * both completions are done. cleanup may leak sup intentionally. */
	if (wl) {
		/* best effort: if srcu still live, clean it */
		if (wl->srcu.srcu_sup)
			cleanup_srcu_struct(&wl->srcu);
		if (marker)
			free_percpu(marker);
		kfree(wl);
	}
	pr_warn("module exit (natural_rounds=%d)\n", atomic_read(&natural_rounds));
}

module_init(wilcsim_init);
module_exit(wilcsim_exit);
MODULE_LICENSE("GPL");

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

* gpio: gpiolib: final put of the gpio_device happens inside  the SRCU read critical section on the setup-failure path -- deterministic use-after-free read at guard release (unfixed upstream)
  2026-10-27 14:12 ` Greg KH
                     ` (4 preceding siblings ...)
  2026-11-03 12:07   ` wifi: wilc1000: SDIO/SPI remove destroys the srcu_struct before wiphy_unregister() -- late cfg80211 callbacks write through dangling srcu state (unfixed upstream; controlled + natural Oops) 徐晗翔
@ 2026-11-03 12:10   ` 徐晗翔
  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 subsequent siblings)
  11 siblings, 0 replies; 21+ messages in thread
From: 徐晗翔 @ 2026-11-03 12:10 UTC (permalink / raw)
  To: linusw, brgl; +Cc: linux-gpio, linux-kernel

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

On gpiolib's setup-failure path, the guard that protects the
gpio_device lookup (srcu_read_lock at gpiolib.c:1094) is also held
while the device is being torn down: gpiochip_setup_devs()'s error
path drops the last reference (gpiodev_release at :893-906, which
calls cleanup_srcu_struct at :904 and kfree at :905) while the
caller's guard is still inside the read critical section -- so the
guard's own release (srcu_read_unlock) reads ssp->sda out of an
already freed gpio_device.  In the hog path the refcount can be
exactly 1 at that point, which is the common case, so this is not
an exotic window: it is a sequential-ordering violation that fires
whenever that error path is taken, no concurrency needed.  On a
stock 7.3 KASAN kernel, replaying gpiolib's exact single-threaded
statement sequence 1:1 (lock at the :1094 guard point -> the :904
cleanup -> the :905 kfree -> the guard's unlock) hits
WARNING: kernel/rcu/srcutree.c:696 at cleanup_srcu_struct (the
self-held-reader defensive branch: sda and srcu_sup leaked, not
freed), and the subsequent srcu_read_unlock produces
BUG: KASAN: slab-use-after-free in __srcu_read_unlock+0x1a reading
object+8 -- exactly the ssp->sda field of the freed container.  The
setup-failure path also misses the list_del_rcu() that the success
path performs, so the destroyed device can remain reachable through
the global list.  As far as I can tell this is unfixed upstream.

The detailed description:
* The ordering (Linux 7.3, drivers/gpio/gpiolib.c)
  The caller takes the lookup guard (srcu_read_lock, :1094), then
  gpiochip_setup_devs() fails (e.g. cdev/hog registration errors),
  and the error path drops the final reference;
  gpiodev_release() runs cleanup_srcu_struct(&gdev->srcu) at :904
  and kfree(gdev) at :905 -- while the caller is still inside the
  SRCU read critical section it took at :1094.  When the caller's
  guard destructs, srcu_read_unlock(&gdev->srcu, idx) touches the
  freed object (reads ssp->sda at object offset +8).  The hog path
  leaves refcount==1 here as the common case (no other holders
  yet), so the final put is genuinely final.
  Secondary: the failure path lacks the list_del_rcu() the success
  path performs, leaving the freed device linked.

* Evidence (stock 7.3 KASAN; deterministic, single-threaded;
  reproducer attached)
  gpiodevsim.c replays gpiolib's own sequence with a kzalloc'd
  container of the same layout: srcu_read_lock (the :1094 guard) ->
  cleanup_srcu_struct (the :904 call; hits the srcutree.c:696
  WARNING -- the library's own "reader still inside" defense
  triggers, leaking sda/srcu_sup exactly as the source predicts) ->
  kfree (the :905 call) -> srcu_read_unlock (the guard's
  destructor):
    WARNING: kernel/rcu/srcutree.c:696 at cleanup_srcu_struct
      (leak branch: srcu.sda non-NULL, srcu.srcu_sup non-NULL)
    BUG: KASAN: slab-use-after-free in __srcu_read_unlock+0x1a
      Read of size 8, address = object+8 == the ssp->sda field
      alloc: insmod kzalloc;  free: the kfree step above
  One run, no interleaving, no kprobe parking -- the violation is
  in the ordering itself.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

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

// SPDX-License-Identifier: GPL-2.0+
/*
 * gpiodevsim.c - dynamic verification for the audit-report Case 3 (gpiolib):
 *
 *   gpiochip_setup_devs() (drivers/gpio/gpiolib.c:1084-1109) holds
 *   guard(srcu)(&gdev->srcu) across the loop body and, on
 *   gpiochip_setup_dev() failure, calls gpio_device_put(gdev) at :1104
 *   while the read lock is still held. With refcount==1 (the norm: hogs do
 *   not take device references) gpiodev_release() runs inline:
 *     :898 sync+cleanup desc_srcu (fine)
 *     :904 cleanup_srcu_struct(&gdev->srcu) -> WARN_ON(srcu_readers_active)
 *            at kernel/rcu/srcutree.c:696 -> early return, LEAK (sup/sda
 *            stay allocated), i.e. the defensive branch the report predicts
 *     :905 kfree(gdev)
 *   then the same stack continues on freed memory:
 *     :1105 dev_err(&gdev->dev, ...)          (UAF read)
 *     loop iterator reads gdev->list.next     (UAF read)
 *     guard destructor srcu_read_unlock()     (UAF read of gdev->srcu.sda
 *            via __srcu_ctr_to_ptr inside __srcu_read_unlock)
 *
 * This module reproduces that exact single-thread sequence with a
 * kzalloc()'d stand-in for struct gpio_device (embedded srcu_struct) and
 * makes the final unlock observable as a KASAN use-after-free read:
 *
 *   g = kzalloc(); init_srcu_struct(&g->srcu);
 *   idx = srcu_read_lock(&g->srcu);      // :1094 guard
 *   cleanup_srcu_struct(&g->srcu);       // expect WARN srcutree.c:696 + leak
 *   print post-cleanup sda/srcu_sup (both non-NULL => leak branch proof)
 *   kfree(g);                            // :905
 *   srcu_read_unlock(&g->srcu, idx);     // guard destructor => KASAN UAF
 */
#define pr_fmt(fmt) "GPIODEVSIM: " fmt

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/srcu.h>

struct sim_gdev {
	struct srcu_struct srcu;	/* first member, like gpio_device usage */
};

static int __init gpiodevsim_init(void)
{
	struct sim_gdev *g;
	int idx;

	g = kzalloc(sizeof(*g), GFP_KERNEL);
	if (!g)
		return -ENOMEM;
	if (init_srcu_struct(&g->srcu)) {
		kfree(g);
		return -ENOMEM;
	}
	pr_info("sim gdev=%px srcu initialized (sda=%px sup=%px ctrp=%px)\n",
		g, g->srcu.sda, g->srcu.srcu_sup, g->srcu.srcu_ctrp);

	/* gpiolib.c:1094 guard(srcu)(&gdev->srcu) */
	idx = srcu_read_lock(&g->srcu);
	pr_info("read lock taken idx=%d (self-reader active: the put at :1104 will meet it)\n", idx);

	/* gpiodev_release():904 - expect WARN at srcutree.c:696, early return */
	cleanup_srcu_struct(&g->srcu);
	pr_info("post-cleanup: srcu.sda=%px srcu.srcu_sup=%px (BOTH non-NULL => WARN+LEAK branch taken, no free_percpu) ctrp=%px (stale)\n",
		g->srcu.sda, g->srcu.srcu_sup, g->srcu.srcu_ctrp);

	/* gpiodev_release():905 */
	kfree(g);
	pr_info("sim gdev kfree()d; guard destructor will now read g->srcu.sda from freed slab\n");

	/* gpiochip_setup_devs() loop-body end: guard(srcu) destructor */
	srcu_read_unlock(&g->srcu, idx);

	/* NOT REACHED on a KASAN kernel without kasan_multi_shot only if the
	 * access faults; with multi-shot the report is printed and execution
	 * continues here. */
	pr_warn("sequence complete: expected a KASAN use-after-free READ report for the unlock above\n");
	return 0;
}

static void __exit gpiodevsim_exit(void)
{
	pr_warn("module exit (leaked srcu_usage of the WARN'd srcu is intentional, matching gpiolib behavior)\n");
}

module_init(gpiodevsim_init);
module_exit(gpiodevsim_exit);
MODULE_LICENSE("GPL");

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

* Re: cgroup: fork vs cgroup.procs write race in favordynmods mode -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the stable trees (repro incl. uninstrumented natural hit)
  2026-11-03 11:48   ` cgroup: fork vs cgroup.procs write race in favordynmods mode -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the stable trees (repro incl. uninstrumented natural hit) 徐晗翔
@ 2026-11-03 14:51     ` Sasha Levin
  0 siblings, 0 replies; 21+ messages in thread
From: Sasha Levin @ 2026-11-03 14:51 UTC (permalink / raw)
  To: tj, hannes, mkoutny
  Cc: Sasha Levin, cgroups, linux-kernel, stable, 徐晗翔

> This is fixed upstream by bf6fbf46b5ff ("cgroup: serialize migrations of newly
> forked processes", merged 2026-10-20, already tagged Cc: stable #
> 6.18+), which v7.3 predates.

Queued for 7.3 and 6.18, thanks.

-- 
Thanks,
Sasha

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

* Re: net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3
  2026-11-03 11:57   ` net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3 徐晗翔
@ 2026-11-03 15:49     ` Eric Dumazet
  2026-11-04 15:03     ` Sasha Levin
  1 sibling, 0 replies; 21+ messages in thread
From: Eric Dumazet @ 2026-11-03 15:49 UTC (permalink / raw)
  To: 徐晗翔
  Cc: davem, kuba, pabeni, netdev, linux-kernel, stable

Le mar. 3 nov. 2026 à 12:57, 徐晗翔 <xuhx@hust.edu.cn> a écrit :
>
> On v7.3, rtnl_link_unregister() removes the rtnl_link_ops from the
> global list and immediately destroys the embedded srcu_struct and
> frees the ops memory -- a concurrent RTM_GETLINK dump that already
> found the ops (rtnl_link_ops_get(), rtnetlink.c:599 hand-off inside
> an RCU read critical section at :595) but has not yet taken
> ops->srcu will, after the free, execute __srcu_read_lock() /
> __srcu_read_unlock() on freed memory: two reads of the destroyed
> object (ssp->srcu_ctrp, ssp->sda) whose subsequent percpu counter
> writes land wherever the freed percpu pointers point -- for a module
> rtnl_link_ops, the ops (and its srcu) live in module memory, so
> module_memfree() is the freeing side.  Upstream already fixed this
> with a synchronize_rcu() in rtnl_link_unregister() (the patch with
> Fixes: 6b57ff21a310; its own comment: "Wait for rtnl_link_ops_get()
> readers that found ops but have not yet taken ops->srcu").  v7.3
> does not contain it.  I verified both sides on a stock 7.3 KASAN
> kernel: parking a real dump reader (driven by an ordinary
> RTM_GETLINK + IFLA_LINKINFO dump request) exactly at the :599
> hand-off and unregistering+freeing underneath produces three KASAN
> slab-use-after-free reports (__srcu_read_lock+0xe, +0x25,
> __srcu_read_unlock+0x1a; bad address = ops+0x10 = ops->srcu); and
> rebuilding the SAME kernel with ONLY the upstream one-line fix
> flips the outcome to zero reports, with the kfree demonstrably
> delayed until the parked reader left its RCU section.  Natural
> hammering did not hit (the hand-off window is ~100 ns against a
> ~4 ms unregister -- it needs a preempted/preemptible-RCU reader),
> which matches the fix's own reasoning; the controlled run models
> exactly that preemption.  The purpose of this mail is to support
> the fix's propagation: trees still carrying the unfixed
> rtnl_link_unregister() keep a cross-CPU UAF (write) plus potential
> freed-module execution reachable by unprivileged netlink dumps
> racing a module unload.
>
>
> The detailed description:
>
> * The gap (v7.3, net/core/rtnetlink.c)
>   rtnl_dump_ifinfo() runs lockless for dumps
>   (RTNL_FLAG_DUMP_UNLOCKED), takes rcu_read_lock() at :595, calls
>   linkinfo_to_kind_ops() -> rtnl_link_ops_get() which returns the
>   ops pointer at :599, and only then takes ops->srcu
>   (srcu_read_lock) to call ops->dumpit.  rtnl_link_unregister()
>   holds rtnl_mutex, removes the ops from the list, calls
>   cleanup_srcu_struct(&ops->srcu) and returns to the module, whose
>   exit then frees the module memory.  Nothing between the :599
>   hand-off and the srcu_read_lock is waited for on the unregister
>   side in 7.3 -- hence the fix's synchronize_rcu().
>
> * Evidence (stock 7.3 KASAN, QEMU; A/B on the one-line fix;
>   reproducers attached)
>   rtnlrace.c registers a kzalloc'd rtnl_link_ops (kind "rtnlrace"),
>   parks a dump reader by kprobe at the exact hand-off (return
>   address rtnl_dump_ifinfo+0x374, ENDBR-adjusted), and unregisters
>   + kfrees from another thread (modeling module_exit ->
>   module_memfree).  nlr (userspace) sends the IFLA_LINKINFO dump
>   request that drives the reader path.
>   Defect kernel (stock 7.3):
>     park t=127041 (1004 ms) -> unregister+kfree at t=127249 (park
>     +208 ms, reader still parked 792 ms) -> park ends t=128045:
>     BUG: KASAN: slab-use-after-free in __srcu_read_lock+0xe
>     BUG: KASAN: slab-use-after-free in __srcu_read_lock+0x25
>     BUG: KASAN: slab-use-after-free in __srcu_read_unlock+0x1a
>       read stack: __srcu_read_lock <- rtnl_dump_ifinfo+0x374 <-
>         rtnl_dumpit <- netlink_dump <- rtnetlink_rcv_msg (task nlr)
>       bad address = object+0x10 == ops->srcu (matches the
>       disassembly lea 0x10(%rbx))
>       alloc: insmod kzalloc;  free: unregister thread kfree
>   Fixed kernel (same sources, ONLY the upstream synchronize_rcu()
>   added to rtnl_link_unregister()):
>     park t=191305 -> park ENDS t=192306 -> unregister+kfree
>     t=192307 -- the order literally inverts -- zero KASAN reports,
>     dump completes with zero errors.  Same module, same parking.
>   Natural runs (three samplings, up to ~267k dumps/reader plus
>   6-CPU preemption spinners): zero hits; the window arithmetic
>   (~100 ns hand-off vs ~4 ms unregister) explains it, and the
>   controlled park models precisely the preemption the upstream
>   fix's comment describes.
>

What is the point of this email ? This looks like LLM slope to me.

I think we have far more serious bugs to fix.

3b70edea393d was tagged stable+noautosel on purpose: the race needs
a module unload (CAP_SYS_MODULE in init_user_ns) to line up with a
~100ns window, which your report also shows you could not hit
without a kprobe. It is not reachable by an unprivileged user alone.

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

* Re: mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN  blind spot: slub.o is uninstrumented)
  2026-11-03 11:21   ` mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN blind spot: slub.o is uninstrumented) 徐晗翔
@ 2026-11-04  9:41     ` Qing Wang
  2026-11-04 10:07       ` Qing Wang
  0 siblings, 1 reply; 21+ messages in thread
From: Qing Wang @ 2026-11-04  9:41 UTC (permalink / raw)
  To: xuhx, Vlastimil Babka, Harry Yoo, Andrew Morton, Hao Li,
	Christoph Lameter, David Rientjes, Roman Gushchin
  Cc: linux-mm, linux-kernel

On Tue, 03 Nov 2026 at 19:21, 徐晗翔 <xuhx@hust.edu.cn> wrote:
> flush_all_cpus_locked() calls has_pcs_used() for every CPU to decide
> which flush work to skip, reading each remote CPU's pcs->spare /
> pcs->rcu_free and dereferencing pcs->main->size -- all locklessly
> (mm/slub.c:4041-4051, inlined into flush_all_cpus_locked) -- while
> the owning CPU's alloc/free fast paths mutate exactly those fields
> under only the per-CPU sheaf lock (pcs->main->size++/--, the
> pcs->main pointer swap in __pcs_replace_empty_main, etc.).  The
> reader runs from kmem_cache_shrink() (sysfs shrink attribute, memory
> offline) and is, as far as I can enumerate, the only cross-CPU
> consumer of this state outside the sysfs slabinfo walkers (excluded
> -- they read other fields under a different protocol).  I verified
> the race with KCSAN after discovering why nobody has ever seen it
> reported: mm/Makefile sets KCSAN_SANITIZE_slub.o := n, so BOTH sides
> of this race are structurally invisible to KCSAN on any stock kernel
> (verified at the disassembly level: the four accesses in
> flush_all_cpus_locked compile to bare cmp/mov with zero __tsan
> calls).  On a diagnostic build that only flips that Makefile switch
> (no source change), a shrink+churn workload on a dedicated cache
> produces 436 reader-pair reports covering all five writer families,
> including an 8-byte pcs->main pointer swap with an explicit "value
> changed" line -- the snapshot prerequisite of the (extremely
> unlikely, source-argued) use-after-free chain: a stale pcs->main can
> outlive its sheaf's retirement (spare -> rcu_free -> call_rcu ->
> kfree) if the reader is preempted between load and dereference.  The
> practical impact of the race itself is a wrong skip decision in
> sfw->skip; the fix is to move the emptiness check into the target
> CPU's flush callback, where the state is local and locked.

You're right, there is a data race in has_pcs_used() reading the pcs
fields.  The possible consequences:

1. has_pcs_used() observes an empty pcs and sets sfw->skip, so the
   flush is skipped and the memory is not freed in time.
2. A very unlikely UAF: the reader loads pcs->main, and the old sheaf
   is freed before ->size is dereferenced.

For the fix: could we simply drop the has_pcs_used() check?  The cost
is some extra workqueue overhead — flush work would be queued even on
CPUs whose sheaves are empty.

--
Best regards,
Qing

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

* Re: mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN  blind spot: slub.o is uninstrumented)
  2026-11-04  9:41     ` Qing Wang
@ 2026-11-04 10:07       ` Qing Wang
  0 siblings, 0 replies; 21+ messages in thread
From: Qing Wang @ 2026-11-04 10:07 UTC (permalink / raw)
  To: xuhx, Vlastimil Babka, Harry Yoo, Andrew Morton, Hao Li,
	Christoph Lameter, David Rientjes, Roman Gushchin
  Cc: linux-mm, linux-kernel

On Wed, 04 Nov 2026 at 17:41, Qing Wang <wangqing7171@gmail.com> wrote:
> You're right, there is a data race in has_pcs_used() reading the pcs
> fields.  The possible consequences:
> 
> 1. has_pcs_used() observes an empty pcs and sets sfw->skip, so the
>    flush is skipped and the memory is not freed in time.

Sorry, I didn't state point 1 correctly.  It should be:

has_pcs_used() reads main->size as 0 before the concurrent free
increments it, so sfw->skip is wrongly set and the memory is not
reclaimed in time.

--
Best regards,
Qing

^ permalink raw reply	[flat|nested] 21+ 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
                     ` (5 preceding siblings ...)
  2026-11-03 12:10   ` gpio: gpiolib: final put of the gpio_device happens inside the SRCU read critical section on the setup-failure path -- deterministic use-after-free read at guard release (unfixed upstream) 徐晗翔
@ 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)
  11 siblings, 1 reply; 21+ 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] 21+ 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
                     ` (6 preceding siblings ...)
  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)
  11 siblings, 1 reply; 21+ 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] 21+ 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
                     ` (7 preceding siblings ...)
  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)
  11 siblings, 0 replies; 21+ 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] 21+ 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
                     ` (8 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) 徐晗翔
  11 siblings, 0 replies; 21+ 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] 21+ 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
                     ` (9 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) 徐晗翔
  11 siblings, 0 replies; 21+ 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] 21+ 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
                     ` (10 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   ` 徐晗翔
  11 siblings, 0 replies; 21+ 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] 21+ 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; 21+ 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] 21+ 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; 21+ 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] 21+ messages in thread

* Re: net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3
  2026-11-03 11:57   ` net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3 徐晗翔
  2026-11-03 15:49     ` Eric Dumazet
@ 2026-11-04 15:03     ` Sasha Levin
  1 sibling, 0 replies; 21+ messages in thread
From: Sasha Levin @ 2026-11-04 15:03 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni
  Cc: Sasha Levin, netdev, linux-kernel, stable, 徐晗翔

On 2026-11-03, 徐晗翔 <xuhx@hust.edu.cn> wrote:
> Upstream already fixed this with a synchronize_rcu() in rtnl_link_unregister()

Queued for 7.3 and 6.18, thanks.

-- 
Thanks,
Sasha

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

end of thread, other threads:[~2026-11-04 15:03 UTC | newest]

Thread overview: 21+ 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-03 11:21   ` mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN blind spot: slub.o is uninstrumented) 徐晗翔
2026-11-04  9:41     ` Qing Wang
2026-11-04 10:07       ` Qing Wang
2026-11-03 11:30   ` futex/hrtimer: hrtimer_sleeper::task is read unsynchronized in 16 places on v7.3 -- the accessor series that fixes this is not in 7.3; please make sure it reaches the affected trees (backport nudge + stock-kernel KCSAN evidence) 徐晗翔
2026-11-03 11:48   ` cgroup: fork vs cgroup.procs write race in favordynmods mode -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the stable trees (repro incl. uninstrumented natural hit) 徐晗翔
2026-11-03 14:51     ` Sasha Levin
2026-11-03 11:57   ` net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3 徐晗翔
2026-11-03 15:49     ` Eric Dumazet
2026-11-04 15:03     ` Sasha Levin
2026-11-03 12:07   ` wifi: wilc1000: SDIO/SPI remove destroys the srcu_struct before wiphy_unregister() -- late cfg80211 callbacks write through dangling srcu state (unfixed upstream; controlled + natural Oops) 徐晗翔
2026-11-03 12:10   ` gpio: gpiolib: final put of the gpio_device happens inside the SRCU read critical section on the setup-failure path -- deterministic use-after-free read at guard release (unfixed upstream) 徐晗翔
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

all inboxes | Powered by JetHome®