Skip to contentSkip to conversation

mm: fix deferred split queue races during migration

PATCH In review Lance Yang 5 participants 9 messages Apr 1 – Apr 3

Subsystems (4)
lore
Share & tools
lore

Lance Yang

author reviewer
#0 lore raw

PATCH 1/1 mm: fix deferred split queue races during migration

From: Lance Yang <[email protected]>

migrate_folio_move() records the deferred split queue state from src and replays it on dst. Replaying it after remove_migration_ptes(src, dst, 0) makes dst visible before it is requeued, so a concurrent rmap-removal path can mark dst partially mapped and trip the WARN in deferred_split_folio().

Move the requeue before remove_migration_ptes() so dst is back on the deferred split queue before it becomes visible again.

Because migration still holds dst locked at that point, teach deferred_split_scan() to requeue a folio when folio_trylock() fails. Otherwise a fully mapped underused folio can be dequeued by the shrinker and silently lost from split_queue.

---

[ Backport note ]
This patch is a follow-up fix for 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue"), which is currently only in mm-stable, and should be backported together with it.

Credit for this fix goes to David, thanks!

 mm/huge_memory.c | 12 +++++++-----
 mm/migrate.c     | 18 +++++++++---------
 2 files changed, 16 insertions(+), 14 deletions(-)
mm/huge_memory.c +7 -5
index ff9a42abd1b6..ac6d823e351f 100644
@@ -4558,7 +4558,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 				goto next;
 		}
 		if (!folio_trylock(folio))
-			goto next;
+			goto requeue;
 		if (!split_folio(folio)) {
 			did_split = true;
 			if (underused)
@@ -4569,11 +4569,13 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 next:
 		if (did_split || !folio_test_partially_mapped(folio))
 			continue;
+requeue:
 		/*
-		 * Only add back to the queue if folio is partially mapped.
-		 * If thp_underused returns false, or if split_folio fails
-		 * in the case it was underused, then consider it used and
-		 * don't add it back to split_queue.
+		 * Add back partially mapped folios, or underused folios
+		 * that we could not lock this round.  If thp_underused()
+		 * returns false, or if split_folio() succeeds, or if
+		 * split_folio() fails in the case it was underused, then
+		 * consider it used and don't add it back to split_queue.
 		 */
 		fqueue = folio_split_queue_lock_irqsave(folio, &flags);
 		if (list_empty(&folio->_deferred_list)) {
mm/migrate.c +9 -9
index 05cb408846f2..8a64291ab5b4 100644
@@ -1385,6 +1385,15 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	if (rc)
 		goto out;
 
+	/*
+	 * Requeue the destination folio on the deferred split queue if
+	 * the source was on the queue.  The source is unqueued in
+	 * __folio_migrate_mapping(), so we recorded the state from
+	 * before move_to_new_folio().
+	 */
+	if (src_deferred_split)
+		deferred_split_folio(dst, src_partially_mapped);
+
 	/*
 	 * When successful, push dst to LRU immediately: so that if it
 	 * turns out to be an mlocked page, remove_migration_ptes() will
@@ -1401,15 +1410,6 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	if (old_page_state & PAGE_WAS_MAPPED)
 		remove_migration_ptes(src, dst, 0);
 
-	/*
-	 * Requeue the destination folio on the deferred split queue if
-	 * the source was on the queue.  The source is unqueued in
-	 * __folio_migrate_mapping(), so we recorded the state from
-	 * before move_to_new_folio().
-	 */
-	if (src_deferred_split)
-		deferred_split_folio(dst, src_partially_mapped);
-
 out_unlock_both:
 	folio_unlock(dst);
 	folio_set_owner_migrate_reason(dst, reason);
Signature · 1 line
2.49.0
reviewer
#1 lore raw
Quoted 29 lines · On Wed, 1 Apr 2026 21:10:32 +0800 Lance Yang <[email protected]> wrote:

From: Lance Yang <[email protected]>

migrate_folio_move() records the deferred split queue state from src and replays it on dst. Replaying it after remove_migration_ptes(src, dst, 0) makes dst visible before it is requeued, so a concurrent rmap-removal path can mark dst partially mapped and trip the WARN in deferred_split_folio().

Move the requeue before remove_migration_ptes() so dst is back on the deferred split queue before it becomes visible again.

Because migration still holds dst locked at that point, teach deferred_split_scan() to requeue a folio when folio_trylock() fails. Otherwise a fully mapped underused folio can be dequeued by the shrinker and silently lost from split_queue.

---

[ Backport note ]
This patch is a follow-up fix for 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue"), which is currently only in mm-stable, and should be backported together with it.

Quoted 6 lines

Credit for this fix goes to David, thanks!

 mm/huge_memory.c | 12 +++++++-----
 mm/migrate.c     | 18 +++++++++---------
 2 files changed, 16 insertions(+), 14 deletions(-)

Thanks for the fix! And sorry for introducing the bug in migrate_folio_move() :)

So I am happy with the migrate_folio_move() change, it makes sense.

The goto next if folio is locked in deferred_split_scan() was actually on purpose. The reasoning was that if the folio is locked, we consider it as in use by someone and therefore we shouldnt split it. Eventhough thp_underused() does a zero-filled check, the whole point of the shrinker was to split THPs that are "not in use", and in my mind, locked folio is a folio in use. So not sure about that change..

Quoted 71 lines
mm/huge_memory.c +7 -5
index ff9a42abd1b6..ac6d823e351f 100644
@@ -4558,7 +4558,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 				goto next;
 		}
 		if (!folio_trylock(folio))
