From: sashiko-bot@kernel.org
To: "Nhat Pham" <nphamcs@gmail.com>, akpm@linux-foundation.org
Cc: chrisl@kernel.org, kasong@tencent.com, hannes@cmpxchg.org,
mhocko@kernel.org, roman.gushchin@linux.dev,
shakeel.butt@linux.dev, yosry@kernel.org, david@kernel.org,
muchun.song@linux.dev, shikemeng@huaweicloud.com,
baoquan.he@linux.dev, baohua@kernel.org, youngjun.park@lge.com,
chengming.zhou@linux.dev, ljs@kernel.org, liam@infradead.org,
vbabka@kernel.org, rppt@kernel.org, surenb@google.com,
qi.zheng@linux.dev, axelrasmussen@google.com, yuanchu@google.com,
weixugc@google.com, riel@surriel.com, gourry@gourry.net,
haowenchao22@gmail.com, corbet@lwn.net, hughd@google.com,
baolin.wang@linux.alibaba.com, tj@kernel.org, mkoutny@suse.com,
skhan@linuxfoundation.org, rdunlap@infradead.org,
kunwu.chan@linux.dev, kernel-team@meta.com, linux-mm@kvack.org
Subject: Re: [PATCH v6 10/12] mm, swap: defer memcg_table allocation for physical swap clusters
Date: Thu, 22 Oct 2026 06:19:25 +0000 [thread overview]
Message-ID: <sashiko-outbox-181084@kernel.org> (raw)
In-Reply-To: <20261021061837.1569125-1-nphamcs@gmail.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] mm, swap: infinite loop and cluster demotion on lazy memcg_table alloc failure
- [High] mm, swap: NULL ci->table deref when lazy memcg_table alloc fails in race
--
commit d992a89e7fdb8e94a85278f2c60420245b2472d5
Author: Nhat Pham <nphamcs@gmail.com>
mm, swap: defer memcg_table allocation for physical swap clusters
This patch defers allocating ci->memcg_table for physical swap clusters when
vswap is enabled until a direct-use swap slot is allocated in
alloc_swap_scan_cluster(). Clusters that only hold vswap backings skip
allocating ci->memcg_table altogether.
> diff --git a/mm/swapfile.c b/mm/swapfile.c
> index 12cd5caeaaf23..e95f2441fdd7c 100644
> --- a/mm/swapfile.c
> +++ b/mm/swapfile.c
> @@ -476,7 +476,8 @@ 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)
> +static int swap_cluster_alloc_table(struct swap_info_struct *si,
> + struct swap_cluster_info *ci, gfp_t gfp)
> {
> struct swap_table *table = NULL;
> struct folio *folio;
> @@ -497,7 +498,14 @@ static int swap_cluster_alloc_table(struct swap_cluster_info *ci, gfp_t gfp)
> return -ENOMEM;
>
> #ifdef CONFIG_MEMCG
> - if (!mem_cgroup_disabled()) {
> + /*
> + * A physical cluster under vswap may hold only vswap backings, which
> + * record their memcg on the vswap cluster's table, not this one. Such
> + * clusters defer memcg_table allocation until they hand out a direct-use
> + * slot, including one for hibernation.
> + */
> + if ((!vswap_is_enabled() || swap_is_vswap(si)) &&
> + !mem_cgroup_disabled()) {
> VM_WARN_ON_ONCE(ci->memcg_table);
> ci->memcg_table = kzalloc_obj(*ci->memcg_table, gfp);
> if (!ci->memcg_table) {
> @@ -575,8 +583,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))
> return ci;
>
> /*
> @@ -589,8 +597,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);
>
> /*
> * Back to atomic context. We might have migrated to a new CPU with a
[ ... ]
> @@ -1090,7 +1098,9 @@ static bool __swap_cluster_alloc_entries(struct swap_info_struct *si,
> /* Try use a new cluster for current CPU and allocate from it. */
> static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
> struct swap_cluster_info *ci,
> - struct folio *folio, unsigned long offset)
> + struct folio *folio,
> + unsigned long offset,
> + bool *nomem)
> {
> unsigned int next = SWAP_ENTRY_INVALID, found = SWAP_ENTRY_INVALID;
> unsigned long start = ALIGN_DOWN(offset, SWAPFILE_CLUSTER);
> @@ -1119,6 +1129,23 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
> if (!ret)
> continue;
> }
> +#ifdef CONFIG_MEMCG
> + /*
> + * Lazy-allocate memcg_table on the first direct-use slot of a
> + * physical cluster.
> + */
> + if (vswap_is_enabled() &&
> + (!folio || !folio_test_swapcache(folio)) &&
> + !mem_cgroup_disabled() && !ci->memcg_table) {
> + ci->memcg_table = kzalloc_obj(*ci->memcg_table,
> + GFP_ATOMIC | __GFP_NOWARN);
> + if (!ci->memcg_table) {
> + if (nomem)
> + *nomem = true;
> + goto out;
> + }
> + }
> +#endif
> if (!__swap_cluster_alloc_entries(si, ci, folio, offset % SWAPFILE_CLUSTER))
> break;
> found = offset;
> @@ -1128,7 +1155,15 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
> break;
> }
> out:
> - relocate_cluster(si, ci);
> + /*
> + * On a discard-capable device, relocating a cluster whose memcg_table
> + * allocation failed queues a discard for slots that were never used,
> + * which folio_alloc_phys_swap() reads as progress and retries on.
> + */
> + if (nomem && *nomem && !ci->count)
> + __free_cluster(si, ci);
> + else
> + relocate_cluster(si, ci);
[Severity: High]
Can a concurrent caller with nomem == NULL race with swap_cluster_populate()
here and free ci->table before swap_cluster_populate() finishes isolating the
cluster?
In mm/swapfile.c, when isolate_lock_cluster() isolates an empty physical
cluster from si->free_clusters and the initial atomic table allocation fails,
swap_cluster_populate() drops ci->lock and percpu_swap_cluster.lock to call
swap_cluster_alloc_table() with GFP_KERNEL:
spin_unlock(&ci->lock);
if (!(si->flags & SWP_SOLIDSTATE))
spin_unlock(&si->global_cluster_lock);
local_unlock(&percpu_swap_cluster.lock);
ret = swap_cluster_alloc_table(si, ci, __GFP_HIGH | __GFP_NOMEMALLOC |
GFP_KERNEL);
With this patch, swap_cluster_alloc_table() skips allocating ci->memcg_table
for physical clusters when vswap_is_enabled() is true, and publishes
rcu_assign_pointer(ci->table, table) while ci->lock is not held.
Before swap_cluster_populate() re-acquires ci->lock, another caller whose
cached percpu_swap_cluster.offset[order] or si->global_cluster->next[order]
still points into ci (such as swap_alloc_fast(), cluster_alloc_swap_entry(),
or swap_alloc_hibernation_slot()) can lock ci, see cluster_is_usable(ci,
order) == true and cluster_is_empty(ci) == true, and call
alloc_swap_scan_cluster(si, ci, folio, offset, NULL) with nomem == NULL for a
direct-use slot.
If the lazy kzalloc_obj(*ci->memcg_table, GFP_ATOMIC | __GFP_NOWARN) fails in
alloc_swap_scan_cluster(), it jumps to out with ci->count == 0. Because nomem
is NULL, the check (nomem && *nomem && !ci->count) is false, so it calls
relocate_cluster(si, ci) -> free_cluster(si, ci), which either frees
ci->table via __free_cluster() and moves ci back to si->free_clusters, or
queues ci on si->discard_clusters when SWP_PAGE_DISCARD is set.
When swap_cluster_populate() then re-acquires ci->lock, it only checks if
(ret) before returning ci to isolate_lock_cluster() -> alloc_swap_scan_list()
-> alloc_swap_scan_cluster() -> cluster_scan_range() -> __swap_table_get(),
dereferencing a NULL ci->table (or allocating from a cluster on
si->discard_clusters).
> swap_cluster_unlock(ci);
> if (swap_is_vswap(si)) {
> this_cpu_write(percpu_vswap_cluster.offset[order], next);
> @@ -1149,7 +1184,13 @@ static unsigned int alloc_swap_scan_list(struct swap_info_struct *si,
> bool scan_all)
> {
> unsigned int found = SWAP_ENTRY_INVALID;
> + bool nomem = false;
>
> + /*
> + * In rare cases alloc_swap_scan_cluster() can fail due to
> + * memcg_table allocation failure. Short-circuit to avoid looping
> + * over the list indefinitely.
> + */
> do {
> struct swap_cluster_info *ci = isolate_lock_cluster(si, list);
> unsigned long offset;
> @@ -1157,10 +1198,10 @@ static unsigned int alloc_swap_scan_list(struct swap_info_struct *si,
> if (!ci)
> break;
> offset = cluster_offset(si, ci);
> - found = alloc_swap_scan_cluster(si, ci, folio, offset);
> + found = alloc_swap_scan_cluster(si, ci, folio, offset, &nomem);
> if (found)
> break;
> - } while (scan_all);
> + } while (scan_all && !nomem);
>
> return found;
> }
[Severity: High]
What happens in cluster_alloc_swap_entry() and swap_alloc_slow() when the
lazy GFP_ATOMIC allocation of ci->memcg_table fails under memory pressure?
Previously, swap_cluster_populate() had a lock-dropping GFP_KERNEL fallback
when allocating ci->memcg_table, and swapon pre-allocated ci->memcg_table
with GFP_KERNEL for cluster 0 and the bad/EOF clusters. Moving physical
cluster ci->memcg_table allocation into alloc_swap_scan_cluster() under
ci->lock and percpu_swap_cluster.lock with GFP_ATOMIC | __GFP_NOWARN appears
to cause several issues when that atomic allocation fails during direct
physical swap allocation:
1. For an empty cluster isolated from si->free_clusters,
swap_cluster_populate() has just allocated ci->table (potentially dropping
locks to use GFP_KERNEL), and alloc_swap_scan_cluster() immediately calls
__free_cluster(si, ci) -> swap_cluster_free_table(ci), freeing the newly
allocated ci->table via call_rcu() and returning ci to si->free_clusters.
2. For a non-empty cluster (ci->count > 0) isolated from
si->nonfull_clusters[order] (such as cluster 0, the EOF cluster, or a
partially filled vswap-backing cluster), the check (nomem && *nomem &&
!ci->count) in alloc_swap_scan_cluster() is false, so relocate_cluster(si,
ci) demotes the cluster from si->nonfull_clusters[order] to
si->frag_clusters[ci->order].
3. In alloc_swap_scan_list(), stopping on !nomem skips any remaining clusters
on si->nonfull_clusters[order] that already have ci->memcg_table allocated,
while nomem is not propagated to cluster_alloc_swap_entry(). As a result,
cluster_alloc_swap_entry() continues to si->free_clusters,
swap_reclaim_full_clusters(), and the order-0 higher-order stealing loop over
si->frag_clusters[o] and si->nonfull_clusters[o], demoting or fragmenting
higher-order clusters.
4. When cluster_alloc_swap_entry(si, folio) returns 0 to
swap_alloc_slow(folio) for an order-0 folio (!folio_test_large(folio)),
swap_alloc_slow() does not bail out:
plist_for_each_entry_safe(si, next, &swap_avail_head, avail_list) {
/* Rotate the device and switch to a new cluster */
plist_requeue(&si->avail_list, &swap_avail_head);
spin_unlock(&swap_avail_lock);
if (get_swap_device_info(si)) {
found = cluster_alloc_swap_entry(si, folio);
put_swap_device(si);
if (found)
return swp_entry(si->type, found);
if (folio_test_large(folio))
return (swp_entry_t){};
}
Because no slots were allocated, si is not full and stays on swap_avail_head.
When two or more swap devices share the same priority on swap_avail_head,
plist_requeue(&si->avail_list, &swap_avail_head) moves si after next on each
iteration, causing swap_alloc_slow() to loop indefinitely between those
devices while holding percpu_swap_cluster.lock.
Could swap_cluster_populate() or a lock-dropping fallback in the caller
handle allocating ci->memcg_table with GFP_KERNEL, and avoid demoting
clusters or looping in swap_alloc_slow() when allocation fails?
> @@ -1181,7 +1222,8 @@ static unsigned int vswap_alloc_cluster(struct swap_info_struct *si,
[ ... ]
>
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261021060323.553433-1-nphamcs@gmail.com?part=10
next prev parent reply other threads:[~2026-10-22 6:19 UTC|newest]
Thread overview: 22+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-21 6:03 [PATCH v6 00/12] Virtual Swap Space (Swap Table Edition) Nhat Pham
2026-10-21 6:03 ` [PATCH v6 01/12] mm, swap: add virtual swap device infrastructure Nhat Pham
2026-10-21 6:03 ` [PATCH v6 02/12] mm, swap: support zswap and zero-filled swap pages as vswap backends Nhat Pham
2026-10-21 6:03 ` [PATCH v6 03/12] mm, swap: prepare the swap IO path for vswap Nhat Pham
2026-10-21 6:03 ` [PATCH v6 04/12] mm, swap: support physical swap as a vswap backend Nhat Pham
2026-10-22 6:19 ` sashiko-bot
2026-10-21 6:03 ` [PATCH v6 05/12] mm, swap: enable THP swapin for vswap entries Nhat Pham
2026-10-22 6:19 ` sashiko-bot
2026-10-21 6:03 ` [PATCH v6 06/12] mm, swap: write back vswap zswap entries to physical swap Nhat Pham
2026-10-22 6:19 ` sashiko-bot
2026-10-21 6:03 ` [PATCH v6 07/12] mm, swap: reclaim physical slots backing cache-only vswap entries Nhat Pham
2026-10-22 6:19 ` sashiko-bot
2026-10-21 6:03 ` [PATCH v6 08/12] mm, swap: only charge physical swap entries Nhat Pham
2026-10-22 6:19 ` sashiko-bot
2026-10-21 6:03 ` [PATCH v6 09/12] mm, swap: add debugfs counters for vswap Nhat Pham
2026-10-21 6:18 ` [PATCH v6 10/12] mm, swap: defer memcg_table allocation for physical swap clusters Nhat Pham
2026-10-22 6:19 ` sashiko-bot [this message]
2026-10-21 6:18 ` [PATCH v6 11/12] mm, swap: add vmstat and memcg counters for vswap Nhat Pham
2026-10-21 6:18 ` [RFC PATCH v6 12/12] mm, swap: back vswap clusters with a VM_SPARSE array Nhat Pham
2026-10-22 6:19 ` sashiko-bot
2026-10-22 2:36 ` [PATCH v6 00/12] Virtual Swap Space (Swap Table Edition) Gregory Price (Meta)
2026-10-22 17:03 ` 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-181084@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=chengming.zhou@linux.dev \
--cc=chrisl@kernel.org \
--cc=corbet@lwn.net \
--cc=david@kernel.org \
--cc=gourry@gourry.net \
--cc=hannes@cmpxchg.org \
--cc=haowenchao22@gmail.com \
--cc=hughd@google.com \
--cc=kasong@tencent.com \
--cc=kernel-team@meta.com \
--cc=kunwu.chan@linux.dev \
--cc=liam@infradead.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=mhocko@kernel.org \
--cc=mkoutny@suse.com \
--cc=muchun.song@linux.dev \
--cc=nphamcs@gmail.com \
--cc=qi.zheng@linux.dev \
--cc=rdunlap@infradead.org \
--cc=riel@surriel.com \
--cc=roman.gushchin@linux.dev \
--cc=rppt@kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
--cc=shakeel.butt@linux.dev \
--cc=shikemeng@huaweicloud.com \
--cc=skhan@linuxfoundation.org \
--cc=surenb@google.com \
--cc=tj@kernel.org \
--cc=vbabka@kernel.org \
--cc=weixugc@google.com \
--cc=yosry@kernel.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