Re: mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN blind spot: slub.o is uninstrumented)

From: Qing Wang

Date: Wed Nov 04 2026 - 04:41:45 EST


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

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

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

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

--
Best regards,
Qing