-			goto next;
+			goto requeue;
 		if (!split_folio(folio)) {
 			did_split = true;
 			if (underused)
@@ -4569,11 +4569,13 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 next:
 		if (did_split || !folio_test_partially_mapped(folio))
 			continue;
+requeue:
 		/*
-		 * Only add back to the queue if folio is partially mapped.
-		 * If thp_underused returns false, or if split_folio fails
-		 * in the case it was underused, then consider it used and
-		 * don't add it back to split_queue.
+		 * Add back partially mapped folios, or underused folios
+		 * that we could not lock this round.  If thp_underused()
+		 * returns false, or if split_folio() succeeds, or if
+		 * split_folio() fails in the case it was underused, then
+		 * consider it used and don't add it back to split_queue.
 		 */
 		fqueue = folio_split_queue_lock_irqsave(folio, &flags);
 		if (list_empty(&folio->_deferred_list)) {
mm/migrate.c +9 -9
index 05cb408846f2..8a64291ab5b4 100644
@@ -1385,6 +1385,15 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	if (rc)
 		goto out;
 
+	/*
+	 * Requeue the destination folio on the deferred split queue if
+	 * the source was on the queue.  The source is unqueued in
+	 * __folio_migrate_mapping(), so we recorded the state from
+	 * before move_to_new_folio().
+	 */
+	if (src_deferred_split)
+		deferred_split_folio(dst, src_partially_mapped);
+
 	/*
 	 * When successful, push dst to LRU immediately: so that if it
 	 * turns out to be an mlocked page, remove_migration_ptes() will
@@ -1401,15 +1410,6 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	if (old_page_state & PAGE_WAS_MAPPED)
 		remove_migration_ptes(src, dst, 0);
 
-	/*
-	 * Requeue the destination folio on the deferred split queue if
-	 * the source was on the queue.  The source is unqueued in
-	 * __folio_migrate_mapping(), so we recorded the state from
-	 * before move_to_new_folio().
-	 */
-	if (src_deferred_split)
-		deferred_split_folio(dst, src_partially_mapped);
-
 out_unlock_both:
 	folio_unlock(dst);
 	folio_set_owner_migrate_reason(dst, reason);
Signature · 1 line
2.49.0
#2 lore raw
Quoted 42 lines · Usama Arif
Quoted 29 lines · On Wed, 1 Apr 2026 21:10:32 +0800 Lance Yang <[email protected]> wrote:

From: Lance Yang <[email protected]>

migrate_folio_move() records the deferred split queue state from src and replays it on dst. Replaying it after remove_migration_ptes(src, dst, 0) makes dst visible before it is requeued, so a concurrent rmap-removal path can mark dst partially mapped and trip the WARN in deferred_split_folio().

Move the requeue before remove_migration_ptes() so dst is back on the deferred split queue before it becomes visible again.

Because migration still holds dst locked at that point, teach deferred_split_scan() to requeue a folio when folio_trylock() fails. Otherwise a fully mapped underused folio can be dequeued by the shrinker and silently lost from split_queue.

---

[ Backport note ]
This patch is a follow-up fix for 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue"), which is currently only in mm-stable, and should be backported together with it.

Quoted 6 lines

Credit for this fix goes to David, thanks!

 mm/huge_memory.c | 12 +++++++-----
 mm/migrate.c     | 18 +++++++++---------
 2 files changed, 16 insertions(+), 14 deletions(-)

Thanks for the fix! And sorry for introducing the bug in migrate_folio_move() :)

Quoted 8 lines

So I am happy with the migrate_folio_move() change, it makes sense.

The goto next if folio is locked in deferred_split_scan() was actually on purpose. The reasoning was that if the folio is locked, we consider it as in use by someone and therefore we shouldnt split it. Eventhough thp_underused() does a zero-filled check, the whole point of the shrinker was to split THPs that are "not in use", and in my mind, locked folio is a folio in use. So not sure about that change..

That is a questionable assessment. It's about checking whether folios are *underused* not, if they are used, by whoever in the system (e.g., migration).

Just take a look when anonymous folios are actually locked :)

