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; 9+ 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] 9+ 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) 徐晗翔
                     ` (5 more replies)
  0 siblings, 6 replies; 9+ 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] 9+ 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-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) 徐晗翔
                     ` (4 subsequent siblings)
  5 siblings, 0 replies; 9+ 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] 9+ 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) 徐晗翔
                     ` (3 subsequent siblings)
  5 siblings, 0 replies; 9+ 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] 9+ 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 徐晗翔
                     ` (2 subsequent siblings)
  5 siblings, 1 reply; 9+ 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] 9+ 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 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) 徐晗翔
  5 siblings, 0 replies; 9+ 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] 9+ 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) 徐晗翔
  5 siblings, 0 replies; 9+ 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] 9+ 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   ` 徐晗翔
  5 siblings, 0 replies; 9+ 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] 9+ 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; 9+ 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] 9+ messages in thread

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

Thread overview: 9+ 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-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 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) 徐晗翔

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®