Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Nhat Pham" <nphamcs@gmail.com>,
	"Andrew Morton" <akpm@linux-foundation.org>
Cc: "Chris Li" <chrisl@kernel.org>,
	"Kairui Song" <kasong@tencent.com>,
	"Kemeng Shi" <shikemeng@huaweicloud.com>,
	"Baoquan He" <baoquan.he@linux.dev>,
	"Barry Song" <baohua@kernel.org>,
	"Youngjun Park" <youngjun.park@lge.com>,
	linux-mm@kvack.org
Subject: Re: [PATCH] mm, swap: revalidate populated clusters before scanning
Date: Tue, 20 Oct 2026 16:06:25 +0000	[thread overview]
Message-ID: <sashiko-outbox-179329@kernel.org> (raw)
In-Reply-To: <20261019160615.2348500-1-nphamcs@gmail.com>

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


  parent reply	other threads:[~2026-10-20 16:06 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
2026-10-20 17:57 ` Kairui Song
2026-10-20 20:19   ` 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-179329@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=akpm@linux-foundation.org \
    --cc=baohua@kernel.org \
    --cc=baoquan.he@linux.dev \
    --cc=chrisl@kernel.org \
    --cc=kasong@tencent.com \
    --cc=linux-mm@kvack.org \
    --cc=nphamcs@gmail.com \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=shikemeng@huaweicloud.com \
    --cc=youngjun.park@lge.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