So the original locked handling here is just bogus.

Signature · 3 lines
Cheers,

David
maintainer Acked-by on v1 1/1
#3 lore raw
In reply to Lance Yang
Quoted 15 lines · Lance Yang

From: Lance Yang <[email protected]>

migrate_folio_move() records the deferred split queue state from src and replays it on dst. Replaying it after remove_migration_ptes(src, dst, 0) makes dst visible before it is requeued, so a concurrent rmap-removal path can mark dst partially mapped and trip the WARN in deferred_split_folio().

Move the requeue before remove_migration_ptes() so dst is back on the deferred split queue before it becomes visible again.

Because migration still holds dst locked at that point, teach deferred_split_scan() to requeue a folio when folio_trylock() fails. Otherwise a fully mapped underused folio can be dequeued by the shrinker and silently lost from split_queue.

Quoted 8 lines

---

LGTM, thanks!

Signature · 3 lines
Cheers,

David
reviewer Acked-by on v1 1/1
#4 lore raw
In reply to Lance Yang
Quoted 54 lines · Lance Yang

From: Lance Yang <[email protected]>

migrate_folio_move() records the deferred split queue state from src and replays it on dst. Replaying it after remove_migration_ptes(src, dst, 0) makes dst visible before it is requeued, so a concurrent rmap-removal path can mark dst partially mapped and trip the WARN in deferred_split_folio().

Move the requeue before remove_migration_ptes() so dst is back on the deferred split queue before it becomes visible again.

Because migration still holds dst locked at that point, teach deferred_split_scan() to requeue a folio when folio_trylock() fails. Otherwise a fully mapped underused folio can be dequeued by the shrinker and silently lost from split_queue.

---

[ Backport note ]
This patch is a follow-up fix for 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue"), which is currently only in mm-stable, and should be backported together with it.

Credit for this fix goes to David, thanks!

 mm/huge_memory.c | 12 +++++++-----
 mm/migrate.c     | 18 +++++++++---------
 2 files changed, 16 insertions(+), 14 deletions(-)
mm/huge_memory.c +2 -1
index ff9a42abd1b6..ac6d823e351f 100644
@@ -4558,7 +4558,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 				goto next;
 		}
 		if (!folio_trylock(folio))
