* [PATCH v2 0/3] mm: fix swapcache writeout and cluster table races
@ 2026-11-10 15:14 Nhat Pham
2026-11-10 15:14 ` [PATCH v2 1/3] mm: vmscan: allow writeout of swapcache folios without anon mappings Nhat Pham
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Nhat Pham @ 2026-11-10 15:14 UTC (permalink / raw)
To: linux-mm
Cc: akpm, hannes, david, mhocko, qi.zheng, shakeel.butt, ljs, kasong,
baohua, axelrasmussen, yuanchu, weixugc, baoquan.he, baolin.wang,
willy, brauner, linux-kernel, chrisl, shikemeng, nphamcs,
youngjun.park, ryncsn, sashiko-bot, stable
Reviews of vswap and v1 of this series found bugs in existing swap code.
Allow swapcache writeout without anon mappings, publish cluster tables
with the allocator locks held, and leave tableless clusters to the
allocator.
Changes in v2:
- Rebase onto the current mm-unstable tree.
- Use Suggested-by for Kairui in patch 2, as he requested.
- Add patch 3 to prevent stale tableless-cluster relocation, addressing
both scenarios in Sashiko's review.
v1: https://lore.kernel.org/all/20261106160432.2552507-1-nphamcs@gmail.com/
Nhat Pham (3):
mm: vmscan: allow writeout of swapcache folios without anon mappings
mm/swap: publish cluster tables after reacquiring allocator locks
mm/swap: skip relocation of tableless clusters
mm/swapfile.c | 113 ++++++++++++++++++++++++++++++++++++--------------
mm/vmscan.c | 3 +-
2 files changed, 83 insertions(+), 33 deletions(-)
base-commit: b024a51b0184b1efb6f55b11aaf7b15a490a6307
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v2 1/3] mm: vmscan: allow writeout of swapcache folios without anon mappings
2026-11-10 15:14 [PATCH v2 0/3] mm: fix swapcache writeout and cluster table races Nhat Pham
@ 2026-11-10 15:14 ` Nhat Pham
2026-11-10 15:14 ` [PATCH v2 2/3] mm/swap: publish cluster tables after reacquiring allocator locks Nhat Pham
2026-11-10 15:14 ` [PATCH v2 3/3] mm/swap: skip relocation of tableless clusters Nhat Pham
2 siblings, 0 replies; 4+ messages in thread
From: Nhat Pham @ 2026-11-10 15:14 UTC (permalink / raw)
To: linux-mm
Cc: akpm, hannes, david, mhocko, qi.zheng, shakeel.butt, ljs, kasong,
baohua, axelrasmussen, yuanchu, weixugc, baoquan.he, baolin.wang,
willy, brauner, linux-kernel, chrisl, shikemeng, nphamcs,
youngjun.park, ryncsn, sashiko-bot, stable
Unfaulted zswap readahead folios can be dirty and in swapcache without
an anonymous mapping. pageout() rejects them before swap_writeout(),
so reclaim cannot write them out while their swap entries remain
referenced.
Allow swapcache folios through the filesystem folio check. They already
have swap entries and can use the existing swap writeout path. Anonymous
folios outside swapcache have no mapping and already return PAGE_KEEP.
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://lore.kernel.org/all/sashiko-outbox-181078@kernel.org/
Fixes: 84798514db50 ("mm: Remove swap_writepage() and shmem_writepage()")
Cc: stable@vger.kernel.org
Reviewed-by: Baolin Wang <baolin.wang@linux.alibaba.com>
Signed-off-by: Nhat Pham <nphamcs@gmail.com>
---
mm/vmscan.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 061fbd168d98..96201eae0ceb 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -607,7 +607,8 @@ static pageout_t pageout(struct swap_io_ctx *ctx, struct address_space *mapping,
*/
if (folio_ref_count(folio) != 1 + folio_nr_pages(folio) || !mapping)
return PAGE_KEEP;
- if (!shmem_mapping(mapping) && !folio_test_anon(folio))
+ /* Anonymous folios outside swapcache returned PAGE_KEEP above. */
+ if (!shmem_mapping(mapping) && !folio_test_swapcache(folio))
return PAGE_ACTIVATE;
if (!folio_clear_dirty_for_io(folio))
return PAGE_CLEAN;
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v2 2/3] mm/swap: publish cluster tables after reacquiring allocator locks
2026-11-10 15:14 [PATCH v2 0/3] mm: fix swapcache writeout and cluster table races Nhat Pham
2026-11-10 15:14 ` [PATCH v2 1/3] mm: vmscan: allow writeout of swapcache folios without anon mappings Nhat Pham
@ 2026-11-10 15:14 ` Nhat Pham
2026-11-10 15:14 ` [PATCH v2 3/3] mm/swap: skip relocation of tableless clusters Nhat Pham
2 siblings, 0 replies; 4+ messages in thread
From: Nhat Pham @ 2026-11-10 15:14 UTC (permalink / raw)
To: linux-mm
Cc: akpm, hannes, david, mhocko, qi.zheng, shakeel.butt, ljs, kasong,
baohua, axelrasmussen, yuanchu, weixugc, baoquan.he, baolin.wang,
willy, brauner, linux-kernel, chrisl, shikemeng, nphamcs,
youngjun.park, ryncsn, sashiko-bot, stable
Under memory pressure, swap_cluster_populate() can publish a table
while its locks are dropped for a sleeping allocation. A cached
allocator can consume and free that table before the populating
allocator resumes, leaving the resumed scan with a NULL table.
CPU0 CPU1
alloc_swap_scan_list()
isolate_lock_cluster()
swap_cluster_populate()
drop local/global/ci locks
swap_cluster_alloc_table()
rcu_assign_pointer(ci->table, table)
folio_alloc_swap()
swap_alloc_fast()
alloc_swap_scan_cluster()
__swap_cluster_alloc_entries()
mem_cgroup_try_charge_swap()
return -ENOMEM
swap_cache_del_folio()
__swap_cache_del_folio()
__swap_cache_do_del_folio()
__swap_cluster_free_entries()
free_cluster()
__free_cluster()
swap_cluster_free_table()
reacquire local/global/ci locks
return ci
alloc_swap_scan_cluster()
cluster_scan_range()
__swap_table_get() /* NULL table */
Keep the count table, cgroup table and zero bitmap private until
swap_cluster_alloc_table() restores all allocator locks. Install them
under the cluster lock and retain it through the scan. Initial bad-slot
setup takes only the cluster lock because the device is unpublished.
The allocator releases its locks before the sleeping allocation. A
reclaimer that dropped ci->lock earlier can then resume and put the
cluster back on the free list. Abandon installation if the list state
changes or a table appears, freeing only private allocations. Restore
locks on allocation failure too, so the caller can return a still-isolated
cluster to its free list.
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://lore.kernel.org/all/sashiko-outbox-181084@kernel.org/
Link: https://lore.kernel.org/all/sashiko-outbox-181086@kernel.org/
Suggested-by: Kairui Song <ryncsn@gmail.com>
Fixes: cdd77f84d966 ("mm, swap: consolidate cluster allocation helpers")
Cc: stable@vger.kernel.org
Signed-off-by: Nhat Pham <nphamcs@gmail.com>
---
mm/swapfile.c | 106 +++++++++++++++++++++++++++++++++++---------------
1 file changed, 74 insertions(+), 32 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 038d2efe26ad..32639f65f13c 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -449,15 +449,23 @@ static void swap_cluster_free_table(struct swap_cluster_info *ci)
swap_cluster_free_count_table(table);
}
-static int swap_cluster_alloc_table(struct swap_cluster_info *ci, gfp_t gfp)
+/*
+ * Return with ci locked. An unlocked allocator also needs si's outer locks;
+ * initial bad-slot setup passes NULL because the device is not published.
+ */
+static int swap_cluster_alloc_table(struct swap_info_struct *si,
+ struct swap_cluster_info *ci,
+ gfp_t gfp, bool locked)
{
struct swap_table *table = NULL;
+#ifdef CONFIG_MEMCG
+ struct swap_memcg_table *memcg_table = NULL;
+#endif
+#if !SWAP_TABLE_HAS_ZEROFLAG
+ unsigned long *zero_bitmap = NULL;
+#endif
struct folio *folio;
-
- /* The cluster must be empty and not on any list during allocation. */
- VM_WARN_ON_ONCE(ci->flags || !cluster_is_empty(ci));
- if (rcu_access_pointer(ci->table))
- return 0;
+ int ret = -ENOMEM;
if (SWP_TABLE_USE_PAGE) {
folio = folio_alloc(gfp | __GFP_ZERO, 0);
@@ -467,27 +475,49 @@ static int swap_cluster_alloc_table(struct swap_cluster_info *ci, gfp_t gfp)
table = kmem_cache_zalloc(swap_table_cachep, gfp);
}
if (!table)
- return -ENOMEM;
+ goto lock;
#ifdef CONFIG_MEMCG
if (!mem_cgroup_disabled()) {
- VM_WARN_ON_ONCE(ci->memcg_table);
- ci->memcg_table = kzalloc_obj(*ci->memcg_table, gfp);
- if (!ci->memcg_table) {
- swap_cluster_free_count_table(table);
- return -ENOMEM;
- }
+ memcg_table = kzalloc_obj(*ci->memcg_table, gfp);
+ if (!memcg_table)
+ goto lock;
}
#endif
#if !SWAP_TABLE_HAS_ZEROFLAG
- VM_WARN_ON_ONCE(ci->zero_bitmap);
- ci->zero_bitmap = bitmap_zalloc(SWAPFILE_CLUSTER, gfp);
- if (!ci->zero_bitmap) {
- swap_cluster_free_table(ci);
- swap_cluster_free_count_table(table);
- return -ENOMEM;
+ zero_bitmap = bitmap_zalloc(SWAPFILE_CLUSTER, gfp);
+ if (!zero_bitmap)
+ goto lock;
+#endif
+ ret = 0;
+
+lock:
+ if (!locked) {
+ if (si) {
+ local_lock(&percpu_swap_cluster.lock);
+ if (!(si->flags & SWP_SOLIDSTATE))
+ spin_lock(&si->global_cluster_lock);
+ }
+ spin_lock(&ci->lock);
}
+ lockdep_assert_held(&ci->lock);
+
+ /* A stale reclaimer may have returned the cluster to the free list. */
+ if (ci->flags != CLUSTER_FLAG_NONE || cluster_table_is_alloced(ci))
+ ret = -EAGAIN;
+ if (ret)
+ goto free;
+
+ /* The cluster must be empty and not on any list during allocation. */
+ VM_WARN_ON_ONCE(!cluster_is_empty(ci));
+#ifdef CONFIG_MEMCG
+ VM_WARN_ON_ONCE(ci->memcg_table);
+ ci->memcg_table = memcg_table;
+#endif
+#if !SWAP_TABLE_HAS_ZEROFLAG
+ VM_WARN_ON_ONCE(ci->zero_bitmap);
+ ci->zero_bitmap = zero_bitmap;
#endif
/*
@@ -496,6 +526,17 @@ static int swap_cluster_alloc_table(struct swap_cluster_info *ci, gfp_t gfp)
*/
rcu_assign_pointer(ci->table, table);
return 0;
+
+free:
+#ifdef CONFIG_MEMCG
+ kfree(memcg_table);
+#endif
+#if !SWAP_TABLE_HAS_ZEROFLAG
+ bitmap_free(zero_bitmap);
+#endif
+ if (table)
+ swap_cluster_free_count_table(table);
+ return ret;
}
/*
@@ -546,8 +587,8 @@ swap_cluster_populate(struct swap_info_struct *si,
lockdep_assert_held(&si->global_cluster_lock);
lockdep_assert_held(&ci->lock);
- if (!swap_cluster_alloc_table(ci, __GFP_HIGH | __GFP_NOMEMALLOC |
- __GFP_NOWARN))
+ if (!swap_cluster_alloc_table(si, ci, __GFP_HIGH | __GFP_NOMEMALLOC |
+ __GFP_NOWARN, true))
return ci;
/*
@@ -560,8 +601,8 @@ swap_cluster_populate(struct swap_info_struct *si,
spin_unlock(&si->global_cluster_lock);
local_unlock(&percpu_swap_cluster.lock);
- ret = swap_cluster_alloc_table(ci, __GFP_HIGH | __GFP_NOMEMALLOC |
- GFP_KERNEL);
+ ret = swap_cluster_alloc_table(si, ci, __GFP_HIGH | __GFP_NOMEMALLOC |
+ GFP_KERNEL, false);
/*
* Back to atomic context. We might have migrated to a new CPU with a
@@ -571,13 +612,9 @@ swap_cluster_populate(struct swap_info_struct *si,
* could happen with ignoring the percpu cluster is fragmentation,
* which is acceptable since this fallback and race is rare.
*/
- local_lock(&percpu_swap_cluster.lock);
- if (!(si->flags & SWP_SOLIDSTATE))
- spin_lock(&si->global_cluster_lock);
- spin_lock(&ci->lock);
-
if (ret) {
- move_cluster(si, ci, &si->free_clusters, CLUSTER_FLAG_FREE);
+ if (ret != -EAGAIN)
+ move_cluster(si, ci, &si->free_clusters, CLUSTER_FLAG_FREE);
spin_unlock(&ci->lock);
return NULL;
}
@@ -825,9 +862,13 @@ static int swap_cluster_setup_bad_slot(struct swap_info_struct *si,
ci = cluster_info + idx;
/* Need to allocate swap table first for initial bad slot marking. */
- if (!ci->count && swap_cluster_alloc_table(ci, GFP_KERNEL))
- return -ENOMEM;
- spin_lock(&ci->lock);
+ if (!ci->count) {
+ ret = swap_cluster_alloc_table(NULL, ci, GFP_KERNEL, false);
+ if (ret)
+ goto out;
+ } else {
+ spin_lock(&ci->lock);
+ }
/* Check for duplicated bad swap slots. */
if (__swap_table_xchg(ci, ci_off, SWP_TB_BAD) != SWP_TB_NULL) {
pr_warn("Duplicated bad slot offset %d\n", offset);
@@ -835,6 +876,7 @@ static int swap_cluster_setup_bad_slot(struct swap_info_struct *si,
} else {
ci->count++;
}
+out:
spin_unlock(&ci->lock);
WARN_ON(ci->count > SWAPFILE_CLUSTER);
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v2 3/3] mm/swap: skip relocation of tableless clusters
2026-11-10 15:14 [PATCH v2 0/3] mm: fix swapcache writeout and cluster table races Nhat Pham
2026-11-10 15:14 ` [PATCH v2 1/3] mm: vmscan: allow writeout of swapcache folios without anon mappings Nhat Pham
2026-11-10 15:14 ` [PATCH v2 2/3] mm/swap: publish cluster tables after reacquiring allocator locks Nhat Pham
@ 2026-11-10 15:14 ` Nhat Pham
2 siblings, 0 replies; 4+ messages in thread
From: Nhat Pham @ 2026-11-10 15:14 UTC (permalink / raw)
To: linux-mm
Cc: akpm, hannes, david, mhocko, qi.zheng, shakeel.butt, ljs, kasong,
baohua, axelrasmussen, yuanchu, weixugc, baoquan.he, baolin.wang,
willy, brauner, linux-kernel, chrisl, shikemeng, nphamcs,
youngjun.park, ryncsn, sashiko-bot, stable
A stale reclaimer can reach relocate_cluster() after another allocator
re-isolates an emptied cluster and drops its locks to populate the table.
The cluster has zero count, CLUSTER_FLAG_NONE and no table, so relocation
can free it again and dereference NULL in the DEBUG_VM empty check.
CPU0 CPU1
Reclaimer Allocator
alloc_swap_scan_cluster()
cluster_reclaim_range()
spin_unlock(&ci->lock)
__try_to_reclaim_swap()
swap_cache_del_folio()
__swap_cache_del_folio()
__swap_cache_do_del_folio()
__swap_cluster_free_entries()
free_cluster()
__free_cluster()
swap_cluster_free_table()
swap_cluster_unlock(ci)
isolate_lock_cluster()
swap_cluster_populate()
drop ci and outer locks
swap_cluster_alloc_table()
spin_lock(&ci->lock)
relocate_cluster()
free_cluster()
__free_cluster()
swap_cluster_assert_empty()
__swap_table_get() /* NULL table */
Leave tableless clusters to the allocator under ci->lock. This also stops
a stale reclaimer from returning a cluster to the free list while table
population is pending, avoiding the resulting FREE-to-NONE state cycle.
The existing cluster lock serializes the table check and relocation with
population.
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://lore.kernel.org/all/sashiko-outbox-203018@kernel.org/
Fixes: 07adc4cf1ecd ("mm, swap: implement dynamic allocation of swap table")
Cc: stable@vger.kernel.org
Signed-off-by: Nhat Pham <nphamcs@gmail.com>
---
mm/swapfile.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 32639f65f13c..9fd62e4ca7a0 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -815,6 +815,13 @@ static void relocate_cluster(struct swap_info_struct *si,
if (cluster_is_discard(ci))
return;
+ /*
+ * An allocator may still be preparing the table.
+ * Leave the cluster to it.
+ */
+ if (!cluster_table_is_alloced(ci))
+ return;
+
if (!ci->count) {
if (ci->flags != CLUSTER_FLAG_FREE)
free_cluster(si, ci);
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-11-10 15:14 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-10 15:14 [PATCH v2 0/3] mm: fix swapcache writeout and cluster table races Nhat Pham
2026-11-10 15:14 ` [PATCH v2 1/3] mm: vmscan: allow writeout of swapcache folios without anon mappings Nhat Pham
2026-11-10 15:14 ` [PATCH v2 2/3] mm/swap: publish cluster tables after reacquiring allocator locks Nhat Pham
2026-11-10 15:14 ` [PATCH v2 3/3] mm/swap: skip relocation of tableless clusters Nhat Pham
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®