mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: 徐晗翔 <xuhx@hust.edu.cn>
To: ajay.kathat@microchip.com, claudiu.beznea@tuxon.dev
Cc: linux-wireless@vger.kernel.org, linux-kernel@vger.kernel.org,
	netdev@vger.kernel.org
Subject: 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)
Date: Tue, 3 Nov 2026 20:07:24 +0800 (GMT+08:00)	[thread overview]
Message-ID: <5533f511.1d687.1a1a14e94bc.Coremail.xuhx@hust.edu.cn> (raw)
In-Reply-To: <2026102727-data-sequel-ce41@gregkh>

[-- 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");

  parent reply	other threads:[~2026-11-03 12:07 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 15:49     ` Eric Dumazet
2026-11-03 12:07   ` 徐晗翔 [this message]
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) 徐晗翔

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=5533f511.1d687.1a1a14e94bc.Coremail.xuhx@hust.edu.cn \
    --to=xuhx@hust.edu.cn \
    --cc=ajay.kathat@microchip.com \
    --cc=claudiu.beznea@tuxon.dev \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-wireless@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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®