-			goto next;
+			goto requeue;
 		if (!split_folio(folio)) {
 			did_split = true;
 			if (underused)
@@ -4569,11 +4569,13 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 next:
 		if (did_split || !folio_test_partially_mapped(folio))
 			continue;
+requeue:
 		/*

mm/huge_memory.c static unsigned long deferred_split_scan(struct shrinker *shrink, · line 4579

Quoted 10 lines
mm/huge_memory.c +5 -4
-		 * Only add back to the queue if folio is partially mapped.
-		 * If thp_underused returns false, or if split_folio fails
-		 * in the case it was underused, then consider it used and
-		 * don't add it back to split_queue.
+		 * Add back partially mapped folios, or underused folios
+		 * that we could not lock this round.  If thp_underused()
+		 * returns false, or if split_folio() succeeds, or if
+		 * split_folio() fails in the case it was underused, then
+		 * consider it used and don't add it back to split_queue.
 		 */

Should the sentence
“If thp_underused() returns false, or if split_folio() succeeds, or if split_folio() fails in the case it was underused, then consider it used and don't add it back to split_queue.”
be moved to below label next?

Since “thp_underused() returns false” is describing “if (!underused) goto next”, “split_folio() succeeds” is describing “did_split == true in the if”, “split_folio() fails in the case it was underused” is describing “did_split == false and !folio_test_partially_mapped(folio) in the if”.

The first sentence matches the goto requeue for folio_trylock().

Otherwise, LGTM.

Quoted 40 lines
mm/huge_memory.c +0 -0
 		fqueue = folio_split_queue_lock_irqsave(folio, &flags);
 		if (list_empty(&folio->_deferred_list)) {
mm/migrate.c +9 -9
index 05cb408846f2..8a64291ab5b4 100644
@@ -1385,6 +1385,15 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	if (rc)
 		goto out;
 
+	/*
+	 * Requeue the destination folio on the deferred split queue if
+	 * the source was on the queue.  The source is unqueued in
+	 * __folio_migrate_mapping(), so we recorded the state from
+	 * before move_to_new_folio().
+	 */
+	if (src_deferred_split)
+		deferred_split_folio(dst, src_partially_mapped);
+
 	/*
 	 * When successful, push dst to LRU immediately: so that if it
 	 * turns out to be an mlocked page, remove_migration_ptes() will
@@ -1401,15 +1410,6 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	if (old_page_state & PAGE_WAS_MAPPED)
 		remove_migration_ptes(src, dst, 0);
 
-	/*
-	 * Requeue the destination folio on the deferred split queue if
-	 * the source was on the queue.  The source is unqueued in
-	 * __folio_migrate_mapping(), so we recorded the state from
-	 * before move_to_new_folio().
-	 */
-	if (src_deferred_split)
-		deferred_split_folio(dst, src_partially_mapped);
-
 out_unlock_both:
 	folio_unlock(dst);
 	folio_set_owner_migrate_reason(dst, reason);
Signature · 1 line
2.49.0

Best Regards,
Yan, Zi

reviewer
#5 lore raw
Quoted 73 lines · Zi Yan
Quoted 54 lines · Lance Yang

From: Lance Yang <[email protected]>

migrate_folio_move() records the deferred split queue state from src and replays it on dst. Replaying it after remove_migration_ptes(src, dst, 0) makes dst visible before it is requeued, so a concurrent rmap-removal path can mark dst partially mapped and trip the WARN in deferred_split_folio().

Move the requeue before remove_migration_ptes() so dst is back on the deferred split queue before it becomes visible again.

Because migration still holds dst locked at that point, teach deferred_split_scan() to requeue a folio when folio_trylock() fails. Otherwise a fully mapped underused folio can be dequeued by the shrinker and silently lost from split_queue.

---

[ Backport note ]
This patch is a follow-up fix for 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue"), which is currently only in mm-stable, and should be backported together with it.

Credit for this fix goes to David, thanks!

 mm/huge_memory.c | 12 +++++++-----
 mm/migrate.c     | 18 +++++++++---------
 2 files changed, 16 insertions(+), 14 deletions(-)
mm/huge_memory.c +2 -1
index ff9a42abd1b6..ac6d823e351f 100644
@@ -4558,7 +4558,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 				goto next;
 		}
 		if (!folio_trylock(folio))
-			goto next;
+			goto requeue;
 		if (!split_folio(folio)) {
 			did_split = true;
 			if (underused)
@@ -4569,11 +4569,13 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 next:
 		if (did_split || !folio_test_partially_mapped(folio))
 			continue;
+requeue:
 		/*

mm/huge_memory.c static unsigned long deferred_split_scan(struct shrinker *shrink, · line 4579

Quoted 10 lines
mm/huge_memory.c +5 -4
-		 * Only add back to the queue if folio is partially mapped.
-		 * If thp_underused returns false, or if split_folio fails
-		 * in the case it was underused, then consider it used and
-		 * don't add it back to split_queue.
+		 * Add back partially mapped folios, or underused folios
+		 * that we could not lock this round.  If thp_underused()
+		 * returns false, or if split_folio() succeeds, or if
+		 * split_folio() fails in the case it was underused, then
+		 * consider it used and don't add it back to split_queue.
 		 */

Should the sentence
“If thp_underused() returns false, or if split_folio() succeeds, or if split_folio() fails in the case it was underused, then consider it used and don't add it back to split_queue.”
be moved to below label next?

Quoted 6 lines

Since “thp_underused() returns false” is describing “if (!underused) goto next”, “split_folio() succeeds” is describing “did_split == true in the if”, “split_folio() fails in the case it was underused” is describing “did_split == false and !folio_test_partially_mapped(folio) in the if”.

The first sentence matches the goto requeue for folio_trylock().

Hi Andrew,

Can you apply the fixup below to move the comment? Lance told me he would be away for a while, so he could not send a fixup to move the comment.

Thanks.

From 6ebeca9f7215cb91905d3f49385dbbafce5a80c2 Mon Sep 17 00:00:00 2001
From: Zi Yan <[email protected]>
Date: Wed, 1 Apr 2026 18:52:43 -0400
Subject: [PATCH] move the comment.

---
 mm/huge_memory.c | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)
mm/huge_memory.c +8 -5
index ac6d823e351ff..970e077019b75 100644
@@ -4567,15 +4567,18 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 		}
 		folio_unlock(folio);
 next:
+		/*
+		 * If thp_underused() returns false, or if split_folio()
+		 * succeeds, or if split_folio() fails in the case it was
+		 * underused, then consider it used and don't add it back to
+		 * split_queue.
+		 */
 		if (did_split || !folio_test_partially_mapped(folio))
 			continue;
 requeue:
 		/*
-		 * Add back partially mapped folios, or underused folios
-		 * that we could not lock this round.  If thp_underused()
-		 * returns false, or if split_folio() succeeds, or if
-		 * split_folio() fails in the case it was underused, then
-		 * consider it used and don't add it back to split_queue.
+		 * Add back partially mapped folios, or underused folios that
+		 * we could not lock this round.
 		 */
 		fqueue = folio_split_queue_lock_irqsave(folio, &flags);
 		if (list_empty(&folio->_deferred_list)) {
Signature · 1 line
2.53.0
Quoted 41 lines

Otherwise, LGTM.

Quoted 36 lines
mm/huge_memory.c +0 -0
 		fqueue = folio_split_queue_lock_irqsave(folio, &flags);
 		if (list_empty(&folio->_deferred_list)) {
mm/migrate.c +9 -9
index 05cb408846f2..8a64291ab5b4 100644
@@ -1385,6 +1385,15 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	if (rc)
 		goto out;
 
+	/*
+	 * Requeue the destination folio on the deferred split queue if
+	 * the source was on the queue.  The source is unqueued in
+	 * __folio_migrate_mapping(), so we recorded the state from
+	 * before move_to_new_folio().
+	 */
+	if (src_deferred_split)
+		deferred_split_folio(dst, src_partially_mapped);
+
 	/*
 	 * When successful, push dst to LRU immediately: so that if it
 	 * turns out to be an mlocked page, remove_migration_ptes() will
@@ -1401,15 +1410,6 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	if (old_page_state & PAGE_WAS_MAPPED)
 		remove_migration_ptes(src, dst, 0);
 
-	/*
-	 * Requeue the destination folio on the deferred split queue if
-	 * the source was on the queue.  The source is unqueued in
-	 * __folio_migrate_mapping(), so we recorded the state from
-	 * before move_to_new_folio().
-	 */
-	if (src_deferred_split)
-		deferred_split_folio(dst, src_partially_mapped);
-
 out_unlock_both:
Quoted 8 lines
Quoted 4 lines
mm/migrate.c +0 -0
 	folio_unlock(dst);
 	folio_set_owner_migrate_reason(dst, reason);
Signature · 1 line
2.49.0

Best Regards,
Yan, Zi

Best Regards,
Yan, Zi

maintainer Acked-by David Hildenbrand (Arm) Acked-by Zi Yan
#6 lore raw
Quoted 3 lines · On Wed, 01 Apr 2026 18:55:48 -0400 Zi Yan <[email protected]> wrote:

Can you apply the fixup below to move the comment? Lance told me he would be away for a while, so he could not send a fixup to move the comment.

Thanks. I folded that into Lance's base patch so here's the whole thing:

From: Lance Yang <[email protected]>
Subject: mm: fix deferred split queue races during migration
Date: Wed, 1 Apr 2026 21:10:32 +0800

migrate_folio_move() records the deferred split queue state from src and replays it on dst. Replaying it after remove_migration_ptes(src, dst, 0) makes dst visible before it is requeued, so a concurrent rmap-removal path can mark dst partially mapped and trip the WARN in deferred_split_folio().

Move the requeue before remove_migration_ptes() so dst is back on the deferred split queue before it becomes visible again.

Because migration still holds dst locked at that point, teach deferred_split_scan() to requeue a folio when folio_trylock() fails. Otherwise a fully mapped underused folio can be dequeued by the shrinker and silently lost from split_queue.

[[email protected]: move the comment]
Fixes 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue")
Reported-by [email protected]
Cc Alistair Popple <[email protected]>
Cc Baolin Wang <[email protected]>
Cc Barry Song <[email protected]>
Cc Byungchul Park <[email protected]>
Cc David Hildenbrand <[email protected]>
Cc Deepanshu Kartikey <[email protected]>
Cc Dev Jain <[email protected]>
Cc Gregory Price <[email protected]>
Cc "Huang, Ying" <[email protected]>
Cc Joshua Hahn <[email protected]>
Cc Lance Yang <[email protected]>
Cc Liam Howlett <[email protected]>
Cc Lorenzo Stoakes (Oracle) <[email protected]>
Cc Matthew Brost <[email protected]>
Cc Nico Pache <[email protected]>
Cc Rakie Kim <[email protected]>
Cc Ryan Roberts <[email protected]>
Cc Wei Yang <[email protected]>
Cc Ying Huang <[email protected]>
Cc Usama Arif <[email protected]>

---

 mm/huge_memory.c |   15 ++++++++++-----
 mm/migrate.c     |   18 +++++++++---------
 2 files changed, 19 insertions(+), 14 deletions(-)
mm/huge_memory.c renamed from mm/huge_memory.c~mm-fix-deferred-split-queue-races-during-migration +10 -5
@@ -4542,7 +4542,7 @@ retry:
 				goto next;
 		}
 		if (!folio_trylock(folio))
-			goto next;
+			goto requeue;
 		if (!split_folio(folio)) {
 			did_split = true;
 			if (underused)
@@ -4551,13 +4551,18 @@ retry:
 		}
 		folio_unlock(folio);
 next:
+		/*
+		 * If thp_underused() returns false, or if split_folio()
+		 * succeeds, or if split_folio() fails in the case it was
+		 * underused, then consider it used and don't add it back to
+		 * split_queue.
+		 */
 		if (did_split || !folio_test_partially_mapped(folio))
 			continue;
+requeue:
 		/*
-		 * Only add back to the queue if folio is partially mapped.
-		 * If thp_underused returns false, or if split_folio fails
-		 * in the case it was underused, then consider it used and
-		 * don't add it back to split_queue.
+		 * Add back partially mapped folios, or underused folios that
+		 * we could not lock this round.
 		 */
 		fqueue = folio_split_queue_lock_irqsave(folio, &flags);
 		if (list_empty(&folio->_deferred_list)) {
mm/migrate.c renamed from mm/migrate.c~mm-fix-deferred-split-queue-races-during-migration +9 -9
@@ -1384,6 +1384,15 @@ static int migrate_folio_move(free_folio
 		goto out;
 
 	/*
+	 * Requeue the destination folio on the deferred split queue if
+	 * the source was on the queue.  The source is unqueued in
+	 * __folio_migrate_mapping(), so we recorded the state from
+	 * before move_to_new_folio().
+	 */
+	if (src_deferred_split)
+		deferred_split_folio(dst, src_partially_mapped);
+
+	/*
 	 * When successful, push dst to LRU immediately: so that if it
 	 * turns out to be an mlocked page, remove_migration_ptes() will
 	 * automatically build up the correct dst->mlock_count for it.
@@ -1399,15 +1408,6 @@ static int migrate_folio_move(free_folio
 	if (old_page_state & PAGE_WAS_MAPPED)
 		remove_migration_ptes(src, dst, 0);
 
-	/*
-	 * Requeue the destination folio on the deferred split queue if
-	 * the source was on the queue.  The source is unqueued in
-	 * __folio_migrate_mapping(), so we recorded the state from
-	 * before move_to_new_folio().
-	 */
-	if (src_deferred_split)
-		deferred_split_folio(dst, src_partially_mapped);
-
 out_unlock_both:
 	folio_unlock(dst);
 	folio_set_owner_migrate_reason(dst, reason);

_

author reviewer
#7 lore raw
Quoted 9 lines · Andrew Morton
Quoted 3 lines · On Wed, 01 Apr 2026 18:55:48 -0400 Zi Yan <[email protected]> wrote:

Can you apply the fixup below to move the comment? Lance told me he would be away for a while, so he could not send a fixup to move the comment.

Thanks. I folded that into Lance's base patch so here's the whole thing:

Thank you all!
Lance

Quoted 128 lines

From: Lance Yang <[email protected]>
Subject: mm: fix deferred split queue races during migration
Date: Wed, 1 Apr 2026 21:10:32 +0800

migrate_folio_move() records the deferred split queue state from src and replays it on dst. Replaying it after remove_migration_ptes(src, dst, 0) makes dst visible before it is requeued, so a concurrent rmap-removal path can mark dst partially mapped and trip the WARN in deferred_split_folio().

Move the requeue before remove_migration_ptes() so dst is back on the deferred split queue before it becomes visible again.

Because migration still holds dst locked at that point, teach deferred_split_scan() to requeue a folio when folio_trylock() fails. Otherwise a fully mapped underused folio can be dequeued by the shrinker and silently lost from split_queue.

[[email protected]: move the comment]
Fixes 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue")
Reported-by [email protected]
Cc Alistair Popple <[email protected]>
Cc Baolin Wang <[email protected]>
Cc Barry Song <[email protected]>
Cc Byungchul Park <[email protected]>
Cc David Hildenbrand <[email protected]>
Cc Deepanshu Kartikey <[email protected]>
Cc Dev Jain <[email protected]>
Cc Gregory Price <[email protected]>
Cc "Huang, Ying" <[email protected]>
Cc Joshua Hahn <[email protected]>
Cc Lance Yang <[email protected]>
Cc Liam Howlett <[email protected]>
Cc Lorenzo Stoakes (Oracle) <[email protected]>
Cc Matthew Brost <[email protected]>
Cc Nico Pache <[email protected]>
Cc Rakie Kim <[email protected]>
Cc Ryan Roberts <[email protected]>
Cc Wei Yang <[email protected]>
Cc Ying Huang <[email protected]>
Cc Usama Arif <[email protected]>

---

  mm/huge_memory.c |   15 ++++++++++-----
  mm/migrate.c     |   18 +++++++++---------
  2 files changed, 19 insertions(+), 14 deletions(-)
mm/huge_memory.c renamed from mm/huge_memory.c~mm-fix-deferred-split-queue-races-during-migration +10 -5
@@ -4542,7 +4542,7 @@ retry:
  				goto next;
  		}
  		if (!folio_trylock(folio))
-			goto next;
+			goto requeue;
  		if (!split_folio(folio)) {
  			did_split = true;
  			if (underused)
@@ -4551,13 +4551,18 @@ retry:
  		}
  		folio_unlock(folio);
  next:
+		/*
+		 * If thp_underused() returns false, or if split_folio()
+		 * succeeds, or if split_folio() fails in the case it was
+		 * underused, then consider it used and don't add it back to
+		 * split_queue.
+		 */
  		if (did_split || !folio_test_partially_mapped(folio))
  			continue;
+requeue:
  		/*
-		 * Only add back to the queue if folio is partially mapped.
-		 * If thp_underused returns false, or if split_folio fails
-		 * in the case it was underused, then consider it used and
-		 * don't add it back to split_queue.
+		 * Add back partially mapped folios, or underused folios that
+		 * we could not lock this round.
  		 */
  		fqueue = folio_split_queue_lock_irqsave(folio, &flags);
  		if (list_empty(&folio->_deferred_list)) {
mm/migrate.c renamed from mm/migrate.c~mm-fix-deferred-split-queue-races-during-migration +9 -9
@@ -1384,6 +1384,15 @@ static int migrate_folio_move(free_folio
  		goto out;
 
  	/*
+	 * Requeue the destination folio on the deferred split queue if
+	 * the source was on the queue.  The source is unqueued in
+	 * __folio_migrate_mapping(), so we recorded the state from
+	 * before move_to_new_folio().
+	 */
+	if (src_deferred_split)
+		deferred_split_folio(dst, src_partially_mapped);
+
+	/*
  	 * When successful, push dst to LRU immediately: so that if it
  	 * turns out to be an mlocked page, remove_migration_ptes() will
  	 * automatically build up the correct dst->mlock_count for it.
@@ -1399,15 +1408,6 @@ static int migrate_folio_move(free_folio
  	if (old_page_state & PAGE_WAS_MAPPED)
  		remove_migration_ptes(src, dst, 0);
 
-	/*
-	 * Requeue the destination folio on the deferred split queue if
-	 * the source was on the queue.  The source is unqueued in
-	 * __folio_migrate_mapping(), so we recorded the state from
-	 * before move_to_new_folio().
-	 */
-	if (src_deferred_split)
-		deferred_split_folio(dst, src_partially_mapped);
-
  out_unlock_both:
  	folio_unlock(dst);
  	folio_set_owner_migrate_reason(dst, reason);

_

maintainer
#8 lore raw
In reply to Lance Yang
Quoted 7 lines · On Wed, 1 Apr 2026 21:10:32 +0800 Lance Yang <[email protected]> wrote:

From: Lance Yang <[email protected]>

migrate_folio_move() records the deferred split queue state from src and replays it on dst. Replaying it after remove_migration_ptes(src, dst, 0) makes dst visible before it is requeued, so a concurrent rmap-removal path can mark dst partially mapped and trip the WARN in deferred_split_folio().

Quoted 7 lines

Move the requeue before remove_migration_ptes() so dst is back on the deferred split queue before it becomes visible again.

Because migration still holds dst locked at that point, teach deferred_split_scan() to requeue a folio when folio_trylock() fails. Otherwise a fully mapped underused folio can be dequeued by the shrinker and silently lost from split_queue.

Thanks.

Quoted 7 lines

I'll add this to mm-unstable with a plan to move it into the current mm-stable batch in a few days. So that 8a8ca142a488 and this follow-up fix stay in the same bundle.

Quoted 4 lines

[ Backport note ]
This patch is a follow-up fix for 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue"), which is currently only in mm-stable, and should be backported together with it.

As far as I understand it, this should happen automatically. 8a8ca142a488 has cc:stable, this patch has Fixes:8a8ca142a488 and also cc:stable.

There's enough info here for the -stable people to figure it out!