From: sashiko-bot@kernel.org
To: "Nhat Pham" <nphamcs@gmail.com>,
linux-mm@kvack.org, akpm@linux-foundation.org,
axelrasmussen@google.com, baohua@kernel.org,
baolin.wang@linux.alibaba.com, baoquan.he@linux.dev,
brauner@kernel.org, chrisl@kernel.org, david@kernel.org,
hannes@cmpxchg.org, kasong@tencent.com, ljs@kernel.org,
mhocko@kernel.org, qi.zheng@linux.dev, shakeel.butt@linux.dev,
shikemeng@huaweicloud.com, weixugc@google.com,
willy@infradead.org, youngjun.park@lge.com, yuanchu@google.com
Cc: ryncsn@gmail.com
Subject: Re: [PATCH 2/2] mm/swap: publish cluster tables after reacquiring allocator locks
Date: Sat, 07 Nov 2026 16:04:58 +0000 [thread overview]
Message-ID: <sashiko-outbox-203018@kernel.org> (raw)
In-Reply-To: <20261106160432.2552507-3-nphamcs@gmail.com>
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] mm/swap: NULL deref in swap_cluster_assert_empty() on stale
reclaimer race
- [Medium] mm/swap: ABA race on cluster state check in
swap_cluster_alloc_table()
--
commit 22e220b18bd05df45a828a3170169b92c0c14f76
Author: Nhat Pham <nphamcs@gmail.com>
mm/swap: publish cluster tables after reacquiring allocator locks
This commit keeps newly allocated swap cluster tables private until
swap_cluster_alloc_table() reacquires the allocator and cluster locks, and
abandons installation if a stale reclaimer returned the cluster while the
locks were dropped.
Link: https://lore.kernel.org/all/sashiko-outbox-181084@kernel.org/
Link: https://lore.kernel.org/all/sashiko-outbox-181086@kernel.org/
> diff --git a/mm/swapfile.c b/mm/swapfile.c
> index 189f04d8838f3..162a73a9e6638 100644
> --- a/mm/swapfile.c
> +++ b/mm/swapfile.c
[ ... ]
> @@ -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 allocation. */
> + if (ci->flags != CLUSTER_FLAG_NONE || cluster_table_is_alloced(ci))
> + ret = -EAGAIN;
[Severity: Medium]
Can a stale reclaimer trigger a NULL pointer dereference in
swap_cluster_assert_empty() while ci->lock is dropped here?
Because ci->table now remains NULL across the sleeping allocation in
swap_cluster_populate(), a stale reclaimer that dropped ci->lock before the
cluster was freed and re-isolated can reacquire ci->lock while ci->count is 0,
ci->flags is CLUSTER_FLAG_NONE, and ci->table is NULL:
CPU0 CPU1
cluster_reclaim_range()
spin_unlock(&ci->lock)
/* ci is freed and table is freed */
isolate_lock_cluster()
ci->flags = CLUSTER_FLAG_NONE
swap_cluster_populate()
/* atomic alloc fails */
spin_unlock(&ci->lock)
swap_cluster_alloc_table()
spin_lock(&ci->lock)
...
relocate_cluster()
free_cluster()
__free_cluster()
swap_cluster_assert_empty()
__swap_table_get()
<-- NULL pointer dereference
In relocate_cluster(), !ci->count and ci->flags != CLUSTER_FLAG_FREE are both
true, so it calls free_cluster() -> __free_cluster() without checking
cluster_table_is_alloced(ci). With CONFIG_DEBUG_VM enabled,
swap_cluster_assert_empty() calls __swap_table_get(), which dereferences the
NULL ci->table. With SWP_PAGE_DISCARD enabled, free_cluster() also schedules
a redundant discard before swap_do_scheduled_discard() calls __free_cluster()
on the tableless cluster.
Should relocate_cluster() avoid calling free_cluster() when
!cluster_table_is_alloced(ci)?
[ ... ]
> @@ -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;
> }
[Severity: Medium]
Can an ABA race on ci->flags and ci->table cause a failed allocator to put a
cluster back onto si->free_clusters while a second allocator still has it
isolated?
When CONFIG_DEBUG_VM and SWP_PAGE_DISCARD are disabled, a stale reclaimer
calling relocate_cluster() -> free_cluster() -> __free_cluster() moves the
tableless cluster back to si->free_clusters. A second allocator can then
re-isolate it and also drop ci->lock for a sleeping table allocation:
CPU0 CPU1 CPU2
swap_cluster_populate()
spin_unlock(&ci->lock)
swap_cluster_alloc_table()
/* alloc fails */
relocate_cluster()
...
__free_cluster()
ci->flags =
CLUSTER_FLAG_FREE
isolate_lock_cluster()
ci->flags =
CLUSTER_FLAG_NONE
swap_cluster_populate()
spin_unlock(&ci->lock)
spin_lock(&ci->lock)
/* flags == NONE,
* table == NULL */
return -ENOMEM
move_cluster(..., FREE) <-- puts ci on free_clusters while CPU2 owns it
When CPU0 reacquires ci->lock, ci->flags is back to CLUSTER_FLAG_NONE and
cluster_table_is_alloced(ci) is still false, so swap_cluster_alloc_table()
returns -ENOMEM instead of -EAGAIN and CPU0 moves the cluster back onto
si->free_clusters while CPU2 is still sleeping in swap_cluster_alloc_table().
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261106160432.2552507-1-nphamcs@gmail.com?part=2
prev parent reply other threads:[~2026-11-07 16:05 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-06 16:04 [PATCH 0/2] mm: fix swapcache writeout and cluster table publication Nhat Pham
2026-11-06 16:04 ` [PATCH 1/2] mm: vmscan: allow writeout of swapcache folios without anon mappings Nhat Pham
2026-11-06 16:04 ` [PATCH 2/2] mm/swap: publish cluster tables after reacquiring allocator locks Nhat Pham
2026-11-06 19:59 ` Nhat Pham
2026-11-07 18:02 ` Kairui Song
2026-11-07 16:04 ` 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-203018@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=akpm@linux-foundation.org \
--cc=axelrasmussen@google.com \
--cc=baohua@kernel.org \
--cc=baolin.wang@linux.alibaba.com \
--cc=baoquan.he@linux.dev \
--cc=brauner@kernel.org \
--cc=chrisl@kernel.org \
--cc=david@kernel.org \
--cc=hannes@cmpxchg.org \
--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=ryncsn@gmail.com \
--cc=sashiko-reviews@lists.linux.dev \
--cc=shakeel.butt@linux.dev \
--cc=shikemeng@huaweicloud.com \
--cc=weixugc@google.com \
--cc=willy@infradead.org \
--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