From: sashiko-bot@kernel.org
To: "Alexandre Ghiti" <alex@ghiti.fr>,
"Andrew Morton" <akpm@linux-foundation.org>
Cc: "Kairui Song" <kasong@tencent.com>,
"Barry Song" <baohua@kernel.org>, "Chris Li" <chrisl@kernel.org>,
"Kemeng Shi" <shikemeng@huaweicloud.com>,
"Nhat Pham" <nphamcs@gmail.com>,
"Baoquan He" <baoquan.he@linux.dev>,
"Youngjun Park" <youngjun.park@lge.com>,
"Johannes Weiner" <hannes@cmpxchg.org>,
"Hugh Dickins" <hughd@google.com>,
"Baolin Wang" <baolin.wang@linux.alibaba.com>,
"David Hildenbrand" <david@kernel.org>,
"Michal Hocko" <mhocko@kernel.org>,
"Qi Zheng" <qi.zheng@linux.dev>,
"Shakeel Butt" <shakeel.butt@linux.dev>,
"Lorenzo Stoakes" <ljs@kernel.org>,
"Axel Rasmussen" <axelrasmussen@google.com>,
"Yuanchu Xie" <yuanchu@google.com>, "Wei Xu" <weixugc@google.com>,
linux-mm@kvack.org
Subject: Re: [RFC PATCH 1/1] mm/swap: allocate contiguous swap slots for a reclaim batch
Date: Fri, 23 Oct 2026 14:57:44 +0000 [thread overview]
Message-ID: <sashiko-outbox-182881@kernel.org> (raw)
In-Reply-To: <20261022145615.2184343-2-alex@ghiti.fr>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] mm/swap: stack use-after-return on skipped list in alloc_swap_scan_list_contig()
- [Medium] mm/swap: folio_alloc_swap_split() skips cursor check and order accounting
- [Medium] mm/swap: swap_alloc_prepare() and swap_alloc_fast() strand usable clusters on frag_clusters
--
commit 0160f7d5d1e92861c5a388fc552e5871f0f57f93
Author: Alexandre Ghiti <alex@ghiti.fr>
mm/swap: allocate contiguous swap slots for a reclaim batch
This commit records the number of pages per small order in a reclaim batch
before swapping them out and updates the swap cluster allocator to seek
contiguous runs of up to SWAP_CLUSTER_MAX free slots in nonfull or free
clusters.
> diff --git a/mm/swapfile.c b/mm/swapfile.c
> index c3288910b3e35..67161e1849052 100644
> --- a/mm/swapfile.c
> +++ b/mm/swapfile.c
[ ... ]
> @@ -1042,6 +1079,59 @@ static unsigned int alloc_swap_scan_list(struct swap_info_struct *si,
> return found;
> }
>
> +/* Larger folios already make large enough write requests on their own */
> +#define SWAP_BATCH_MAX_ORDER (ilog2(SWAP_CLUSTER_MAX) - 1)
> +
> +static inline unsigned int swap_batch_contig(unsigned int left)
> +{
> + return min_t(unsigned int, left, SWAP_CLUSTER_MAX);
> +}
> +
> +/*
> + * Like alloc_swap_scan_list(), but only allocate at the start of @nr_contig
> + * free slots. Order 0 clusters without them move to the frag list until a
> + * slot is freed, larger orders stay nonfull as the folio may still fit.
> + */
> +static unsigned int alloc_swap_scan_list_contig(struct swap_info_struct *si,
> + struct list_head *list,
> + struct folio *folio,
> + unsigned int nr_contig)
> +{
> + unsigned int found = SWAP_ENTRY_INVALID, order = folio_order(folio);
> + struct swap_cluster_info *ci;
> + unsigned long start, offset;
> + LIST_HEAD(skipped);
> +
> + while ((ci = isolate_lock_cluster(si, list))) {
> + start = cluster_offset(si, ci);
> + offset = SWAP_ENTRY_INVALID;
> + if (SWAPFILE_CLUSTER - ci->count >= nr_contig)
> + offset = cluster_find_free_contig(ci, start,
> + start + SWAPFILE_CLUSTER,
> + 1 << order, nr_contig);
> + if (offset == SWAP_ENTRY_INVALID) {
> + if (order)
> + move_cluster(si, ci, &skipped,
> + CLUSTER_FLAG_NONFULL);
> + else
> + relocate_cluster(si, ci);
> + swap_cluster_unlock(ci);
> + continue;
> + }
> + found = alloc_swap_scan_cluster(si, ci, folio, offset);
> + if (found)
> + break;
> + }
> +
> + if (!list_empty(&skipped)) {
> + spin_lock(&si->lock);
> + list_splice_tail(&skipped, list);
> + spin_unlock(&si->lock);
> + }
> +
> + return found;
> +}
[Severity: High]
Can moving an order > 0 cluster onto the stack-local skipped list with
CLUSTER_FLAG_NONFULL and then dropping ci->lock cause a stack
use-after-return on skipped?
When shrink_folio_list() reclaims a batch of mTHP folios (order > 0, up to
SWAP_BATCH_MAX_ORDER), folio_alloc_swap() -> swap_alloc_slow() ->
cluster_alloc_swap_entry() -> alloc_swap_scan_list_contig() places any
cluster from si->nonfull_clusters[order] lacking nr_contig contiguous free
slots onto skipped with ci->flags set to CLUSTER_FLAG_NONFULL, and then
unlocks ci->lock.
While ci is unlocked on skipped, another CPU can lock ci by offset (for
example, when freeing its remaining entries via
__swap_cluster_free_entries() -> free_cluster(), or through a cached
percpu_swap_cluster / global_cluster cursor in swap_alloc_fast() /
cluster_alloc_swap_entry() -> alloc_swap_scan_cluster() ->
relocate_cluster()). Because ci->flags != CLUSTER_FLAG_NONE, move_cluster()
removes ci from skipped under si->lock:
mm/swapfile.c:move_cluster() {
spin_lock(&si->lock);
if (ci->flags == CLUSTER_FLAG_NONE)
list_add_tail(&ci->list, list);
else
list_move_tail(&ci->list, list);
spin_unlock(&si->lock);
ci->flags = new_flags;
}
When ci is the last entry on skipped, __list_del(&skipped, &skipped) in
include/linux/list.h updates skipped.prev and skipped.next without a
memory barrier:
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
WRITE_ONCE(prev->next, next);
}
Because alloc_swap_scan_list_contig() tests !list_empty(&skipped)
(READ_ONCE(skipped.next) == &skipped) outside si->lock before returning,
can it observe skipped.next == &skipped, skip spin_lock(&si->lock), and
return while the other CPU is still writing skipped.prev on the popped
stack frame?
Also, while those order > 0 clusters sit on the local skipped list, they
are hidden from concurrent alloc_swap_scan_list() callers scanning
si->nonfull_clusters[order], which could cause premature -E2BIG splits.
[ ... ]
> @@ -1390,7 +1503,15 @@ static bool swap_alloc_fast(struct folio *folio)
> if (cluster_is_usable(ci, order)) {
> if (cluster_is_empty(ci))
> offset = cluster_offset(si, ci);
> - alloc_swap_scan_cluster(si, ci, folio, offset);
> + /* The batch's contiguous slots are used up */
> + if (this_cpu_read(percpu_swap_cluster.batch_left[order]) &&
> + !cluster_slots_free(ci, offset, 1 << order)) {
> + swap_cluster_unlock(ci);
> + this_cpu_write(percpu_swap_cluster.offset[order],
> + SWAP_ENTRY_INVALID);
> + } else {
> + alloc_swap_scan_cluster(si, ci, folio, offset);
> + }
> } else {
> swap_cluster_unlock(ci);
> }
> @@ -1729,6 +1850,86 @@ static int swap_dup_entries_cluster(struct swap_info_struct *si,
> return err;
> }
>
> +static void swap_alloc_prepare(unsigned int order, unsigned int nr)
> +{
> + unsigned int nr_contig = swap_batch_contig(nr);
> + struct swap_cluster_info *ci;
> + struct swap_info_struct *si;
> + unsigned long offset;
> + bool keep = false;
> +
> + local_lock(&percpu_swap_cluster.lock);
> + if (nr <= (1 << order)) {
> + this_cpu_write(percpu_swap_cluster.batch_left[order], 0);
> + goto out;
> + }
> + this_cpu_write(percpu_swap_cluster.batch_left[order], nr);
> +
> + /* Keep the cursor if enough free slots follow it */
> + si = this_cpu_read(percpu_swap_cluster.si[order]);
> + offset = this_cpu_read(percpu_swap_cluster.offset[order]);
> + if (si && offset && get_swap_device_info(si)) {
> + ci = swap_cluster_lock(si, offset);
> + if (cluster_is_usable(ci, order)) {
> + if (cluster_is_empty(ci))
> + offset = cluster_offset(si, ci);
> + keep = offset % SWAPFILE_CLUSTER + nr_contig <=
> + SWAPFILE_CLUSTER &&
> + cluster_find_free_contig(ci, offset,
> + offset + nr_contig,
> + 1 << order,
> + nr_contig) == offset;
> + }
> + swap_cluster_unlock(ci);
> + put_swap_device(si);
> + }
> + if (!keep)
> + this_cpu_write(percpu_swap_cluster.offset[order],
> + SWAP_ENTRY_INVALID);
> +out:
> + local_unlock(&percpu_swap_cluster.lock);
> +}
[Severity: Medium]
When swap_alloc_prepare() or swap_alloc_fast() clears
percpu_swap_cluster.offset[order] to SWAP_ENTRY_INVALID without calling
alloc_swap_scan_cluster(), does that strand usable clusters on
si->frag_clusters[order]?
On the first allocation from a cluster, alloc_swap_scan_cluster() calls
relocate_cluster(), which moves any partially populated cluster
(0 < ci->count < SWAPFILE_CLUSTER) to si->frag_clusters[ci->order]:
mm/swapfile.c:relocate_cluster() {
...
} else if (ci->count != SWAPFILE_CLUSTER) {
if (ci->flags != CLUSTER_FLAG_FRAG)
move_cluster(si, ci, &si->frag_clusters[ci->order],
CLUSTER_FLAG_FRAG);
}
}
Previously, the per-CPU cursor kept allocating from that cluster until the
end of the cluster was reached. With this change, if swap_alloc_prepare()
finds fewer than nr_contig free slots immediately at offset, or if
swap_alloc_fast() (or cluster_alloc_swap_entry() for rotating storage)
encounters an occupied slot at offset while batch_left[order] is non-zero,
the cursor is abandoned while ci remains on si->frag_clusters[order], even
if large contiguous free regions remain later in the same cluster.
Since si->frag_clusters[order] is only scanned after si->nonfull_clusters
and si->free_clusters are exhausted (and only one cluster at a time), could
abandoning the cursor without returning ci to si->nonfull_clusters[order]
or scanning the rest of ci cause premature cluster exhaustion and -E2BIG
mTHP swapout fallbacks?
> +
> +/**
> + * folio_alloc_swap_prepare - prepare swap allocation for a reclaim batch
> + * @folios: list of folios reclaim is about to swap out
> + *
> + * Record how many pages of each small order the batch will swap out, so
> + * that their slots are allocated contiguously. Nothing is reserved.
> + */
> +void folio_alloc_swap_prepare(struct list_head *folios)
> +{
> + unsigned int max_order = min(SWAP_BATCH_MAX_ORDER, SWAP_NR_ORDERS - 1);
> + unsigned int nr[SWAP_BATCH_MAX_ORDER + 1] = {};
> + struct folio *folio;
> + unsigned int order;
> +
> + if (!total_swap_pages)
> + return;
> +
> + list_for_each_entry(folio, folios, lru) {
> + if (!folio_test_swapbacked(folio) || folio_test_swapcache(folio))
> + continue;
> + order = folio_order(folio);
> + if (order <= max_order)
> + nr[order] += 1 << order;
> + }
> +
> + for (order = 0; order <= max_order; order++)
> + swap_alloc_prepare(order, nr[order]);
> +}
> +
> +/**
> + * folio_alloc_swap_split - account for a large folio split by reclaim
> + * @nr_pages: number of order-0 folios the split produced
> + */
> +void folio_alloc_swap_split(unsigned int nr_pages)
> +{
> + local_lock(&percpu_swap_cluster.lock);
> + this_cpu_add(percpu_swap_cluster.batch_left[0], nr_pages);
> + local_unlock(&percpu_swap_cluster.lock);
> +}
[Severity: Medium]
Should folio_alloc_swap_split() also validate percpu_swap_cluster.offset[0]
for contiguous free slots and decrement batch_left[order] for the split
folio's original order?
When folio_alloc_swap_prepare() sees 0 or 1 order-0 folios in the batch,
swap_alloc_prepare(0, nr[0]) sets batch_left[0] to 0 and leaves the
existing percpu_swap_cluster.offset[0] cursor untouched. If a large folio
is subsequently split in shrink_folio_list() or shmem_writeout(),
folio_alloc_swap_split() adds nr_pages to batch_left[0] without checking
whether percpu_swap_cluster.offset[0] has swap_batch_contig(batch_left[0])
contiguous free slots following it.
Because swap_alloc_fast() only checks cluster_slots_free(ci, offset, 1) for
order-0 allocations, if percpu_swap_cluster.offset[0] points into a small
free hole, the first subpages of the split folio will be allocated into
that hole until an occupied slot is reached.
In addition, if the split folio had order <= max_order,
folio_alloc_swap_prepare() already added 1 << order to batch_left[order].
When that folio is split before folio_alloc_swap() (such as a partially
mapped folio in shrink_folio_list() or a folio in shmem_writeout()) or
after folio_alloc_swap() fails without clearing batch_left[order],
batch_left[order] is not decremented, leaving it inflated for subsequent
folios of that order.
титься
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261022145615.2184343-1-alex@ghiti.fr?part=1
next prev parent reply other threads:[~2026-10-23 14:57 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 14:54 [RFC PATCH 0/1] mm/swap: keep reclaim batches contiguous on swap Alexandre Ghiti
2026-10-22 14:54 ` [RFC PATCH 1/1] mm/swap: allocate contiguous swap slots for a reclaim batch Alexandre Ghiti
2026-10-23 14:57 ` sashiko-bot [this message]
2026-10-23 16:05 ` Nhat Pham
2026-10-22 15:27 ` [RFC PATCH 0/1] mm/swap: keep reclaim batches contiguous on swap Kairui Song
2026-10-22 16:17 ` Alexandre Ghiti
2026-10-26 13:38 ` Kairui Song
2026-10-22 19:57 ` Nhat Pham
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-182881@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=akpm@linux-foundation.org \
--cc=alex@ghiti.fr \
--cc=axelrasmussen@google.com \
--cc=baohua@kernel.org \
--cc=baolin.wang@linux.alibaba.com \
--cc=baoquan.he@linux.dev \
--cc=chrisl@kernel.org \
--cc=david@kernel.org \
--cc=hannes@cmpxchg.org \
--cc=hughd@google.com \
--cc=kasong@tencent.com \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=mhocko@kernel.org \
--cc=nphamcs@gmail.com \
--cc=qi.zheng@linux.dev \
--cc=sashiko-reviews@lists.linux.dev \
--cc=shakeel.butt@linux.dev \
--cc=shikemeng@huaweicloud.com \
--cc=weixugc@google.com \
--cc=youngjun.park@lge.com \
--cc=yuanchu@google.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