* [PATCH] mm, swap: revalidate populated clusters before scanning
@ 2026-10-19 16:06 Nhat Pham
2026-10-19 16:16 ` Nhat Pham
` (3 more replies)
0 siblings, 4 replies; 9+ messages in thread
From: Nhat Pham @ 2026-10-19 16:06 UTC (permalink / raw)
To: Andrew Morton
Cc: Chris Li, Kairui Song, Kemeng Shi, Baoquan He, Barry Song,
Youngjun Park, linux-mm, linux-kernel, stable
swap_cluster_populate() can publish a cluster's table with the cluster
lock dropped. A cached allocator can use and empty it before the
populating allocator reacquires the lock. The resumed scan can then
dereference a NULL table.
For example, CPU1 can retain a cached cluster offset on an SSD with
per-page discard disabled:
CPU0 CPU1
---- ----
swap_cluster_populate()
swap_cluster_alloc_table() fails
spin_unlock(&ci->lock)
local_unlock(&percpu_swap_cluster.lock)
swap_cluster_alloc_table()
rcu_assign_pointer(ci->table, table)
swap_alloc_fast()
local_unlock(&percpu_swap_cluster.lock)
mem_cgroup_try_charge_swap() fails
swap_cache_del_folio()
__swap_cluster_free_entries()
free_cluster()
__free_cluster()
swap_cluster_free_table()
rcu_assign_pointer(..., NULL)
local_lock(&percpu_swap_cluster.lock)
spin_lock(&ci->lock)
return ci
alloc_swap_scan_cluster()
cluster_scan_range()
__swap_table_get()
Reject an unusable cluster under its lock before scanning it. Relocate
it only while its table is present.
Fixes: cdd77f84d966 ("mm, swap: consolidate cluster allocation helpers")
Cc: stable@vger.kernel.org
Signed-off-by: Nhat Pham <nphamcs@gmail.com>
---
mm/swapfile.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index c3288910b3e3..988b7316e0a1 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -981,7 +981,11 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
bool need_reclaim, ret, usable;
lockdep_assert_held(&ci->lock);
- VM_WARN_ON(!cluster_is_usable(ci, order));
+ if (!cluster_is_usable(ci, order)) {
+ if (cluster_table_is_alloced(ci))
+ relocate_cluster(si, ci);
+ goto out_unlock;
+ }
if (end < nr_pages || ci->count + nr_pages > SWAPFILE_CLUSTER)
goto out;
@@ -1010,6 +1014,7 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
}
out:
relocate_cluster(si, ci);
+out_unlock:
swap_cluster_unlock(ci);
if (si->flags & SWP_SOLIDSTATE) {
this_cpu_write(percpu_swap_cluster.offset[order], next);
base-commit: 4dbe24720eb3811563cd56b35d87ddf3f9beed2a
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH] mm, swap: revalidate populated clusters before scanning
2026-10-19 16:06 [PATCH] mm, swap: revalidate populated clusters before scanning Nhat Pham
@ 2026-10-19 16:16 ` Nhat Pham
2026-10-20 6:58 ` Youngjun Park
` (2 subsequent siblings)
3 siblings, 0 replies; 9+ messages in thread
From: Nhat Pham @ 2026-10-19 16:16 UTC (permalink / raw)
To: Andrew Morton
Cc: Chris Li, Kairui Song, Baoquan He, Barry Song, Youngjun Park,
linux-mm, linux-kernel, stable
On Mon, Oct 19, 2026 at 9:06 AM Nhat Pham <nphamcs@gmail.com> wrote:
>
> swap_cluster_populate() can publish a cluster's table with the cluster
> lock dropped. A cached allocator can use and empty it before the
> populating allocator reacquires the lock. The resumed scan can then
> dereference a NULL table.
>
> For example, CPU1 can retain a cached cluster offset on an SSD with
> per-page discard disabled:
>
> CPU0 CPU1
> ---- ----
> swap_cluster_populate()
> swap_cluster_alloc_table() fails
> spin_unlock(&ci->lock)
> local_unlock(&percpu_swap_cluster.lock)
> swap_cluster_alloc_table()
> rcu_assign_pointer(ci->table, table)
> swap_alloc_fast()
> local_unlock(&percpu_swap_cluster.lock)
> mem_cgroup_try_charge_swap() fails
> swap_cache_del_folio()
> __swap_cluster_free_entries()
> free_cluster()
> __free_cluster()
> swap_cluster_free_table()
> rcu_assign_pointer(..., NULL)
> local_lock(&percpu_swap_cluster.lock)
> spin_lock(&ci->lock)
> return ci
> alloc_swap_scan_cluster()
> cluster_scan_range()
> __swap_table_get()
>
> Reject an unusable cluster under its lock before scanning it. Relocate
> it only while its table is present.
>
> Fixes: cdd77f84d966 ("mm, swap: consolidate cluster allocation helpers")
> Cc: stable@vger.kernel.org
> Signed-off-by: Nhat Pham <nphamcs@gmail.com>
I'm revisiting the lifecycle of clusters, because vswap introduced
dynamic (i.e run-time freeing) behavior to it. For the most part it
looks good (both physical and virtual side), but this look racy to me.
I don't see how it can be safe. Once you drop the lock, all bets are
off...
But at the same time, I have not been able to reproduce it (yet). Will
report here if I manage to.
(Side note: my vswap code is actually safe in this scenario. CPU0
allocates a new cluster entirely, and lock it before publishing it. So
even if the cluster index is cached in CPU1, this race cannot happen).
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] mm, swap: revalidate populated clusters before scanning
2026-10-19 16:06 [PATCH] mm, swap: revalidate populated clusters before scanning Nhat Pham
2026-10-19 16:16 ` Nhat Pham
@ 2026-10-20 6:58 ` Youngjun Park
2026-10-20 13:12 ` Nhat Pham
2026-10-20 16:06 ` sashiko-bot
2026-10-20 17:57 ` Kairui Song
3 siblings, 1 reply; 9+ messages in thread
From: Youngjun Park @ 2026-10-20 6:58 UTC (permalink / raw)
To: Nhat Pham
Cc: Andrew Morton, Chris Li, Kairui Song, Kemeng Shi, Baoquan He,
Barry Song, linux-mm, linux-kernel, stable
On Mon, Oct 19, 2026 at 09:06:15AM -0700, Nhat Pham wrote:
> swap_cluster_populate() can publish a cluster's table with the cluster
> lock dropped. A cached allocator can use and empty it before the
> populating allocator reacquires the lock. The resumed scan can then
> dereference a NULL table.
>
> For example, CPU1 can retain a cached cluster offset on an SSD with
> per-page discard disabled:
>
> CPU0 CPU1
> ---- ----
> swap_cluster_populate()
> swap_cluster_alloc_table() fails
> spin_unlock(&ci->lock)
> local_unlock(&percpu_swap_cluster.lock)
> swap_cluster_alloc_table()
> rcu_assign_pointer(ci->table, table)
> swap_alloc_fast()
> local_unlock(&percpu_swap_cluster.lock)
> mem_cgroup_try_charge_swap() fails
> swap_cache_del_folio()
> __swap_cluster_free_entries()
> free_cluster()
> __free_cluster()
> swap_cluster_free_table()
> rcu_assign_pointer(..., NULL)
> local_lock(&percpu_swap_cluster.lock)
> spin_lock(&ci->lock)
> return ci
> alloc_swap_scan_cluster()
> cluster_scan_range()
> __swap_table_get()
Hello Nhat.
Good catch!
> Reject an unusable cluster under its lock before scanning it. Relocate
> it only while its table is present.
>
> Fixes: cdd77f84d966 ("mm, swap: consolidate cluster allocation helpers")
> Cc: stable@vger.kernel.org
> Signed-off-by: Nhat Pham <nphamcs@gmail.com>
> ---
> mm/swapfile.c | 7 ++++++-
> 1 file changed, 6 insertions(+), 1 deletion(-)
>
> diff --git a/mm/swapfile.c b/mm/swapfile.c
> index c3288910b3e3..988b7316e0a1 100644
> --- a/mm/swapfile.c
> +++ b/mm/swapfile.c
> @@ -981,7 +981,11 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
> bool need_reclaim, ret, usable;
>
> lockdep_assert_held(&ci->lock);
> - VM_WARN_ON(!cluster_is_usable(ci, order));
> + if (!cluster_is_usable(ci, order)) {
> + if (cluster_table_is_alloced(ci))
> + relocate_cluster(si, ci);
> + goto out_unlock;
> + }
If this patch is applied as bloew, does it prevent the issue described here?
https://lore.kernel.org/linux-mm/20260720071342.50742-2-shikemeng@huaweicloud.com/
(This patch publishes cluster table after lock acquired.)
This patch is chagned the way I suggested during review... :(
As you pointed out, another context can free the table
without alloc context knwoing this situation.
I missed this situation.
IMHO, Publishing the table only after reacquiring the cluster lock, as Kemeng
suggested, and rejecting an unusable cluster under the lock as in this
patch both look correct to me!
Reviewed-by: Youngjun Park <youngjun.park@lge.com>
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] mm, swap: revalidate populated clusters before scanning
2026-10-20 6:58 ` Youngjun Park
@ 2026-10-20 13:12 ` Nhat Pham
2026-10-20 13:18 ` Nhat Pham
0 siblings, 1 reply; 9+ messages in thread
From: Nhat Pham @ 2026-10-20 13:12 UTC (permalink / raw)
To: Youngjun Park
Cc: Andrew Morton, Chris Li, Kairui Song, Kemeng Shi, Baoquan He,
Barry Song, linux-mm, linux-kernel, stable
On Mon, Oct 19, 2026 at 11:58 PM Youngjun Park <youngjun.park@lge.com> wrote:
>
> On Mon, Oct 19, 2026 at 09:06:15AM -0700, Nhat Pham wrote:
> > swap_cluster_populate() can publish a cluster's table with the cluster
> > lock dropped. A cached allocator can use and empty it before the
> > populating allocator reacquires the lock. The resumed scan can then
> > dereference a NULL table.
> >
> > For example, CPU1 can retain a cached cluster offset on an SSD with
> > per-page discard disabled:
> >
> > CPU0 CPU1
> > ---- ----
> > swap_cluster_populate()
> > swap_cluster_alloc_table() fails
> > spin_unlock(&ci->lock)
> > local_unlock(&percpu_swap_cluster.lock)
> > swap_cluster_alloc_table()
> > rcu_assign_pointer(ci->table, table)
> > swap_alloc_fast()
> > local_unlock(&percpu_swap_cluster.lock)
> > mem_cgroup_try_charge_swap() fails
> > swap_cache_del_folio()
> > __swap_cluster_free_entries()
> > free_cluster()
> > __free_cluster()
> > swap_cluster_free_table()
> > rcu_assign_pointer(..., NULL)
> > local_lock(&percpu_swap_cluster.lock)
> > spin_lock(&ci->lock)
> > return ci
> > alloc_swap_scan_cluster()
> > cluster_scan_range()
> > __swap_table_get()
>
> Hello Nhat.
>
> Good catch!
>
> > Reject an unusable cluster under its lock before scanning it. Relocate
> > it only while its table is present.
> >
> > Fixes: cdd77f84d966 ("mm, swap: consolidate cluster allocation helpers")
> > Cc: stable@vger.kernel.org
> > Signed-off-by: Nhat Pham <nphamcs@gmail.com>
> > ---
> > mm/swapfile.c | 7 ++++++-
> > 1 file changed, 6 insertions(+), 1 deletion(-)
> >
> > diff --git a/mm/swapfile.c b/mm/swapfile.c
> > index c3288910b3e3..988b7316e0a1 100644
> > --- a/mm/swapfile.c
> > +++ b/mm/swapfile.c
> > @@ -981,7 +981,11 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
> > bool need_reclaim, ret, usable;
> >
> > lockdep_assert_held(&ci->lock);
> > - VM_WARN_ON(!cluster_is_usable(ci, order));
> > + if (!cluster_is_usable(ci, order)) {
> > + if (cluster_table_is_alloced(ci))
> > + relocate_cluster(si, ci);
> > + goto out_unlock;
> > + }
>
> If this patch is applied as bloew, does it prevent the issue described here?
>
> https://lore.kernel.org/linux-mm/20260720071342.50742-2-shikemeng@huaweicloud.com/
> (This patch publishes cluster table after lock acquired.)
>
> This patch is chagned the way I suggested during review... :(
Ah I missed that patch series and your review! What happened to it?
Did we just never land it?
Looks like we are still doing neither Keming's approach (table
copying) nor your approach (publishing at the end of
swap_cluster_alloc_table()).
>
> As you pointed out, another context can free the table
> without alloc context knwoing this situation.
> I missed this situation.
Yeah I feel like it's still not safe with your approach...
swap_cluster_alloc_table() does not hold nor acquire cluster's lock.
Between its publishing of the table and the subsequent lock
acquisition in its caller (swap_cluster_populate()), someone can still
come in and free the table.
It's tricky. We do not have an explicit contract for the metadata's
lifetime, so it's a bit of reading the tea leaves for me...
I think this is the last gap, and it's rare enough, so this simple
check should suffice to cover the edgiest of the edge cases :) But who
knows :) If bugs re-surfaced again, I think we can spend a bit more
time ironcladding the lifetime of clusters/tables:
1. Allocate outside of lock context, but make swap_cluster_populate()
lock the cluster, check cluster's status, and only then publish the
table (and return with the cluster locked). Basically Keming's
approach.
2. Carefully enforce metadata's lifetime. Perhaps we can add a refcnt?
In addition to occupied slots holding cluster's refcnt, we can allow
for temporary holders. As long as we can acquire a refcnt, we know for
sure the metadata is not freed under us. This is my approach for the
old vswap patch series, but it's tricky, especially with caching in
the picture.
>
> IMHO, Publishing the table only after reacquiring the cluster lock, as Kemeng
> suggested, and rejecting an unusable cluster under the lock as in this
> patch both look correct to me!
>
> Reviewed-by: Youngjun Park <youngjun.park@lge.com>
Thanks for the review, Youngjun!
>
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] mm, swap: revalidate populated clusters before scanning
2026-10-20 13:12 ` Nhat Pham
@ 2026-10-20 13:18 ` Nhat Pham
2026-10-20 17:58 ` Kairui Song
0 siblings, 1 reply; 9+ messages in thread
From: Nhat Pham @ 2026-10-20 13:18 UTC (permalink / raw)
To: Youngjun Park
Cc: Andrew Morton, Chris Li, Kairui Song, Kemeng Shi, Baoquan He,
Barry Song, linux-mm, linux-kernel, stable
On Tue, Oct 20, 2026 at 6:12 AM Nhat Pham <nphamcs@gmail.com> wrote:
>
> On Mon, Oct 19, 2026 at 11:58 PM Youngjun Park <youngjun.park@lge.com> wrote:
> >
> > On Mon, Oct 19, 2026 at 09:06:15AM -0700, Nhat Pham wrote:
> > > swap_cluster_populate() can publish a cluster's table with the cluster
> > > lock dropped. A cached allocator can use and empty it before the
> > > populating allocator reacquires the lock. The resumed scan can then
> > > dereference a NULL table.
> > >
> > > For example, CPU1 can retain a cached cluster offset on an SSD with
> > > per-page discard disabled:
> > >
> > > CPU0 CPU1
> > > ---- ----
> > > swap_cluster_populate()
> > > swap_cluster_alloc_table() fails
> > > spin_unlock(&ci->lock)
> > > local_unlock(&percpu_swap_cluster.lock)
> > > swap_cluster_alloc_table()
> > > rcu_assign_pointer(ci->table, table)
> > > swap_alloc_fast()
> > > local_unlock(&percpu_swap_cluster.lock)
> > > mem_cgroup_try_charge_swap() fails
> > > swap_cache_del_folio()
> > > __swap_cluster_free_entries()
> > > free_cluster()
> > > __free_cluster()
> > > swap_cluster_free_table()
> > > rcu_assign_pointer(..., NULL)
> > > local_lock(&percpu_swap_cluster.lock)
> > > spin_lock(&ci->lock)
> > > return ci
> > > alloc_swap_scan_cluster()
> > > cluster_scan_range()
> > > __swap_table_get()
> >
> > Hello Nhat.
> >
> > Good catch!
> >
> > > Reject an unusable cluster under its lock before scanning it. Relocate
> > > it only while its table is present.
> > >
> > > Fixes: cdd77f84d966 ("mm, swap: consolidate cluster allocation helpers")
> > > Cc: stable@vger.kernel.org
> > > Signed-off-by: Nhat Pham <nphamcs@gmail.com>
> > > ---
> > > mm/swapfile.c | 7 ++++++-
> > > 1 file changed, 6 insertions(+), 1 deletion(-)
> > >
> > > diff --git a/mm/swapfile.c b/mm/swapfile.c
> > > index c3288910b3e3..988b7316e0a1 100644
> > > --- a/mm/swapfile.c
> > > +++ b/mm/swapfile.c
> > > @@ -981,7 +981,11 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
> > > bool need_reclaim, ret, usable;
> > >
> > > lockdep_assert_held(&ci->lock);
> > > - VM_WARN_ON(!cluster_is_usable(ci, order));
> > > + if (!cluster_is_usable(ci, order)) {
> > > + if (cluster_table_is_alloced(ci))
> > > + relocate_cluster(si, ci);
> > > + goto out_unlock;
> > > + }
> >
> > If this patch is applied as bloew, does it prevent the issue described here?
> >
> > https://lore.kernel.org/linux-mm/20260720071342.50742-2-shikemeng@huaweicloud.com/
> > (This patch publishes cluster table after lock acquired.)
> >
> > This patch is chagned the way I suggested during review... :(
>
> Ah I missed that patch series and your review! What happened to it?
> Did we just never land it?
>
> Looks like we are still doing neither Keming's approach (table
> copying) nor your approach (publishing at the end of
> swap_cluster_alloc_table()).
Ah nope, I saw that Kemeng implemented your approach:
https://lore.kernel.org/linux-mm/20260907091356.53026-2-shikemeng@huaweicloud.com/
My bad! But yeah I think we still need this small fix (or Kemeng's
original proposal?), unless I missed something...?
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] mm, swap: revalidate populated clusters before scanning
2026-10-19 16:06 [PATCH] mm, swap: revalidate populated clusters before scanning Nhat Pham
2026-10-19 16:16 ` Nhat Pham
2026-10-20 6:58 ` Youngjun Park
@ 2026-10-20 16:06 ` sashiko-bot
2026-10-20 17:57 ` Kairui Song
3 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-10-20 16:06 UTC (permalink / raw)
To: Nhat Pham, Andrew Morton
Cc: Chris Li, Kairui Song, Kemeng Shi, Baoquan He, Barry Song,
Youngjun Park, linux-mm
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] mm, swap: alloc_swap_scan_cluster() relocates and scans stolen clusters
- [High] mm, swap: alloc_swap_scan_cluster() relocates unusable cluster after reclaim
- [Medium] mm, swap: alloc_swap_scan_list() aborts free_clusters scan on stolen cluster
--
commit 4e1646405a5a83e283c6f4c0fe1ba0012bdd06c6
Author: Nhat Pham <nphamcs@gmail.com>
mm, swap: revalidate populated clusters before scanning
This commit checks cluster_is_usable() at the start of
alloc_swap_scan_cluster() to avoid dereferencing a NULL cluster table when a
cached allocator uses and frees a newly populated cluster while
swap_cluster_populate() has dropped the cluster lock.
> diff --git a/mm/swapfile.c b/mm/swapfile.c
> index c3288910b3e35..988b7316e0a18 100644
> --- a/mm/swapfile.c
> +++ b/mm/swapfile.c
> @@ -981,7 +981,11 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
> bool need_reclaim, ret, usable;
>
> lockdep_assert_held(&ci->lock);
> - VM_WARN_ON(!cluster_is_usable(ci, order));
> + if (!cluster_is_usable(ci, order)) {
> + if (cluster_table_is_alloced(ci))
> + relocate_cluster(si, ci);
> + goto out_unlock;
> + }
[Severity: High]
Can this relocate a cluster that another CPU has already relocated or
isolated, and still scan a cluster that was stolen while
swap_cluster_populate() dropped ci->lock?
All callers of alloc_swap_scan_cluster() other than
isolate_lock_cluster(si, &si->free_clusters) either check
cluster_is_usable(ci, order) under ci->lock immediately prior or isolate ci
without dropping ci->lock. When swap_cluster_populate() publishes ci->table
in swap_cluster_alloc_table() while ci->lock is dropped, ci initially has
ci->flags == CLUSTER_FLAG_NONE and ci->count == 0, which satisfies
cluster_is_usable(ci, order) for all orders.
Consequently, !cluster_is_usable(ci, order) at entry to
alloc_swap_scan_cluster() can only happen if a racing CPU (CPU1) locked ci via
a cached offset (such as in swap_alloc_fast()) and ran
alloc_swap_scan_cluster() before the populating CPU (CPU0) reacquired
ci->lock. When CPU1 finished alloc_swap_scan_cluster(), CPU1 already called
relocate_cluster(si, ci) at the out label, placing ci onto frag_clusters,
full_clusters, discard_clusters, or free_clusters.
If CPU0 then calls relocate_cluster(si, ci) when cluster_table_is_alloced(ci)
is true:
1. If a third CPU (CPU2) subsequently isolated ci from frag_clusters or
full_clusters via isolate_lock_cluster() (setting ci->flags to
CLUSTER_FLAG_NONE) and dropped ci->lock inside cluster_reclaim_range() or
swap_reclaim_full_clusters(), CPU0's relocate_cluster(si, ci) sees
ci->flags == CLUSTER_FLAG_NONE:
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);
} else {
if (ci->flags != CLUSTER_FLAG_FULL)
move_cluster(si, ci, &si->full_clusters,
CLUSTER_FLAG_FULL);
}
}
This moves ci back onto si->frag_clusters[ci->order] or si->full_clusters
while CPU2 still holds it isolated, allowing another CPU to isolate the same
cluster concurrently.
2. If an entry in ci was freed after CPU1 allocated from it,
partial_free_cluster() moved ci to si->nonfull_clusters[ci->order] with
ci->flags == CLUSTER_FLAG_NONFULL. CPU0's relocate_cluster(si, ci) sees
ci->flags != CLUSTER_FLAG_FRAG and demotes ci from nonfull_clusters to
frag_clusters.
Conversely, because swap_cluster_populate() publishes ci->table before
reacquiring percpu_swap_cluster.lock, si->global_cluster_lock, and ci->lock,
what happens if CPU1 allocates from ci while ci->lock is dropped, but
cluster_is_usable(ci, order) remains true for CPU0's order?
mm/swapfile.c:cluster_is_usable() {
if (unlikely(ci->flags > CLUSTER_FLAG_USABLE))
return false;
if (!cluster_table_is_alloced(ci))
return false;
if (!order)
return true;
return cluster_is_empty(ci) || order == ci->order;
}
For example, if CPU0 has order == 0 while CPU1 claimed ci for a higher order
and placed it on si->frag_clusters[ci->order], or if CPU1 allocated the same
order and placed ci on frag_clusters (or CPU2 isolated it from frag_clusters
and dropped ci->lock in cluster_reclaim_range()), cluster_is_usable(ci, order)
still returns true on CPU0. Wouldn't CPU0 then scan, allocate from, and
relocate a cluster that is already on frag_clusters or isolated by CPU2,
fragmenting high-order clusters and caching the same cluster on multiple CPUs?
[Severity: Medium]
Also, when alloc_swap_scan_cluster() takes the goto out_unlock path for a
stolen cluster, it returns SWAP_ENTRY_INVALID to alloc_swap_scan_list():
mm/swapfile.c:alloc_swap_scan_list() {
do {
struct swap_cluster_info *ci = isolate_lock_cluster(si, list);
unsigned long offset;
if (!ci)
break;
offset = cluster_offset(si, ci);
found = alloc_swap_scan_cluster(si, ci, folio, offset);
if (found)
break;
} while (scan_all);
}
In cluster_alloc_swap_entry(), both calls scanning &si->free_clusters pass
scan_all = false:
mm/swapfile.c:cluster_alloc_swap_entry() {
if (si->flags & SWP_PAGE_DISCARD) {
found = alloc_swap_scan_list(si, &si->free_clusters, folio, false);
if (found)
goto done;
}
...
if (!(si->flags & SWP_PAGE_DISCARD)) {
found = alloc_swap_scan_list(si, &si->free_clusters, folio, false);
if (found)
goto done;
}
}
Because scan_all is false, won't alloc_swap_scan_list() exit the loop after a
single stolen cluster without trying to isolate another cluster from
si->free_clusters (causing PMD_ORDER allocations, or allocations when
nonfull_clusters and frag_clusters are empty, to fail even when free clusters
remain on si->free_clusters)?
>
> if (end < nr_pages || ci->count + nr_pages > SWAPFILE_CLUSTER)
> goto out;
> @@ -1010,6 +1014,7 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
> }
> out:
> relocate_cluster(si, ci);
> +out_unlock:
> swap_cluster_unlock(ci);
> if (si->flags & SWP_SOLIDSTATE) {
> this_cpu_write(percpu_swap_cluster.offset[order], next);
[Severity: High]
Does the !usable path after cluster_reclaim_range() in
alloc_swap_scan_cluster() have a similar issue when jumping to out instead of
out_unlock?
In the scan loop just above:
mm/swapfile.c:alloc_swap_scan_cluster() {
if (need_reclaim) {
ret = cluster_reclaim_range(si, ci, offset, order, &usable);
if (!usable)
goto out;
}
cluster_reclaim_range() drops ci->lock to call __try_to_reclaim_swap() and
then rechecks cluster_is_usable(ci, order) after reacquiring ci->lock:
mm/swapfile.c:cluster_reclaim_range() {
spin_unlock(&ci->lock);
...
spin_lock(&ci->lock);
...
if (!cluster_is_usable(ci, order)) {
*usable = false;
return false;
}
}
While ci->lock is dropped in cluster_reclaim_range(), __try_to_reclaim_swap()
or a concurrent free on another CPU can free the last entry in ci, causing
__swap_cluster_free_entries() -> free_cluster() -> __free_cluster() to free
ci->table (setting ci->table to NULL) and move ci to si->free_clusters with
ci->flags = CLUSTER_FLAG_FREE.
Before cluster_reclaim_range() reacquires ci->lock, another CPU (CPU2) can
call isolate_lock_cluster(si, &si->free_clusters), which removes ci from
si->free_clusters, sets ci->flags = CLUSTER_FLAG_NONE, and drops ci->lock in
swap_cluster_populate() when atomic table allocation fails.
When cluster_reclaim_range() reacquires ci->lock, ci->count is 0, ci->flags is
CLUSTER_FLAG_NONE, and ci->table is NULL, so cluster_reclaim_range() sets
*usable = false. Back in alloc_swap_scan_cluster(), 'if (!usable) goto out;'
calls relocate_cluster(si, ci), which sees !ci->count and
ci->flags != CLUSTER_FLAG_FREE and calls free_cluster(si, ci) ->
__free_cluster(si, ci) while ci->table is NULL and CPU2 is running
swap_cluster_alloc_table() unlocked.
Won't that dereference a NULL ci->table in swap_cluster_assert_empty() (under
CONFIG_DEBUG_VM), race swap_cluster_free_table() against CPU2's unlocked
swap_cluster_alloc_table() on ci->memcg_table and ci->zero_bitmap, and place
ci back onto si->free_clusters while CPU2 holds it isolated?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261019160615.2348500-1-nphamcs@gmail.com?part=1
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] mm, swap: revalidate populated clusters before scanning
2026-10-19 16:06 [PATCH] mm, swap: revalidate populated clusters before scanning Nhat Pham
` (2 preceding siblings ...)
2026-10-20 16:06 ` sashiko-bot
@ 2026-10-20 17:57 ` Kairui Song
2026-10-20 20:19 ` Nhat Pham
3 siblings, 1 reply; 9+ messages in thread
From: Kairui Song @ 2026-10-20 17:57 UTC (permalink / raw)
To: Nhat Pham
Cc: Andrew Morton, Chris Li, Kemeng Shi, Baoquan He, Barry Song,
Youngjun Park, linux-mm, linux-kernel, stable
On Tue, Oct 20, 2026 at 1:41 AM Nhat Pham <nphamcs@gmail.com> wrote:
>
> swap_cluster_populate() can publish a cluster's table with the cluster
> lock dropped. A cached allocator can use and empty it before the
> populating allocator reacquires the lock. The resumed scan can then
> dereference a NULL table.
>
> For example, CPU1 can retain a cached cluster offset on an SSD with
> per-page discard disabled:
>
> CPU0 CPU1
> ---- ----
> swap_cluster_populate()
> swap_cluster_alloc_table() fails
> spin_unlock(&ci->lock)
> local_unlock(&percpu_swap_cluster.lock)
> swap_cluster_alloc_table()
> rcu_assign_pointer(ci->table, table)
Nice catch!
I think the real problem is that it turns out calling
swap_cluster_alloc_table() without holding ci lock is bad idea... I
think this patch still leaves some other call site too?
We may instead add a "bool locked" to swap_cluster_alloc_table, and
let it lock the ci first before it does any assign (not the alloc) if
the allocation context is not locked. That is, keep the
sleep-allocated tables private until swap_cluster_populate()
reacquires local/global/ci, then install them under ci->lock,
something like pre-cdd77f84d966, always assign it atomically.
That should also cover the problem reported by Kemeng a few weeks
earlier too, and prevent further misuse. My Bad on that change :P
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] mm, swap: revalidate populated clusters before scanning
2026-10-20 13:18 ` Nhat Pham
@ 2026-10-20 17:58 ` Kairui Song
0 siblings, 0 replies; 9+ messages in thread
From: Kairui Song @ 2026-10-20 17:58 UTC (permalink / raw)
To: Nhat Pham
Cc: Youngjun Park, Andrew Morton, Chris Li, Kemeng Shi, Baoquan He,
Barry Song, linux-mm, linux-kernel, stable
On Tue, Oct 20, 2026 at 9:23 PM Nhat Pham <nphamcs@gmail.com> wrote:
>
> On Tue, Oct 20, 2026 at 6:12 AM Nhat Pham <nphamcs@gmail.com> wrote:
> >
> > On Mon, Oct 19, 2026 at 11:58 PM Youngjun Park <youngjun.park@lge.com> wrote:
> > >
> > > On Mon, Oct 19, 2026 at 09:06:15AM -0700, Nhat Pham wrote:
> > > > swap_cluster_populate() can publish a cluster's table with the cluster
> > > > lock dropped. A cached allocator can use and empty it before the
> > > > populating allocator reacquires the lock. The resumed scan can then
> > > > dereference a NULL table.
> > > >
> > > > For example, CPU1 can retain a cached cluster offset on an SSD with
> > > > per-page discard disabled:
> > > >
> > > > CPU0 CPU1
> > > > ---- ----
> > > > swap_cluster_populate()
> > > > swap_cluster_alloc_table() fails
> > > > spin_unlock(&ci->lock)
> > > > local_unlock(&percpu_swap_cluster.lock)
> > > > swap_cluster_alloc_table()
> > > > rcu_assign_pointer(ci->table, table)
> > > > swap_alloc_fast()
> > > > local_unlock(&percpu_swap_cluster.lock)
> > > > mem_cgroup_try_charge_swap() fails
> > > > swap_cache_del_folio()
> > > > __swap_cluster_free_entries()
> > > > free_cluster()
> > > > __free_cluster()
> > > > swap_cluster_free_table()
> > > > rcu_assign_pointer(..., NULL)
> > > > local_lock(&percpu_swap_cluster.lock)
> > > > spin_lock(&ci->lock)
> > > > return ci
> > > > alloc_swap_scan_cluster()
> > > > cluster_scan_range()
> > > > __swap_table_get()
> > >
> > > Hello Nhat.
> > >
> > > Good catch!
> > >
> > > > Reject an unusable cluster under its lock before scanning it. Relocate
> > > > it only while its table is present.
> > > >
> > > > Fixes: cdd77f84d966 ("mm, swap: consolidate cluster allocation helpers")
> > > > Cc: stable@vger.kernel.org
> > > > Signed-off-by: Nhat Pham <nphamcs@gmail.com>
> > > > ---
> > > > mm/swapfile.c | 7 ++++++-
> > > > 1 file changed, 6 insertions(+), 1 deletion(-)
> > > >
> > > > diff --git a/mm/swapfile.c b/mm/swapfile.c
> > > > index c3288910b3e3..988b7316e0a1 100644
> > > > --- a/mm/swapfile.c
> > > > +++ b/mm/swapfile.c
> > > > @@ -981,7 +981,11 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
> > > > bool need_reclaim, ret, usable;
> > > >
> > > > lockdep_assert_held(&ci->lock);
> > > > - VM_WARN_ON(!cluster_is_usable(ci, order));
> > > > + if (!cluster_is_usable(ci, order)) {
> > > > + if (cluster_table_is_alloced(ci))
> > > > + relocate_cluster(si, ci);
> > > > + goto out_unlock;
> > > > + }
> > >
> > > If this patch is applied as bloew, does it prevent the issue described here?
> > >
> > > https://lore.kernel.org/linux-mm/20260720071342.50742-2-shikemeng@huaweicloud.com/
> > > (This patch publishes cluster table after lock acquired.)
> > >
> > > This patch is chagned the way I suggested during review... :(
> >
> > Ah I missed that patch series and your review! What happened to it?
> > Did we just never land it?
> >
> > Looks like we are still doing neither Keming's approach (table
> > copying) nor your approach (publishing at the end of
> > swap_cluster_alloc_table()).
>
> Ah nope, I saw that Kemeng implemented your approach:
>
> https://lore.kernel.org/linux-mm/20260907091356.53026-2-shikemeng@huaweicloud.com/
>
> My bad! But yeah I think we still need this small fix (or Kemeng's
> original proposal?), unless I missed something...?
Right I remember that, I acked part of that and was waiting the update
on that sashiko comment, as the first patch isn't perfect, while
Kemeng's mail at "shikemeng@huaweicloud.com" is now bouncing back...
I'm not sure if Kemeng still plans to update that.
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] mm, swap: revalidate populated clusters before scanning
2026-10-20 17:57 ` Kairui Song
@ 2026-10-20 20:19 ` Nhat Pham
0 siblings, 0 replies; 9+ messages in thread
From: Nhat Pham @ 2026-10-20 20:19 UTC (permalink / raw)
To: Kairui Song
Cc: Andrew Morton, Chris Li, Kemeng Shi, Baoquan He, Barry Song,
Youngjun Park, linux-mm, linux-kernel, stable
On Tue, Oct 20, 2026 at 10:58 AM Kairui Song <ryncsn@gmail.com> wrote:
>
> On Tue, Oct 20, 2026 at 1:41 AM Nhat Pham <nphamcs@gmail.com> wrote:
> >
> > swap_cluster_populate() can publish a cluster's table with the cluster
> > lock dropped. A cached allocator can use and empty it before the
> > populating allocator reacquires the lock. The resumed scan can then
> > dereference a NULL table.
> >
> > For example, CPU1 can retain a cached cluster offset on an SSD with
> > per-page discard disabled:
> >
> > CPU0 CPU1
> > ---- ----
> > swap_cluster_populate()
> > swap_cluster_alloc_table() fails
> > spin_unlock(&ci->lock)
> > local_unlock(&percpu_swap_cluster.lock)
> > swap_cluster_alloc_table()
> > rcu_assign_pointer(ci->table, table)
>
> Nice catch!
>
> I think the real problem is that it turns out calling
> swap_cluster_alloc_table() without holding ci lock is bad idea... I
> think this patch still leaves some other call site too?
Do we have another callsite? There's a callsite in setup code, but I
assume that's safe...?
But at this point, who knows any more. The caching only stores a
swapfile struct, and an offset. I wonder if there is a potential race
between swapoff-then-re-swapon and cached allocation? That'd be pretty
wild.
>
> We may instead add a "bool locked" to swap_cluster_alloc_table, and
> let it lock the ci first before it does any assign (not the alloc) if
> the allocation context is not locked. That is, keep the
> sleep-allocated tables private until swap_cluster_populate()
> reacquires local/global/ci, then install them under ci->lock,
> something like pre-cdd77f84d966, always assign it atomically.
Hmm so a bitlock?
Lemme think about it a bit.
>
> That should also cover the problem reported by Kemeng a few weeks
> earlier too, and prevent further misuse. My Bad on that change :P
No worries. Lifetime is hard :)
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-10-20 20:19 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-19 16:06 [PATCH] mm, swap: revalidate populated clusters before scanning Nhat Pham
2026-10-19 16:16 ` Nhat Pham
2026-10-20 6:58 ` Youngjun Park
2026-10-20 13:12 ` Nhat Pham
2026-10-20 13:18 ` Nhat Pham
2026-10-20 17:58 ` Kairui Song
2026-10-20 16:06 ` sashiko-bot
2026-10-20 17:57 ` Kairui Song
2026-10-20 20:19 ` Nhat Pham
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox