Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Junseo Lim" <zirajs7@gmail.com>,
	vbabka@kernel.org, harry@kernel.org, akpm@linux-foundation.org
Cc: hao.li@linux.dev, linux-mm@kvack.org
Subject: Re: [RFC PATCH] mm/slub: barn sheaf counts can exceed their limit
Date: Thu, 12 Nov 2026 14:23:59 +0000	[thread overview]
Message-ID: <sashiko-outbox-211310@kernel.org> (raw)
In-Reply-To: <20261111142337.748120-1-zirajs7@gmail.com>

Thank you for your contribution! Sashiko AI review found 5 potential
issues to consider:
- [High] mm/slub: missing Signed-off-by trailer and RFC email body in
  commit message
- [Medium] mm/slub: unbounded barn sheaf growth across refill waves in
  __pcs_replace_empty_main()
- [Low] mm/slub: off-by-one and unlocked check on barn->nr_alloc in
  __pcs_replace_empty_main()
- [Low] mm/slub: redundant back-to-back barn->lock acquisitions in
  __pcs_replace_empty_main()
- [Low] mm/slub: missing Fixes trailer for commit 2d517aa09bbc in commit
  message
--

commit 4002c32d0ca36a2bf13b1261531aaa78aeae34dc
Author: Junseo Lim <zirajs7@gmail.com>

> mm/slub: barn sheaf counts can exceed their limit
>
> Hello,
>
> Following the suggestion to discuss this publicly, I would like to
> report a repeatable overshoot of the SLUB barn limits. Although
> MAX_FULL_SHEAVES and MAX_EMPTY_SHEAVES are both 10, a userspace workload
> can repeatedly trigger the refill recovery path. In one run, I observed
> states like nr_full=91.
>
> I would appreciate feedback on the appropriate way to enforce these
> limits.
>
> Description
> -----------
>
> While the sheaf layer provides a simple array structure for
> amortization, the edge cases that involve refills and allocating
> structures are inherently complex when combined with multiple threads.
> One problematic case occurs in the allocation path. Consider a state
> where every object is drained and TH1 on CPU1 is running:
>
>   CPU1
>   =======================================================================
>   TH1                    TH2                    TH3
>   ---------------------  ---------------------  --------------------- ...
>   pcs->main->size=0      not entered            not entered
>   pcs->spare->size=0
>   nr_full=0 nr_empty=10
>
>     <observed states>      <observed states>      <observed states>
>
> If an allocation request hits this state, __pcs_replace_empty_main is
> called to retrieve a full sheaf and swap it with main. However, no full
> sheaf is available in the barn, so TH1 removes pcs->spare, sets it to
> NULL, and refills the removed sheaf through the slow path. Here, the
> per-CPU sheaf lock is released during refill and reacquired afterward.
> If preemption occurs during this period, a subsequent thread (TH2) sees
> pcs->spare=NULL and tries to get a remaining sheaf from the barn's empty
> list and refill it. Likewise, this part is not protected by the lock.
> Chaining this results in a state where new threads keep draining the
> sheaf and ultimately consume all the empty sheaves in the barn and start
> to allocate new sheaves.
>
>   CPU1
>   =======================================================================
>   TH1 (preempted)        TH2 (preempted)        TH3
>   ---------------------  ---------------------  --------------------- ...
>   pcs->main->size=0      pcs->main->size=0      pcs->main->size=0
>   pcs->spare->size=0     pcs->spare=NULL        pcs->spare=NULL
>   nr_full=0 nr_empty=10  nr_full=0 nr_empty=10  nr_full=0 nr_empty=9
>
>     <observed states>      <observed states>      <observed states>
>
> The construction of the sheaf layer allows such racy scenarios, as
> mentioned in the comments. Nevertheless, this overshoot is much larger
> than what we would expect, and the fact that this state can be recreated
> with moderate effort implies that a fix would be appropriate.
>
> Impact
> ------
>
> This itself does not pose a direct threat or system failure. But, in a
> security sense, this behavior can be leveraged for heap grooming that
> targets the underlying slab_sheaf.
>
> Design considerations and Question
> ----------------------------------
>
> One way to limit barn overshoot is to constrain the racy allocations.
> Simply using the current nr_full and nr_empty values does not help in
> detecting excessive allocation. What I came up with is adding an
> nr_alloc counter that keeps track of allocations during the unlocked
> path. From what I've tested, this restricted the targeted workload that
> induces the excessive allocation. Before this patch, nr_full reached 60
> to 90. After the patch, it remained at 22 under the adversarial
> workload.
>
> I haven't checked every possible interleaving yet, but I would like to
> know whether you think this is the right direction. The possible
> regressions are that it requires an additional field just to track the
> adversarial input and that, in a highly contended scenario, every
> allocation after hitting the nr_alloc limit will go to the slow path
> until the preempted task returns and continues the allocation.
>
> Is tracking in-flight sheaf allocations per barn, and falling back when
> too many are in progress, the right way to limit this refill race?
>
> Note: This is only an implementation on the allocation side and is still
> a prototype.

[Severity: High]
This commit is missing a Signed-off-by: trailer, and the commit message is
formatted as a mailing-list question rather than a changelog entry (any
discussion or prototype notes belong below the --- line).

[Severity: Low]
Should this commit include a Fixes: tag for the commit that introduced
per-CPU sheaves and __pcs_replace_empty_main()?

  Fixes: 2d517aa09bbc ("slab: add opt-in caching layer of percpu sheaves")

> diff --git a/mm/slub.c b/mm/slub.c
> index a151268b97263..7076902509ed3 100644
> --- a/mm/slub.c
> +++ b/mm/slub.c

[ ... ]

> @@ -4798,6 +4802,19 @@ __pcs_replace_empty_main(struct kmem_cache *s, struct slub_percpu_sheaves *pcs,
>  		}
>  	}
>  
> +	if (allow_spin && !empty) {
> +		/* The limit is advisory; concurrent reservations may exceed it. */
> +		if (data_race(barn->nr_alloc) > MAX_EMPTY_SHEAVES) {
> +			local_unlock(&s->cpu_sheaves->lock);
> +			return NULL;
> +		}
> +
> +		spin_lock_irqsave(&barn->lock, flags);
> +		barn->nr_alloc++;
> +		spin_unlock_irqrestore(&barn->lock, flags);
> +		alloc_new = true;
> +	}

[Severity: Low]
Should this check be >= MAX_EMPTY_SHEAVES, and rechecked after acquiring
barn->lock?  Starting from barn->nr_alloc == 0, checking > MAX_EMPTY_SHEAVES
allows MAX_EMPTY_SHEAVES + 1 (11) concurrent allocations rather than
MAX_EMPTY_SHEAVES (10), and threads on other CPUs that pass the unlocked
check before taking barn->lock will still increment barn->nr_alloc past the
limit.

[ ... ]

> @@ -4841,24 +4858,31 @@ __pcs_replace_empty_main(struct kmem_cache *s, struct slub_percpu_sheaves *pcs,
>  		else
>  			barn_put_empty_sheaf(barn, pcs->main);
>  		pcs->main = full;
> -		return pcs;
> +		goto out;
>  	}
>  
>  	if (!pcs->spare) {
>  		pcs->spare = full;
> -		return pcs;
> +		goto out;
>  	}
>  
>  	if (pcs->spare->size == 0) {
>  		barn_put_empty_sheaf(barn, pcs->spare);
>  		pcs->spare = full;
> -		return pcs;
> +		goto out;
>  	}
>  
>  barn_put:
>  	barn_put_full_sheaf(barn, full);
>  	stat(s, BARN_PUT);
>  
> +out:
> +	if (alloc_new) {
> +		spin_lock_irqsave(&barn->lock, flags);
> +		barn->nr_alloc--;
> +		spin_unlock_irqrestore(&barn->lock, flags);
> +	}
> +
>  	return pcs;
>  }

[Severity: Medium]
Can barn->nr_full and barn->nr_empty still grow without bound across
repeated drain-and-refill waves?

barn->nr_alloc is only checked and incremented when !empty (after
pcs->spare and barn->sheaves_empty are already exhausted), and is
decremented back to 0 at out: as soon as __pcs_replace_empty_main()
finishes.  When a wave of threads allocates new sheaves via
alloc_empty_sheaf(), any excess sheaves are placed into the barn by
barn_put_full_sheaf() or barn_put_empty_sheaf() without checking
MAX_FULL_SHEAVES or MAX_EMPTY_SHEAVES, while barn->nr_alloc returns to 0.
As subsequent allocations consume those full sheaves via
barn_replace_empty_sheaf(), they are swapped onto barn->sheaves_empty
without checking MAX_EMPTY_SHEAVES:

mm/slub.c:barn_replace_empty_sheaf() {
    ...
	if (likely(barn->nr_full)) {
		full = list_first_entry(&barn->sheaves_full, struct slab_sheaf,
					barn_list);
		list_del(&full->barn_list);
		list_add(&empty->barn_list, &barn->sheaves_empty);
		barn->nr_full--;
		barn->nr_empty++;
	}
    ...
}

In the next preemption wave, threads pull all of those accumulated empty
sheaves via barn_get_empty_sheaf() with empty != NULL (leaving
barn->nr_alloc at 0 while preempted in refill_sheaf()), and once
barn->nr_empty hits 0 again, another wave sees empty == NULL and
barn->nr_alloc == 0 and allocates another batch of MAX_EMPTY_SHEAVES + 1
sheaves.  Meanwhile, on a NUMA node with more than 11 CPUs warming up a
cold cache, capping per-node in-flight allocations at MAX_EMPTY_SHEAVES
forces concurrent allocations on other CPUs onto the ___slab_alloc()
slowpath even when no preemption race occurs.  Enforcing MAX_FULL_SHEAVES
and MAX_EMPTY_SHEAVES when returning excess sheaves to the barn (and
flushing and freeing any excess via sheaf_flush_unused() and
free_empty_sheaf(), as rcu_free_sheaf() and kmem_cache_return_sheaf() do)
would bound the barn directly without throttling cold-cache allocations.

[Severity: Low]
Does this acquire and release barn->lock back-to-back on the recovery
paths?  When alloc_new is true and __pcs_replace_empty_main() calls
barn_put_empty_sheaf() or barn_put_full_sheaf(), those helpers take and
drop barn->lock, and the out: label immediately takes and drops barn->lock
again under s->cpu_sheaves->lock just to decrement barn->nr_alloc (which is
only ever read via data_race()).

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261111142337.748120-1-zirajs7@gmail.com?part=1


      reply	other threads:[~2026-11-12 14:24 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-11 14:23 [RFC PATCH] mm/slub: barn sheaf counts can exceed their limit Junseo Lim
2026-11-12 14:23 ` sashiko-bot [this message]

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=sashiko-outbox-211310@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=akpm@linux-foundation.org \
    --cc=hao.li@linux.dev \
    --cc=harry@kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=vbabka@kernel.org \
    --cc=zirajs7@gmail.com \
    /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