Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/5] Fix deferred_split_isolate() and clean up __folio_freeze_and_split_unmapped()
@ 2026-08-26 16:20 Kiryl Shutsemau
  2026-08-26 16:20 ` [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate() Kiryl Shutsemau
                   ` (4 more replies)
  0 siblings, 5 replies; 17+ messages in thread
From: Kiryl Shutsemau @ 2026-08-26 16:20 UTC (permalink / raw)
  To: akpm, david, ljs, hannes, usama.arif
  Cc: lance.yang, ziy, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

deferred_split_isolate() probes each queued folio with folio_try_get().
folio_try_get() failure is treated as a lost race with folio_put().

It leads to wrong results when !folio_try_get() was not caused by
folio_put(): for a frozen folio, PG_partially_mapped gets wrongfully
cleared and the folio dropped from the queue.

It came up in the review of my collapse RFC series:

  https://lore.kernel.org/all/20260824131224.73344-1-lance.yang@linux.dev/

The bug is inert in upstream code:

  - __folio_split() works around it;
  - __folio_migrate_mapping() freezes a folio it is about to replace;
  - reclaim freezes only what try_to_unmap() already unmapped.

No stable@ needed. But my collapse rework steps on it, so it is worth
fixing.

The first patch fixes deferred_split_isolate().

The second patch removes the workaround for this deferred_split_isolate()
behaviour from __folio_freeze_and_split_unmapped().

The other three patches are trivial cleanups in
__folio_freeze_and_split_unmapped() that I stumbled on while looking at
it.

Tested in a VM: split_huge_page_test, folio_split_race_test and cow pass.

Also ran a test that leaves 16 partially mapped THPs on the deferred split
queue and drives thp-deferred_split through debugfs, checking
nr_anon_partially_mapped.

Kiryl Shutsemau (Meta) (5):
  mm/huge_memory: do not touch frozen folios in deferred_split_isolate()
  mm/huge_memory: dequeue the deferred split after the split freeze
  mm/huge_memory: reduce indent level in
    __folio_freeze_and_split_unmapped()
  mm/huge_memory: fold nested ifs in __folio_freeze_and_split_unmapped()
  mm/huge_memory: turn the swapcache-with-mapping error case into an
    assert

 mm/huge_memory.c | 260 ++++++++++++++++++++---------------------------
 1 file changed, 109 insertions(+), 151 deletions(-)


base-commit: 33f61b12d297562321533c048e034b1fb21c1cf3
-- 
2.54.0



^ permalink raw reply	[flat|nested] 17+ messages in thread

