* 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; 10+ 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] 10+ 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; 10+ 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] 10+ 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; 10+ 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] 10+ 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; 10+ 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] 10+ messages in thread
* net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3
2026-10-27 14:12 ` Greg KH
` (2 preceding siblings ...)
2026-11-03 11:48 ` cgroup: fork vs cgroup.procs write race in favordynmods mode -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the stable trees (repro incl. uninstrumented natural hit) 徐晗翔
@ 2026-11-03 11:57 ` 徐晗翔
2026-11-03 15:49 ` Eric Dumazet
2026-11-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, 1 reply; 10+ 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] 10+ messages in thread* Re: net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3
2026-11-03 11:57 ` net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3 徐晗翔
@ 2026-11-03 15:49 ` Eric Dumazet
0 siblings, 0 replies; 10+ messages in thread
From: Eric Dumazet @ 2026-11-03 15:49 UTC (permalink / raw)
To: 徐晗翔
Cc: davem, kuba, pabeni, netdev, linux-kernel, stable
Le mar. 3 nov. 2026 à 12:57, 徐晗翔 <xuhx@hust.edu.cn> a écrit :
>
> On v7.3, rtnl_link_unregister() removes the rtnl_link_ops from the
> global list and immediately destroys the embedded srcu_struct and
> frees the ops memory -- a concurrent RTM_GETLINK dump that already
> found the ops (rtnl_link_ops_get(), rtnetlink.c:599 hand-off inside
> an RCU read critical section at :595) but has not yet taken
> ops->srcu will, after the free, execute __srcu_read_lock() /
> __srcu_read_unlock() on freed memory: two reads of the destroyed
> object (ssp->srcu_ctrp, ssp->sda) whose subsequent percpu counter
> writes land wherever the freed percpu pointers point -- for a module
> rtnl_link_ops, the ops (and its srcu) live in module memory, so
> module_memfree() is the freeing side. Upstream already fixed this
> with a synchronize_rcu() in rtnl_link_unregister() (the patch with
> Fixes: 6b57ff21a310; its own comment: "Wait for rtnl_link_ops_get()
> readers that found ops but have not yet taken ops->srcu"). v7.3
> does not contain it. I verified both sides on a stock 7.3 KASAN
> kernel: parking a real dump reader (driven by an ordinary
> RTM_GETLINK + IFLA_LINKINFO dump request) exactly at the :599
> hand-off and unregistering+freeing underneath produces three KASAN
> slab-use-after-free reports (__srcu_read_lock+0xe, +0x25,
> __srcu_read_unlock+0x1a; bad address = ops+0x10 = ops->srcu); and
> rebuilding the SAME kernel with ONLY the upstream one-line fix
> flips the outcome to zero reports, with the kfree demonstrably
> delayed until the parked reader left its RCU section. Natural
> hammering did not hit (the hand-off window is ~100 ns against a
> ~4 ms unregister -- it needs a preempted/preemptible-RCU reader),
> which matches the fix's own reasoning; the controlled run models
> exactly that preemption. The purpose of this mail is to support
> the fix's propagation: trees still carrying the unfixed
> rtnl_link_unregister() keep a cross-CPU UAF (write) plus potential
> freed-module execution reachable by unprivileged netlink dumps
> racing a module unload.
>
>
> The detailed description:
>
> * The gap (v7.3, net/core/rtnetlink.c)
> rtnl_dump_ifinfo() runs lockless for dumps
> (RTNL_FLAG_DUMP_UNLOCKED), takes rcu_read_lock() at :595, calls
> linkinfo_to_kind_ops() -> rtnl_link_ops_get() which returns the
> ops pointer at :599, and only then takes ops->srcu
> (srcu_read_lock) to call ops->dumpit. rtnl_link_unregister()
> holds rtnl_mutex, removes the ops from the list, calls
> cleanup_srcu_struct(&ops->srcu) and returns to the module, whose
> exit then frees the module memory. Nothing between the :599
> hand-off and the srcu_read_lock is waited for on the unregister
> side in 7.3 -- hence the fix's synchronize_rcu().
>
> * Evidence (stock 7.3 KASAN, QEMU; A/B on the one-line fix;
> reproducers attached)
> rtnlrace.c registers a kzalloc'd rtnl_link_ops (kind "rtnlrace"),
> parks a dump reader by kprobe at the exact hand-off (return
> address rtnl_dump_ifinfo+0x374, ENDBR-adjusted), and unregisters
> + kfrees from another thread (modeling module_exit ->
> module_memfree). nlr (userspace) sends the IFLA_LINKINFO dump
> request that drives the reader path.
> Defect kernel (stock 7.3):
> park t=127041 (1004 ms) -> unregister+kfree at t=127249 (park
> +208 ms, reader still parked 792 ms) -> park ends t=128045:
> BUG: KASAN: slab-use-after-free in __srcu_read_lock+0xe
> BUG: KASAN: slab-use-after-free in __srcu_read_lock+0x25
> BUG: KASAN: slab-use-after-free in __srcu_read_unlock+0x1a
> read stack: __srcu_read_lock <- rtnl_dump_ifinfo+0x374 <-
> rtnl_dumpit <- netlink_dump <- rtnetlink_rcv_msg (task nlr)
> bad address = object+0x10 == ops->srcu (matches the
> disassembly lea 0x10(%rbx))
> alloc: insmod kzalloc; free: unregister thread kfree
> Fixed kernel (same sources, ONLY the upstream synchronize_rcu()
> added to rtnl_link_unregister()):
> park t=191305 -> park ENDS t=192306 -> unregister+kfree
> t=192307 -- the order literally inverts -- zero KASAN reports,
> dump completes with zero errors. Same module, same parking.
> Natural runs (three samplings, up to ~267k dumps/reader plus
> 6-CPU preemption spinners): zero hits; the window arithmetic
> (~100 ns hand-off vs ~4 ms unregister) explains it, and the
> controlled park models precisely the preemption the upstream
> fix's comment describes.
>
What is the point of this email ? This looks like LLM slope to me.
I think we have far more serious bugs to fix.
3b70edea393d was tagged stable+noautosel on purpose: the race needs
a module unload (CAP_SYS_MODULE in init_user_ns) to line up with a
~100ns window, which your report also shows you could not hit
without a kprobe. It is not reachable by an unprivileged user alone.
^ permalink raw reply [flat|nested] 10+ 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; 10+ 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] 10+ 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; 10+ 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] 10+ messages in thread