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");
next prev 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®