* [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate()
  2026-08-26 16:20 [PATCH 0/5] Fix deferred_split_isolate() and clean up __folio_freeze_and_split_unmapped() Kiryl Shutsemau
@ 2026-08-26 16:20 ` Kiryl Shutsemau
  2026-08-26 16:45   ` Zi Yan
                     ` (3 more replies)
  2026-08-26 16:20 ` [PATCH 2/5] mm/huge_memory: dequeue the deferred split after the split freeze Kiryl Shutsemau
                   ` (3 subsequent siblings)
  4 siblings, 4 replies; 17+ messages in thread
From: Kiryl Shutsemau @ 2026-08-26 16:20 UTC (permalink / raw)
  To: akpm, david, ljs, hannes, usama.arif
  Cc: lance.yang, ziy, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

deferred_split_isolate() probes each queued folio with folio_try_get().
folio_try_get() failure is treated as a lost race with folio_put(): clear
PG_partially_mapped, correct MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, take
the folio off the queue.

The folio_put() race is the most common case for !folio_try_get(), but
it is not the only option. Another scenario is folio_ref_freeze().

A zero refcount in such cases does not mean the folio is going away.  It
means "don't touch me" and current deferred_split_isolate() doesn't
respect it. It can lead to unqueueing folios from the deferred list for
no reason:

    CPU 0                            CPU 1
    ---------------------------      ------------------------------
    freeze a mapped folio            deferred_split_scan()
      folio_ref_freeze()               folio_try_get() fails
                                       folio_clear_partially_mapped()
                                       NR_ANON_PARTIALLY_MAPPED--
                                       folio off the queue
    give up, put it back
      folio_ref_unfreeze()

The folio is still partially mapped, but it is no longer a split candidate.
Nothing queues it again until part of it is unmapped once more.

Skip the folio instead: whoever freezes the folio, owns it and owner is
responsible for its fate. It also covers the folio_put() case:
__folio_put() unqueues the folio via folio_unqueue_deferred_split().

Reported-by: Lance Yang <lance.yang@linux.dev>
Link: https://lore.kernel.org/all/20260824131224.73344-1-lance.yang@linux.dev/
Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 mm/huge_memory.c | 19 ++++---------------
 1 file changed, 4 insertions(+), 15 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index ced400f72d43..6281ed993243 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -4590,22 +4590,11 @@ static enum lru_status deferred_split_isolate(struct list_head *item,
 	struct folio *folio = container_of(item, struct folio, _deferred_list);
 	struct list_head *freeable = cb_arg;
 
-	if (folio_try_get(folio)) {
-		list_lru_isolate_move(lru, item, freeable);
-		return LRU_REMOVED;
-	}
+	/* Lost race to folio_put() or the folio is under folio_ref_freeze() */
+	if (!folio_try_get(folio))
+		return LRU_SKIP;
 
-	/*
-	 * We lost race with folio_put(). Read folio state before the
-	 * isolate: folio_unqueue_deferred_split() checks list_empty()
-	 * locklessly, so once removed the folio can be freed any time.
-	 */
-	if (folio_test_partially_mapped(folio)) {
-		folio_clear_partially_mapped(folio);
-		mod_mthp_stat(folio_order(folio),
-			      MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
-	}
-	list_lru_isolate(lru, item);
+	list_lru_isolate_move(lru, item, freeable);
 	return LRU_REMOVED;
 }
 
-- 
2.54.0



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 2/5] mm/huge_memory: dequeue the deferred split after the split freeze
  2026-08-26 16:20 [PATCH 0/5] Fix deferred_split_isolate() and clean up __folio_freeze_and_split_unmapped() Kiryl Shutsemau
  2026-08-26 16:20 ` [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate() Kiryl Shutsemau
@ 2026-08-26 16:20 ` Kiryl Shutsemau
  2026-08-26 17:10   ` Zi Yan
                     ` (2 more replies)
  2026-08-26 16:20 ` [PATCH 3/5] mm/huge_memory: reduce indent level in __folio_freeze_and_split_unmapped() Kiryl Shutsemau
                   ` (2 subsequent siblings)
  4 siblings, 3 replies; 17+ messages in thread
From: Kiryl Shutsemau @ 2026-08-26 16:20 UTC (permalink / raw)
  To: akpm, david, ljs, hannes, usama.arif
  Cc: lance.yang, ziy, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

__folio_freeze_and_split_unmapped() takes the deferred split list_lru lock
across the freeze. It is only there to stop deferred_split_scan() from
touching the folio under split.

With deferred_split_isolate() fixed, the workaround can be dropped.

Unqueue the folio after folio_ref_freeze(), the way
__folio_migrate_mapping() does: folio_unqueue_deferred_split() needs a
zero refcount and a memcg still set, and both hold there.

If the split is called from deferred_split_scan(), the unqueue is a
no-op -- the folio is already removed from the list. But
PG_partially_mapped is still set, so it has to be cleared here or
MTHP_STAT_NR_ANON_PARTIALLY_MAPPED never comes back down.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 mm/huge_memory.c | 44 +++++++++++++-------------------------------
 1 file changed, 13 insertions(+), 31 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 6281ed993243..c84e8cbc986d 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -3931,41 +3931,27 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
 	struct folio *end_folio = folio_next(folio);
 	struct folio *new_folio, *next;
 	int old_order = folio_order(folio);
-	struct list_lru_one *lru;
-	bool dequeue_deferred;
 	int ret = 0;
 
 	VM_WARN_ON_ONCE(!mapping && end);
-	/*
-	 * If this folio can be on the deferred split queue, lock out
-	 * the shrinker before freezing the ref. If the shrinker sees
-	 * a 0-ref folio, it assumes it beat folio_put() to the list
-	 * lock and must clean up the LRU state - the same dequeue we
-	 * will do below as part of the split.
-	 */
-	dequeue_deferred = folio_test_anon(folio) && old_order > 1;
-	if (dequeue_deferred) {
-		struct mem_cgroup *memcg;
 
-		rcu_read_lock();
-		memcg = folio_memcg(folio);
-		lru = list_lru_lock(&deferred_split_lru,
-				    folio_nid(folio), &memcg);
-	}
 	if (folio_ref_freeze(folio, folio_cache_ref_count(folio) + 1)) {
 		struct swap_cluster_info *ci = NULL;
 		struct lruvec *lruvec;
 
-		if (dequeue_deferred) {
-			__list_lru_del(&deferred_split_lru, lru,
-				       &folio->_deferred_list, folio_nid(folio));
-			if (folio_test_partially_mapped(folio)) {
-				folio_clear_partially_mapped(folio);
-				mod_mthp_stat(old_order,
-					MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
-			}
-			list_lru_unlock(lru);
-			rcu_read_unlock();
+		/* Take off the deferred split queue while frozen and memcg set */
+		folio_unqueue_deferred_split(folio);
+
+		/*
+		 * deferred_split_scan() takes the folio off the queue before it
+		 * splits it, so the unqueue above finds an empty list and
+		 * leaves PG_partially_mapped set.
+		 * Clear it here: the flag does not survive the split.
+		 */
+		if (folio_test_partially_mapped(folio)) {
+			folio_clear_partially_mapped(folio);
+			mod_mthp_stat(old_order,
+				      MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
 		}
 
 		if (mapping) {
@@ -4067,10 +4053,6 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
 		if (ci)
 			swap_cluster_unlock(ci);
 	} else {
-		if (dequeue_deferred) {
-			list_lru_unlock(lru);
-			rcu_read_unlock();
-		}
 		return -EAGAIN;
 	}
 
-- 
2.54.0



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 3/5] mm/huge_memory: reduce indent level in __folio_freeze_and_split_unmapped()
  2026-08-26 16:20 [PATCH 0/5] Fix deferred_split_isolate() and clean up __folio_freeze_and_split_unmapped() Kiryl Shutsemau
  2026-08-26 16:20 ` [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate() Kiryl Shutsemau
  2026-08-26 16:20 ` [PATCH 2/5] mm/huge_memory: dequeue the deferred split after the split freeze Kiryl Shutsemau
@ 2026-08-26 16:20 ` Kiryl Shutsemau
  2026-08-26 16:34   ` Zi Yan
  2026-08-26 16:21 ` [PATCH 4/5] mm/huge_memory: fold nested ifs " Kiryl Shutsemau
  2026-08-26 16:21 ` [PATCH 5/5] mm/huge_memory: turn the swapcache-with-mapping error case into an assert Kiryl Shutsemau
  4 siblings, 1 reply; 17+ messages in thread
From: Kiryl Shutsemau @ 2026-08-26 16:20 UTC (permalink / raw)
  To: akpm, david, ljs, hannes, usama.arif
  Cc: lance.yang, ziy, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

Trivial cleanup: short-circuit the function if folio_ref_freeze() fails.
It save an indent level for the whole function.

No functional change intended.

Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 mm/huge_memory.c | 233 +++++++++++++++++++++++------------------------
 1 file changed, 115 insertions(+), 118 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index c84e8cbc986d..36e90756e83b 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -3930,132 +3930,129 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
 {
 	struct folio *end_folio = folio_next(folio);
 	struct folio *new_folio, *next;
+	struct swap_cluster_info *ci = NULL;
 	int old_order = folio_order(folio);
+	struct lruvec *lruvec;
 	int ret = 0;
 
 	VM_WARN_ON_ONCE(!mapping && end);
 
-	if (folio_ref_freeze(folio, folio_cache_ref_count(folio) + 1)) {
-		struct swap_cluster_info *ci = NULL;
-		struct lruvec *lruvec;
-
-		/* Take off the deferred split queue while frozen and memcg set */
-		folio_unqueue_deferred_split(folio);
-
-		/*
-		 * deferred_split_scan() takes the folio off the queue before it
-		 * splits it, so the unqueue above finds an empty list and
-		 * leaves PG_partially_mapped set.
-		 * Clear it here: the flag does not survive the split.
-		 */
-		if (folio_test_partially_mapped(folio)) {
-			folio_clear_partially_mapped(folio);
-			mod_mthp_stat(old_order,
-				      MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
-		}
-
-		if (mapping) {
-			int nr = folio_nr_pages(folio);
-
-			if (folio_test_pmd_mappable(folio) &&
-			    new_order < HPAGE_PMD_ORDER) {
-				if (folio_test_swapbacked(folio)) {
-					lruvec_stat_mod_folio(folio,
-							NR_SHMEM_THPS, -nr);
-				} else {
-					lruvec_stat_mod_folio(folio,
-							NR_FILE_THPS, -nr);
-				}
-			}
-		}
-
-		if (folio_test_swapcache(folio)) {
-			if (mapping) {
-				VM_WARN_ON_ONCE_FOLIO(mapping, folio);
-				return -EINVAL;
-			}
-
-			ci = swap_cluster_get_and_lock(folio);
-		}
-
-		/* lock lru list/PageCompound, ref frozen by page_ref_freeze */
-		if (do_lru)
-			lruvec = folio_lruvec_lock(folio);
-
-		ret = __split_unmapped_folio(folio, new_order, split_at, xas,
-					     mapping, split_type);
-
-		/*
-		 * Unfreeze after-split folios and put them back to the right
-		 * list. @folio should be kept frozon until page cache
-		 * entries are updated with all the other after-split folios
-		 * to prevent others seeing stale page cache entries.
-		 * As a result, new_folio starts from the next folio of
-		 * @folio.
-		 */
-		for (new_folio = folio_next(folio); new_folio != end_folio;
-		     new_folio = next) {
-			unsigned long nr_pages = folio_nr_pages(new_folio);
-
-			next = folio_next(new_folio);
-
-			zone_device_private_split_cb(folio, new_folio);
-
-			folio_ref_unfreeze(new_folio,
-					   folio_cache_ref_count(new_folio) + 1);
-
-			if (do_lru)
-				lru_add_split_folio(folio, new_folio, lruvec, list);
-
-			/*
-			 * Anonymous folio with swap cache.
-			 * NOTE: shmem in swap cache is not supported yet.
-			 */
-			if (ci) {
-				__swap_cache_replace_folio(ci, folio, new_folio);
-				continue;
-			}
-
-			/* Anonymous folio without swap cache */
-			if (!mapping)
-				continue;
-
-			/* Add the new folio to the page cache. */
-			if (new_folio->index < end) {
-				__xa_store(&mapping->i_pages, new_folio->index,
-					   new_folio, 0);
-				continue;
-			}
-
-			VM_WARN_ON_ONCE(!nr_shmem_dropped);
-			/* Drop folio beyond EOF: ->index >= end */
-			if (shmem_mapping(mapping) && nr_shmem_dropped)
-				*nr_shmem_dropped += nr_pages;
-			else if (folio_test_clear_dirty(new_folio))
-				folio_account_cleaned(
-					new_folio, inode_to_wb(mapping->host));
-			__filemap_remove_folio(new_folio, NULL);
-			folio_put_refs(new_folio, nr_pages);
-		}
-
-		zone_device_private_split_cb(folio, NULL);
-		/*
-		 * Unfreeze @folio only after all page cache entries, which
-		 * used to point to it, have been updated with new folios.
-		 * Otherwise, a parallel folio_try_get() can grab @folio
-		 * and its caller can see stale page cache entries.
-		 */
-		folio_ref_unfreeze(folio, folio_cache_ref_count(folio) + 1);
-
-		if (do_lru)
-			lruvec_unlock(lruvec);
-
-		if (ci)
-			swap_cluster_unlock(ci);
-	} else {
+	if (!folio_ref_freeze(folio, folio_cache_ref_count(folio) + 1))
 		return -EAGAIN;
+
+	/* Take off the deferred split queue while frozen and memcg set */
+	folio_unqueue_deferred_split(folio);
+
+	/*
+	 * deferred_split_scan() takes the folio off the queue before it
+	 * splits it, so the unqueue above finds an empty list and
+	 * leaves PG_partially_mapped set.
+	 * Clear it here: the flag does not survive the split.
+	 */
+	if (folio_test_partially_mapped(folio)) {
+		folio_clear_partially_mapped(folio);
+		mod_mthp_stat(old_order,
+			      MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
 	}
 
+	if (mapping) {
+		int nr = folio_nr_pages(folio);
+
+		if (folio_test_pmd_mappable(folio) &&
+		    new_order < HPAGE_PMD_ORDER) {
+			if (folio_test_swapbacked(folio)) {
+				lruvec_stat_mod_folio(folio,
+						      NR_SHMEM_THPS, -nr);
+			} else {
+				lruvec_stat_mod_folio(folio,
+						      NR_FILE_THPS, -nr);
+			}
+		}
+	}
+
+	if (folio_test_swapcache(folio)) {
+		if (mapping) {
+			VM_WARN_ON_ONCE_FOLIO(mapping, folio);
+			return -EINVAL;
+		}
+
+		ci = swap_cluster_get_and_lock(folio);
+	}
+
+	/* lock lru list/PageCompound, ref frozen by page_ref_freeze */
+	if (do_lru)
+		lruvec = folio_lruvec_lock(folio);
+
+	ret = __split_unmapped_folio(folio, new_order, split_at, xas,
+				     mapping, split_type);
+
+	/*
+	 * Unfreeze after-split folios and put them back to the right
+	 * list. @folio should be kept frozon until page cache
+	 * entries are updated with all the other after-split folios
+	 * to prevent others seeing stale page cache entries.
+	 * As a result, new_folio starts from the next folio of
+	 * @folio.
+	 */
+	for (new_folio = folio_next(folio); new_folio != end_folio;
+	     new_folio = next) {
+		unsigned long nr_pages = folio_nr_pages(new_folio);
+
+		next = folio_next(new_folio);
+
+		zone_device_private_split_cb(folio, new_folio);
+
+		folio_ref_unfreeze(new_folio,
+				   folio_cache_ref_count(new_folio) + 1);
+
+		if (do_lru)
+			lru_add_split_folio(folio, new_folio, lruvec, list);
+
+		/*
+		 * Anonymous folio with swap cache.
+		 * NOTE: shmem in swap cache is not supported yet.
+		 */
+		if (ci) {
+			__swap_cache_replace_folio(ci, folio, new_folio);
+			continue;
+		}
+
+		/* Anonymous folio without swap cache */
+		if (!mapping)
+			continue;
+
+		/* Add the new folio to the page cache. */
+		if (new_folio->index < end) {
+			__xa_store(&mapping->i_pages, new_folio->index,
+				   new_folio, 0);
+			continue;
+		}
+
+		VM_WARN_ON_ONCE(!nr_shmem_dropped);
+		/* Drop folio beyond EOF: ->index >= end */
+		if (shmem_mapping(mapping) && nr_shmem_dropped)
+			*nr_shmem_dropped += nr_pages;
+		else if (folio_test_clear_dirty(new_folio))
+			folio_account_cleaned(new_folio, inode_to_wb(mapping->host));
+		__filemap_remove_folio(new_folio, NULL);
+		folio_put_refs(new_folio, nr_pages);
+	}
+
+	zone_device_private_split_cb(folio, NULL);
+	/*
+	 * Unfreeze @folio only after all page cache entries, which
+	 * used to point to it, have been updated with new folios.
+	 * Otherwise, a parallel folio_try_get() can grab @folio
+	 * and its caller can see stale page cache entries.
+	 */
+	folio_ref_unfreeze(folio, folio_cache_ref_count(folio) + 1);
+
+	if (do_lru)
+		lruvec_unlock(lruvec);
+
+	if (ci)
+		swap_cluster_unlock(ci);
+
 	return ret;
 }
 
-- 
2.54.0



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 4/5] mm/huge_memory: fold nested ifs in __folio_freeze_and_split_unmapped()
  2026-08-26 16:20 [PATCH 0/5] Fix deferred_split_isolate() and clean up __folio_freeze_and_split_unmapped() Kiryl Shutsemau
                   ` (2 preceding siblings ...)
  2026-08-26 16:20 ` [PATCH 3/5] mm/huge_memory: reduce indent level in __folio_freeze_and_split_unmapped() Kiryl Shutsemau
@ 2026-08-26 16:21 ` Kiryl Shutsemau
  2026-08-26 16:21 ` [PATCH 5/5] mm/huge_memory: turn the swapcache-with-mapping error case into an assert Kiryl Shutsemau
  4 siblings, 0 replies; 17+ messages in thread
From: Kiryl Shutsemau @ 2026-08-26 16:21 UTC (permalink / raw)
  To: akpm, david, ljs, hannes, usama.arif
  Cc: lance.yang, ziy, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

The two nested conditions guard a single statistics update.

Fold them into one condition and drop the braces from the
single-statement/single-line bodies.

No functional change intended.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 mm/huge_memory.c | 17 ++++++-----------
 1 file changed, 6 insertions(+), 11 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 36e90756e83b..001d89c57fa2 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -3955,19 +3955,14 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
 			      MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
 	}
 
-	if (mapping) {
+	if (mapping && folio_test_pmd_mappable(folio) &&
+	    new_order < HPAGE_PMD_ORDER) {
 		int nr = folio_nr_pages(folio);
 
-		if (folio_test_pmd_mappable(folio) &&
-		    new_order < HPAGE_PMD_ORDER) {
-			if (folio_test_swapbacked(folio)) {
-				lruvec_stat_mod_folio(folio,
-						      NR_SHMEM_THPS, -nr);
-			} else {
-				lruvec_stat_mod_folio(folio,
-						      NR_FILE_THPS, -nr);
-			}
-		}
+		if (folio_test_swapbacked(folio))
+			lruvec_stat_mod_folio(folio, NR_SHMEM_THPS, -nr);
+		else
+			lruvec_stat_mod_folio(folio, NR_FILE_THPS, -nr);
 	}
 
 	if (folio_test_swapcache(folio)) {
-- 
2.54.0



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 5/5] mm/huge_memory: turn the swapcache-with-mapping error case into an assert
  2026-08-26 16:20 [PATCH 0/5] Fix deferred_split_isolate() and clean up __folio_freeze_and_split_unmapped() Kiryl Shutsemau
                   ` (3 preceding siblings ...)
  2026-08-26 16:21 ` [PATCH 4/5] mm/huge_memory: fold nested ifs " Kiryl Shutsemau
@ 2026-08-26 16:21 ` Kiryl Shutsemau
  4 siblings, 0 replies; 17+ messages in thread
From: Kiryl Shutsemau @ 2026-08-26 16:21 UTC (permalink / raw)
  To: akpm, david, ljs, hannes, usama.arif
  Cc: lance.yang, ziy, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

A folio in the swapcache has no mapping.

__folio_freeze_and_split_unmapped() tests the combination after the freeze
and returns -EINVAL. It leaves the folio frozen. It is not a real
recovery.

Assert the condition with VM_WARN_ON_ONCE_FOLIO() upfront, and do not
pretend to recover from an impossible state.

PG_swapcache is protected by the folio lock and stable from the start of
the function.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 mm/huge_memory.c | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 001d89c57fa2..8fc844ba67a1 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -3936,6 +3936,7 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
 	int ret = 0;
 
 	VM_WARN_ON_ONCE(!mapping && end);
+	VM_WARN_ON_ONCE_FOLIO(mapping && folio_test_swapcache(folio), folio);
 
 	if (!folio_ref_freeze(folio, folio_cache_ref_count(folio) + 1))
 		return -EAGAIN;
@@ -3965,14 +3966,8 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
 			lruvec_stat_mod_folio(folio, NR_FILE_THPS, -nr);
 	}
 
-	if (folio_test_swapcache(folio)) {
-		if (mapping) {
-			VM_WARN_ON_ONCE_FOLIO(mapping, folio);
-			return -EINVAL;
-		}
-
+	if (folio_test_swapcache(folio))
 		ci = swap_cluster_get_and_lock(folio);
-	}
 
 	/* lock lru list/PageCompound, ref frozen by page_ref_freeze */
 	if (do_lru)
-- 
2.54.0



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* Re: [PATCH 3/5] mm/huge_memory: reduce indent level in __folio_freeze_and_split_unmapped()
  2026-08-26 16:20 ` [PATCH 3/5] mm/huge_memory: reduce indent level in __folio_freeze_and_split_unmapped() Kiryl Shutsemau
@ 2026-08-26 16:34   ` Zi Yan
  2026-08-26 16:43     ` Kiryl Shutsemau
  0 siblings, 1 reply; 17+ messages in thread
From: Zi Yan @ 2026-08-26 16:34 UTC (permalink / raw)
  To: Kiryl Shutsemau, akpm, david, ljs, hannes, usama.arif,
	Kairui Song
  Cc: lance.yang, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

On Wed Aug 26, 2026 at 12:20 PM EDT, Kiryl Shutsemau wrote:
> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
>
> Trivial cleanup: short-circuit the function if folio_ref_freeze() fails.
> It save an indent level for the whole function.
>
> No functional change intended.
>
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> ---
>  mm/huge_memory.c | 233 +++++++++++++++++++++++------------------------
>  1 file changed, 115 insertions(+), 118 deletions(-)
>

Kairui has a series that cleans up __folio_split()[1]. It seems that your
patch 3, 4, and 5 overlap with his changes (e.g., your patch 3 is the
same as his patch 3).


[1] https://lore.kernel.org/all/20260821-swap-thp-cleanup-v3-0-9b43f5163238@tencent.com/

-- 
Best Regards,
Yan, Zi



^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 3/5] mm/huge_memory: reduce indent level in __folio_freeze_and_split_unmapped()
  2026-08-26 16:34   ` Zi Yan
@ 2026-08-26 16:43     ` Kiryl Shutsemau
  0 siblings, 0 replies; 17+ messages in thread
From: Kiryl Shutsemau @ 2026-08-26 16:43 UTC (permalink / raw)
  To: Zi Yan
  Cc: akpm, david, ljs, hannes, usama.arif, Kairui Song, lance.yang,
	hughd, baolin.wang, baohua, liam, nico.pache, dev.jain,
	ryan.roberts, balbirs, linux-mm, linux-kernel

On Wed, Aug 26, 2026 at 12:34:47PM -0400, Zi Yan wrote:
> On Wed Aug 26, 2026 at 12:20 PM EDT, Kiryl Shutsemau wrote:
> > From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
> >
> > Trivial cleanup: short-circuit the function if folio_ref_freeze() fails.
> > It save an indent level for the whole function.
> >
> > No functional change intended.
> >
> > Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> > ---
> >  mm/huge_memory.c | 233 +++++++++++++++++++++++------------------------
> >  1 file changed, 115 insertions(+), 118 deletions(-)
> >
> 
> Kairui has a series that cleans up __folio_split()[1]. It seems that your
> patch 3, 4, and 5 overlap with his changes (e.g., your patch 3 is the
> same as his patch 3).

Ah, okay. Ignore 3-5 then.

I will review Kairui's patchset instead.

-- 
  Kiryl Shutsemau / Kirill A. Shutemov


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate()
  2026-08-26 16:20 ` [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate() Kiryl Shutsemau
@ 2026-08-26 16:45   ` Zi Yan
  2026-08-27 15:02   ` Johannes Weiner
                     ` (2 subsequent siblings)
  3 siblings, 0 replies; 17+ messages in thread
From: Zi Yan @ 2026-08-26 16:45 UTC (permalink / raw)
  To: Kiryl Shutsemau, akpm, david, ljs, hannes, usama.arif
  Cc: lance.yang, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

On Wed Aug 26, 2026 at 12:20 PM EDT, Kiryl Shutsemau wrote:
> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
>
> deferred_split_isolate() probes each queued folio with folio_try_get().
> folio_try_get() failure is treated as a lost race with folio_put(): clear
> PG_partially_mapped, correct MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, take
> the folio off the queue.
>
> The folio_put() race is the most common case for !folio_try_get(), but
> it is not the only option. Another scenario is folio_ref_freeze().
>
> A zero refcount in such cases does not mean the folio is going away.  It
> means "don't touch me" and current deferred_split_isolate() doesn't
> respect it. It can lead to unqueueing folios from the deferred list for
> no reason:
>
>     CPU 0                            CPU 1
>     ---------------------------      ------------------------------
>     freeze a mapped folio            deferred_split_scan()
>       folio_ref_freeze()               folio_try_get() fails
>                                        folio_clear_partially_mapped()
>                                        NR_ANON_PARTIALLY_MAPPED--
>                                        folio off the queue
>     give up, put it back
>       folio_ref_unfreeze()
>
> The folio is still partially mapped, but it is no longer a split candidate.
> Nothing queues it again until part of it is unmapped once more.
>
> Skip the folio instead: whoever freezes the folio, owns it and owner is
> responsible for its fate. It also covers the folio_put() case:
> __folio_put() unqueues the folio via folio_unqueue_deferred_split().
>
> Reported-by: Lance Yang <lance.yang@linux.dev>
> Link: https://lore.kernel.org/all/20260824131224.73344-1-lance.yang@linux.dev/

Should this be Closes: ?

> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> ---
>  mm/huge_memory.c | 19 ++++---------------
>  1 file changed, 4 insertions(+), 15 deletions(-)
>

Otherwise, LGTM.

Reviewed-by: Zi Yan <ziy@nvidia.com>



-- 
Best Regards,
Yan, Zi



^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 2/5] mm/huge_memory: dequeue the deferred split after the split freeze
  2026-08-26 16:20 ` [PATCH 2/5] mm/huge_memory: dequeue the deferred split after the split freeze Kiryl Shutsemau
@ 2026-08-26 17:10   ` Zi Yan
  2026-08-27 15:25   ` David Hildenbrand (Arm)
  2026-08-27 16:59   ` Johannes Weiner
  2 siblings, 0 replies; 17+ messages in thread
From: Zi Yan @ 2026-08-26 17:10 UTC (permalink / raw)
  To: Kiryl Shutsemau, akpm, david, ljs, hannes, usama.arif,
	Kairui Song
  Cc: lance.yang, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

On Wed Aug 26, 2026 at 12:20 PM EDT, Kiryl Shutsemau wrote:
> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
>
> __folio_freeze_and_split_unmapped() takes the deferred split list_lru lock
> across the freeze. It is only there to stop deferred_split_scan() from
> touching the folio under split.
>
> With deferred_split_isolate() fixed, the workaround can be dropped.
>
> Unqueue the folio after folio_ref_freeze(), the way
> __folio_migrate_mapping() does: folio_unqueue_deferred_split() needs a
> zero refcount and a memcg still set, and both hold there.
>
> If the split is called from deferred_split_scan(), the unqueue is a
> no-op -- the folio is already removed from the list. But
> PG_partially_mapped is still set, so it has to be cleared here or
> MTHP_STAT_NR_ANON_PARTIALLY_MAPPED never comes back down.
>
> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> ---
>  mm/huge_memory.c | 44 +++++++++++++-------------------------------
>  1 file changed, 13 insertions(+), 31 deletions(-)

+Kairui, since the change affects his cleanup series. I assume this will
be picked up sooner than Kairui's large series.

>
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index 6281ed993243..c84e8cbc986d 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -3931,41 +3931,27 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
>  	struct folio *end_folio = folio_next(folio);
>  	struct folio *new_folio, *next;
>  	int old_order = folio_order(folio);
> -	struct list_lru_one *lru;
> -	bool dequeue_deferred;
>  	int ret = 0;
>  
>  	VM_WARN_ON_ONCE(!mapping && end);
> -	/*
> -	 * If this folio can be on the deferred split queue, lock out
> -	 * the shrinker before freezing the ref. If the shrinker sees
> -	 * a 0-ref folio, it assumes it beat folio_put() to the list
> -	 * lock and must clean up the LRU state - the same dequeue we
> -	 * will do below as part of the split.
> -	 */
> -	dequeue_deferred = folio_test_anon(folio) && old_order > 1;
> -	if (dequeue_deferred) {
> -		struct mem_cgroup *memcg;
>  
> -		rcu_read_lock();
> -		memcg = folio_memcg(folio);
> -		lru = list_lru_lock(&deferred_split_lru,
> -				    folio_nid(folio), &memcg);
> -	}
>  	if (folio_ref_freeze(folio, folio_cache_ref_count(folio) + 1)) {
>  		struct swap_cluster_info *ci = NULL;
>  		struct lruvec *lruvec;
>  
> -		if (dequeue_deferred) {
> -			__list_lru_del(&deferred_split_lru, lru,
> -				       &folio->_deferred_list, folio_nid(folio));
> -			if (folio_test_partially_mapped(folio)) {
> -				folio_clear_partially_mapped(folio);
> -				mod_mthp_stat(old_order,
> -					MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
> -			}
> -			list_lru_unlock(lru);
> -			rcu_read_unlock();
> +		/* Take off the deferred split queue while frozen and memcg set */
> +		folio_unqueue_deferred_split(folio);
> +
> +		/*
> +		 * deferred_split_scan() takes the folio off the queue before it
> +		 * splits it, so the unqueue above finds an empty list and
> +		 * leaves PG_partially_mapped set.
> +		 * Clear it here: the flag does not survive the split.
> +		 */
> +		if (folio_test_partially_mapped(folio)) {
> +			folio_clear_partially_mapped(folio);
> +			mod_mthp_stat(old_order,
> +				      MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
>  		}
>  
>  		if (mapping) {
> @@ -4067,10 +4053,6 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
>  		if (ci)
>  			swap_cluster_unlock(ci);
>  	} else {
> -		if (dequeue_deferred) {
> -			list_lru_unlock(lru);
> -			rcu_read_unlock();
> -		}
>  		return -EAGAIN;
>  	}
>  

This is a great cleanup. Thanks.

Reviewed-by: Zi Yan <ziy@nvidia.com>

-- 
Best Regards,
Yan, Zi



^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate()
  2026-08-26 16:20 ` [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate() Kiryl Shutsemau
  2026-08-26 16:45   ` Zi Yan
@ 2026-08-27 15:02   ` Johannes Weiner
  2026-08-27 15:23   ` David Hildenbrand (Arm)
  2026-08-27 16:38   ` Usama Arif
  3 siblings, 0 replies; 17+ messages in thread
From: Johannes Weiner @ 2026-08-27 15:02 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: akpm, david, ljs, usama.arif, lance.yang, ziy, hughd, baolin.wang,
	baohua, liam, nico.pache, dev.jain, ryan.roberts, balbirs,
	linux-mm, linux-kernel, Kiryl Shutsemau (Meta)

On Wed, Aug 26, 2026 at 05:20:57PM +0100, Kiryl Shutsemau wrote:
> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
> 
> deferred_split_isolate() probes each queued folio with folio_try_get().
> folio_try_get() failure is treated as a lost race with folio_put(): clear
> PG_partially_mapped, correct MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, take
> the folio off the queue.
> 
> The folio_put() race is the most common case for !folio_try_get(), but
> it is not the only option. Another scenario is folio_ref_freeze().
> 
> A zero refcount in such cases does not mean the folio is going away.  It
> means "don't touch me" and current deferred_split_isolate() doesn't
> respect it. It can lead to unqueueing folios from the deferred list for
> no reason:
> 
>     CPU 0                            CPU 1
>     ---------------------------      ------------------------------
>     freeze a mapped folio            deferred_split_scan()
>       folio_ref_freeze()               folio_try_get() fails
>                                        folio_clear_partially_mapped()
>                                        NR_ANON_PARTIALLY_MAPPED--
>                                        folio off the queue
>     give up, put it back
>       folio_ref_unfreeze()
> 
> The folio is still partially mapped, but it is no longer a split candidate.
> Nothing queues it again until part of it is unmapped once more.
> 
> Skip the folio instead: whoever freezes the folio, owns it and owner is
> responsible for its fate. It also covers the folio_put() case:
> __folio_put() unqueues the folio via folio_unqueue_deferred_split().
> 
> Reported-by: Lance Yang <lance.yang@linux.dev>
> Link: https://lore.kernel.org/all/20260824131224.73344-1-lance.yang@linux.dev/
> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>

Reviewed-by: Johannes Weiner <hannes@cmpxchg.org>


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate()
  2026-08-26 16:20 ` [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate() Kiryl Shutsemau
  2026-08-26 16:45   ` Zi Yan
  2026-08-27 15:02   ` Johannes Weiner
@ 2026-08-27 15:23   ` David Hildenbrand (Arm)
  2026-08-27 15:38     ` Zi Yan
  2026-08-27 16:38   ` Usama Arif
  3 siblings, 1 reply; 17+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-27 15:23 UTC (permalink / raw)
  To: Kiryl Shutsemau, akpm, ljs, hannes, usama.arif
  Cc: lance.yang, ziy, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

On 8/26/26 18:20, Kiryl Shutsemau wrote:
> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
> 
> deferred_split_isolate() probes each queued folio with folio_try_get().
> folio_try_get() failure is treated as a lost race with folio_put(): clear
> PG_partially_mapped, correct MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, take
> the folio off the queue.
> 
> The folio_put() race is the most common case for !folio_try_get(), but
> it is not the only option. Another scenario is folio_ref_freeze().
> 
> A zero refcount in such cases does not mean the folio is going away.  It
> means "don't touch me" and current deferred_split_isolate() doesn't
> respect it. It can lead to unqueueing folios from the deferred list for
> no reason:

Yes.

> 
>     CPU 0                            CPU 1
>     ---------------------------      ------------------------------
>     freeze a mapped folio            deferred_split_scan()
>       folio_ref_freeze()               folio_try_get() fails
>                                        folio_clear_partially_mapped()
>                                        NR_ANON_PARTIALLY_MAPPED--
>                                        folio off the queue
>     give up, put it back
>       folio_ref_unfreeze()
> 
> The folio is still partially mapped, but it is no longer a split candidate.
> Nothing queues it again until part of it is unmapped once more.
> 
> Skip the folio instead: whoever freezes the folio, owns it and owner is
> responsible for its fate. It also covers the folio_put() case:
> __folio_put() unqueues the folio via folio_unqueue_deferred_split().
> 
> Reported-by: Lance Yang <lance.yang@linux.dev>
> Link: https://lore.kernel.org/all/20260824131224.73344-1-lance.yang@linux.dev/

Fixes?

> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> ---
>  mm/huge_memory.c | 19 ++++---------------
>  1 file changed, 4 insertions(+), 15 deletions(-)
> 
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index ced400f72d43..6281ed993243 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -4590,22 +4590,11 @@ static enum lru_status deferred_split_isolate(struct list_head *item,
>  	struct folio *folio = container_of(item, struct folio, _deferred_list);
>  	struct list_head *freeable = cb_arg;
>  
> -	if (folio_try_get(folio)) {
> -		list_lru_isolate_move(lru, item, freeable);
> -		return LRU_REMOVED;
> -	}
> +	/* Lost race to folio_put() or the folio is under folio_ref_freeze() */
> +	if (!folio_try_get(folio))
> +		return LRU_SKIP;
>  
> -	/*
> -	 * We lost race with folio_put(). Read folio state before the
> -	 * isolate: folio_unqueue_deferred_split() checks list_empty()
> -	 * locklessly, so once removed the folio can be freed any time.
> -	 */
> -	if (folio_test_partially_mapped(folio)) {
> -		folio_clear_partially_mapped(folio);
> -		mod_mthp_stat(folio_order(folio),
> -			      MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
> -	}
> -	list_lru_isolate(lru, item);
> +	list_lru_isolate_move(lru, item, freeable);
>  	return LRU_REMOVED;
>  }
>  

Who will clean up the stats that we used to clean up? That should be mentioned
in the commit log, otherwise it looks like some piece of the puzzle is missing.

-- 
Cheers,

David


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 2/5] mm/huge_memory: dequeue the deferred split after the split freeze
  2026-08-26 16:20 ` [PATCH 2/5] mm/huge_memory: dequeue the deferred split after the split freeze Kiryl Shutsemau
  2026-08-26 17:10   ` Zi Yan
@ 2026-08-27 15:25   ` David Hildenbrand (Arm)
  2026-08-27 16:59   ` Johannes Weiner
  2 siblings, 0 replies; 17+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-27 15:25 UTC (permalink / raw)
  To: Kiryl Shutsemau, akpm, ljs, hannes, usama.arif
  Cc: lance.yang, ziy, hughd, baolin.wang, baohua, liam, nico.pache,
	dev.jain, ryan.roberts, balbirs, linux-mm, linux-kernel,
	Kiryl Shutsemau (Meta)

On 8/26/26 18:20, Kiryl Shutsemau wrote:
> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
> 
> __folio_freeze_and_split_unmapped() takes the deferred split list_lru lock
> across the freeze. It is only there to stop deferred_split_scan() from
> touching the folio under split.
> 
> With deferred_split_isolate() fixed, the workaround can be dropped.
> 
> Unqueue the folio after folio_ref_freeze(), the way
> __folio_migrate_mapping() does: folio_unqueue_deferred_split() needs a
> zero refcount and a memcg still set, and both hold there.
> 
> If the split is called from deferred_split_scan(), the unqueue is a
> no-op -- the folio is already removed from the list. But
> PG_partially_mapped is still set, so it has to be cleared here or
> MTHP_STAT_NR_ANON_PARTIALLY_MAPPED never comes back down.
> 
> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> ---

LGTM!

Acked-by: David Hildenbrand (Arm) <david@kernel.org>

-- 
Cheers,

David


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate()
  2026-08-27 15:23   ` David Hildenbrand (Arm)
@ 2026-08-27 15:38     ` Zi Yan
  2026-08-27 15:56       ` David Hildenbrand (Arm)
  0 siblings, 1 reply; 17+ messages in thread
From: Zi Yan @ 2026-08-27 15:38 UTC (permalink / raw)
  To: David Hildenbrand (Arm)
  Cc: Kiryl Shutsemau, akpm, ljs, hannes, usama.arif, lance.yang, hughd,
	baolin.wang, baohua, liam, nico.pache, dev.jain, ryan.roberts,
	balbirs, linux-mm, linux-kernel, Kiryl Shutsemau (Meta)

On 27 Aug 2026, at 11:23, David Hildenbrand (Arm) wrote:

> On 8/26/26 18:20, Kiryl Shutsemau wrote:
>> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
>>
>> deferred_split_isolate() probes each queued folio with folio_try_get().
>> folio_try_get() failure is treated as a lost race with folio_put(): clear
>> PG_partially_mapped, correct MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, take
>> the folio off the queue.
>>
>> The folio_put() race is the most common case for !folio_try_get(), but
>> it is not the only option. Another scenario is folio_ref_freeze().
>>
>> A zero refcount in such cases does not mean the folio is going away.  It
>> means "don't touch me" and current deferred_split_isolate() doesn't
>> respect it. It can lead to unqueueing folios from the deferred list for
>> no reason:
>
> Yes.
>
>>
>>     CPU 0                            CPU 1
>>     ---------------------------      ------------------------------
>>     freeze a mapped folio            deferred_split_scan()
>>       folio_ref_freeze()               folio_try_get() fails
>>                                        folio_clear_partially_mapped()
>>                                        NR_ANON_PARTIALLY_MAPPED--
>>                                        folio off the queue
>>     give up, put it back
>>       folio_ref_unfreeze()
>>
>> The folio is still partially mapped, but it is no longer a split candidate.
>> Nothing queues it again until part of it is unmapped once more.
>>
>> Skip the folio instead: whoever freezes the folio, owns it and owner is
>> responsible for its fate. It also covers the folio_put() case:
>> __folio_put() unqueues the folio via folio_unqueue_deferred_split().
>>
>> Reported-by: Lance Yang <lance.yang@linux.dev>
>> Link: https://lore.kernel.org/all/20260824131224.73344-1-lance.yang@linux.dev/
>
> Fixes?
>
>> Assisted-by: Claude-Code:claude-opus-5
>> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
>> ---
>>  mm/huge_memory.c | 19 ++++---------------
>>  1 file changed, 4 insertions(+), 15 deletions(-)
>>
>> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
>> index ced400f72d43..6281ed993243 100644
>> --- a/mm/huge_memory.c
>> +++ b/mm/huge_memory.c
>> @@ -4590,22 +4590,11 @@ static enum lru_status deferred_split_isolate(struct list_head *item,
>>  	struct folio *folio = container_of(item, struct folio, _deferred_list);
>>  	struct list_head *freeable = cb_arg;
>>
>> -	if (folio_try_get(folio)) {
>> -		list_lru_isolate_move(lru, item, freeable);
>> -		return LRU_REMOVED;
>> -	}
>> +	/* Lost race to folio_put() or the folio is under folio_ref_freeze() */
>> +	if (!folio_try_get(folio))
>> +		return LRU_SKIP;
>>
>> -	/*
>> -	 * We lost race with folio_put(). Read folio state before the
>> -	 * isolate: folio_unqueue_deferred_split() checks list_empty()
>> -	 * locklessly, so once removed the folio can be freed any time.
>> -	 */
>> -	if (folio_test_partially_mapped(folio)) {
>> -		folio_clear_partially_mapped(folio);
>> -		mod_mthp_stat(folio_order(folio),
>> -			      MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
>> -	}
>> -	list_lru_isolate(lru, item);
>> +	list_lru_isolate_move(lru, item, freeable);
>>  	return LRU_REMOVED;
>>  }
>>
>
> Who will clean up the stats that we used to clean up? That should be mentioned
> in the commit log, otherwise it looks like some piece of the puzzle is missing.

It is kinda implied in the last paragraph in the commit log (copied below),
but I agree that stating it explicitly is better.

|> Skip the folio instead: whoever freezes the folio, owns it and owner is
|> responsible for its fate. It also covers the folio_put() case:
|> __folio_put() unqueues the folio via folio_unqueue_deferred_split().



Best Regards,
Yan, Zi


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate()
  2026-08-27 15:38     ` Zi Yan
@ 2026-08-27 15:56       ` David Hildenbrand (Arm)
  0 siblings, 0 replies; 17+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-27 15:56 UTC (permalink / raw)
  To: Zi Yan
  Cc: Kiryl Shutsemau, akpm, ljs, hannes, usama.arif, lance.yang, hughd,
	baolin.wang, baohua, liam, nico.pache, dev.jain, ryan.roberts,
	balbirs, linux-mm, linux-kernel, Kiryl Shutsemau (Meta)

On 8/27/26 17:38, Zi Yan wrote:
> On 27 Aug 2026, at 11:23, David Hildenbrand (Arm) wrote:
> 
>> On 8/26/26 18:20, Kiryl Shutsemau wrote:
>>> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
>>>
>>> deferred_split_isolate() probes each queued folio with folio_try_get().
>>> folio_try_get() failure is treated as a lost race with folio_put(): clear
>>> PG_partially_mapped, correct MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, take
>>> the folio off the queue.
>>>
>>> The folio_put() race is the most common case for !folio_try_get(), but
>>> it is not the only option. Another scenario is folio_ref_freeze().
>>>
>>> A zero refcount in such cases does not mean the folio is going away.  It
>>> means "don't touch me" and current deferred_split_isolate() doesn't
>>> respect it. It can lead to unqueueing folios from the deferred list for
>>> no reason:
>>
>> Yes.
>>
>>>
>>>     CPU 0                            CPU 1
>>>     ---------------------------      ------------------------------
>>>     freeze a mapped folio            deferred_split_scan()
>>>       folio_ref_freeze()               folio_try_get() fails
>>>                                        folio_clear_partially_mapped()
>>>                                        NR_ANON_PARTIALLY_MAPPED--
>>>                                        folio off the queue
>>>     give up, put it back
>>>       folio_ref_unfreeze()
>>>
>>> The folio is still partially mapped, but it is no longer a split candidate.
>>> Nothing queues it again until part of it is unmapped once more.
>>>
>>> Skip the folio instead: whoever freezes the folio, owns it and owner is
>>> responsible for its fate. It also covers the folio_put() case:
>>> __folio_put() unqueues the folio via folio_unqueue_deferred_split().
>>>
>>> Reported-by: Lance Yang <lance.yang@linux.dev>
>>> Link: https://lore.kernel.org/all/20260824131224.73344-1-lance.yang@linux.dev/
>>
>> Fixes?
>>
>>> Assisted-by: Claude-Code:claude-opus-5
>>> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
>>> ---
>>>  mm/huge_memory.c | 19 ++++---------------
>>>  1 file changed, 4 insertions(+), 15 deletions(-)
>>>
>>> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
>>> index ced400f72d43..6281ed993243 100644
>>> --- a/mm/huge_memory.c
>>> +++ b/mm/huge_memory.c
>>> @@ -4590,22 +4590,11 @@ static enum lru_status deferred_split_isolate(struct list_head *item,
>>>  	struct folio *folio = container_of(item, struct folio, _deferred_list);
>>>  	struct list_head *freeable = cb_arg;
>>>
>>> -	if (folio_try_get(folio)) {
>>> -		list_lru_isolate_move(lru, item, freeable);
>>> -		return LRU_REMOVED;
>>> -	}
>>> +	/* Lost race to folio_put() or the folio is under folio_ref_freeze() */
>>> +	if (!folio_try_get(folio))
>>> +		return LRU_SKIP;
>>>
>>> -	/*
>>> -	 * We lost race with folio_put(). Read folio state before the
>>> -	 * isolate: folio_unqueue_deferred_split() checks list_empty()
>>> -	 * locklessly, so once removed the folio can be freed any time.
>>> -	 */
>>> -	if (folio_test_partially_mapped(folio)) {
>>> -		folio_clear_partially_mapped(folio);
>>> -		mod_mthp_stat(folio_order(folio),
>>> -			      MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
>>> -	}
>>> -	list_lru_isolate(lru, item);
>>> +	list_lru_isolate_move(lru, item, freeable);
>>>  	return LRU_REMOVED;
>>>  }
>>>
>>
>> Who will clean up the stats that we used to clean up? That should be mentioned
>> in the commit log, otherwise it looks like some piece of the puzzle is missing.
> 
> It is kinda implied in the last paragraph in the commit log (copied below),
> but I agree that stating it explicitly is better.
> 
> |> Skip the folio instead: whoever freezes the folio, owns it and owner is
> |> responsible for its fate. It also covers the folio_put() case:
> |> __folio_put() unqueues the folio via folio_unqueue_deferred_split().

yes, I read that but was not convinced by it that all existing freezing code
would handle it correctly. So spelling that out more clearly would be best.

-- 
Cheers,

David


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate()
  2026-08-26 16:20 ` [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate() Kiryl Shutsemau
                     ` (2 preceding siblings ...)
  2026-08-27 15:23   ` David Hildenbrand (Arm)
@ 2026-08-27 16:38   ` Usama Arif
  3 siblings, 0 replies; 17+ messages in thread
From: Usama Arif @ 2026-08-27 16:38 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: Usama Arif, akpm, david, ljs, hannes, lance.yang, ziy, hughd,
	baolin.wang, baohua, liam, nico.pache, dev.jain, ryan.roberts,
	balbirs, linux-mm, linux-kernel, Kiryl Shutsemau (Meta)

On Wed, 26 Aug 2026 17:20:57 +0100 Kiryl Shutsemau <kirill@shutemov.name> wrote:

> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
> 
> deferred_split_isolate() probes each queued folio with folio_try_get().
> folio_try_get() failure is treated as a lost race with folio_put(): clear
> PG_partially_mapped, correct MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, take
> the folio off the queue.
> 
> The folio_put() race is the most common case for !folio_try_get(), but
> it is not the only option. Another scenario is folio_ref_freeze().
> 
> A zero refcount in such cases does not mean the folio is going away.  It
> means "don't touch me" and current deferred_split_isolate() doesn't
> respect it. It can lead to unqueueing folios from the deferred list for
> no reason:
> 
>     CPU 0                            CPU 1
>     ---------------------------      ------------------------------
>     freeze a mapped folio            deferred_split_scan()
>       folio_ref_freeze()               folio_try_get() fails
>                                        folio_clear_partially_mapped()
>                                        NR_ANON_PARTIALLY_MAPPED--
>                                        folio off the queue
>     give up, put it back
>       folio_ref_unfreeze()
> 
> The folio is still partially mapped, but it is no longer a split candidate.
> Nothing queues it again until part of it is unmapped once more.
> 
> Skip the folio instead: whoever freezes the folio, owns it and owner is
> responsible for its fate. It also covers the folio_put() case:
> __folio_put() unqueues the folio via folio_unqueue_deferred_split().
> 
> Reported-by: Lance Yang <lance.yang@linux.dev>
> Link: https://lore.kernel.org/all/20260824131224.73344-1-lance.yang@linux.dev/
> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> ---
>  mm/huge_memory.c | 19 ++++---------------
>  1 file changed, 4 insertions(+), 15 deletions(-)
> 
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index ced400f72d43..6281ed993243 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -4590,22 +4590,11 @@ static enum lru_status deferred_split_isolate(struct list_head *item,
>  	struct folio *folio = container_of(item, struct folio, _deferred_list);
>  	struct list_head *freeable = cb_arg;
>  
> -	if (folio_try_get(folio)) {
> -		list_lru_isolate_move(lru, item, freeable);
> -		return LRU_REMOVED;
> -	}
> +	/* Lost race to folio_put() or the folio is under folio_ref_freeze() */
> +	if (!folio_try_get(folio))
> +		return LRU_SKIP;

I think we might have a problem here for ZONE_DEVICE folios?

This assumes the final put always dequeues the folio, but ZONE_DEVICE folios
bypass the generic folio_unqueue_deferred_split() path.
With memcg disabled, this can leave a recycled folio linked on the
deferred-split list?

Should we dequeue folios in free_zone_device_folio()?
>  
> -	/*
> -	 * We lost race with folio_put(). Read folio state before the
> -	 * isolate: folio_unqueue_deferred_split() checks list_empty()
> -	 * locklessly, so once removed the folio can be freed any time.
> -	 */
> -	if (folio_test_partially_mapped(folio)) {
> -		folio_clear_partially_mapped(folio);
> -		mod_mthp_stat(folio_order(folio),
> -			      MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
> -	}
> -	list_lru_isolate(lru, item);
> +	list_lru_isolate_move(lru, item, freeable);
>  	return LRU_REMOVED;
>  }
>  
> -- 
> 2.54.0
> 
> 


^ permalink raw reply	[flat|nested] 17+ messages in thread

* Re: [PATCH 2/5] mm/huge_memory: dequeue the deferred split after the split freeze
  2026-08-26 16:20 ` [PATCH 2/5] mm/huge_memory: dequeue the deferred split after the split freeze Kiryl Shutsemau
  2026-08-26 17:10   ` Zi Yan
  2026-08-27 15:25   ` David Hildenbrand (Arm)
@ 2026-08-27 16:59   ` Johannes Weiner
  2 siblings, 0 replies; 17+ messages in thread
From: Johannes Weiner @ 2026-08-27 16:59 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: akpm, david, ljs, usama.arif, lance.yang, ziy, hughd, baolin.wang,
	baohua, liam, nico.pache, dev.jain, ryan.roberts, balbirs,
	linux-mm, linux-kernel, Kiryl Shutsemau (Meta)

On Wed, Aug 26, 2026 at 05:20:58PM +0100, Kiryl Shutsemau wrote:
> From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
> 
> __folio_freeze_and_split_unmapped() takes the deferred split list_lru lock
> across the freeze. It is only there to stop deferred_split_scan() from
> touching the folio under split.
> 
> With deferred_split_isolate() fixed, the workaround can be dropped.
> 
> Unqueue the folio after folio_ref_freeze(), the way
> __folio_migrate_mapping() does: folio_unqueue_deferred_split() needs a
> zero refcount and a memcg still set, and both hold there.
> 
> If the split is called from deferred_split_scan(), the unqueue is a
> no-op -- the folio is already removed from the list. But
> PG_partially_mapped is still set, so it has to be cleared here or
> MTHP_STAT_NR_ANON_PARTIALLY_MAPPED never comes back down.
> 
> Assisted-by: Claude-Code:claude-opus-5
> Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>

Infinitely better!

Reviewed-by: Johannes Weiner <hannes@cmpxchg.org>


^ permalink raw reply	[flat|nested] 17+ messages in thread

end of thread, other threads:[~2026-08-27 16:59 UTC | newest]

Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-26 16:20 [PATCH 0/5] Fix deferred_split_isolate() and clean up __folio_freeze_and_split_unmapped() Kiryl Shutsemau
2026-08-26 16:20 ` [PATCH 1/5] mm/huge_memory: do not touch frozen folios in deferred_split_isolate() Kiryl Shutsemau
2026-08-26 16:45   ` Zi Yan
2026-08-27 15:02   ` Johannes Weiner
2026-08-27 15:23   ` David Hildenbrand (Arm)
2026-08-27 15:38     ` Zi Yan
2026-08-27 15:56       ` David Hildenbrand (Arm)
2026-08-27 16:38   ` Usama Arif
2026-08-26 16:20 ` [PATCH 2/5] mm/huge_memory: dequeue the deferred split after the split freeze Kiryl Shutsemau
2026-08-26 17:10   ` Zi Yan
2026-08-27 15:25   ` David Hildenbrand (Arm)
2026-08-27 16:59   ` Johannes Weiner
2026-08-26 16:20 ` [PATCH 3/5] mm/huge_memory: reduce indent level in __folio_freeze_and_split_unmapped() Kiryl Shutsemau
2026-08-26 16:34   ` Zi Yan
2026-08-26 16:43     ` Kiryl Shutsemau
2026-08-26 16:21 ` [PATCH 4/5] mm/huge_memory: fold nested ifs " Kiryl Shutsemau
2026-08-26 16:21 ` [PATCH 5/5] mm/huge_memory: turn the swapcache-with-mapping error case into an assert Kiryl Shutsemau

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox