VYPR
Unrated severityNVD Advisory· Published May 27, 2026· Updated May 27, 2026

CVE-2026-46017

CVE-2026-46017

Description

In the Linux kernel, the following vulnerability has been resolved:

mm: fix deferred split queue races during migration

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.

[ziy@nvidia.com: move the comment]

Affected products

1

Patches

4
cbf75cf212ee

mm: fix deferred split queue races during migration

2 files changed · +19 15
  • mm/huge_memory.c+10 5 modified
    diff --git a/mm/huge_memory.c b/mm/huge_memory.c
    index b298cba853ab95..123d21cded1bfd 100644
    --- a/mm/huge_memory.c
    +++ b/mm/huge_memory.c
    @@ -4456,7 +4456,7 @@ retry:
     				goto next;
     		}
     		if (!folio_trylock(folio))
    -			goto next;
    +			goto requeue;
     		if (!split_folio(folio)) {
     			did_split = true;
     			if (underused)
    @@ -4465,13 +4465,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+9 10 modified
    diff --git a/mm/migrate.c b/mm/migrate.c
    index 66faf9af9dc0ba..0cb434599c30cf 100644
    --- a/mm/migrate.c
    +++ b/mm/migrate.c
    @@ -1383,6 +1383,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
    @@ -1399,15 +1408,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);
    -- 
    cgit 1.3-korg
    
    
    
3bac01168982

mm: fix deferred split queue races during migration

2 files changed · +19 15
  • mm/huge_memory.c+10 5 modified
    diff --git a/mm/huge_memory.c b/mm/huge_memory.c
    index 745eb3d0d4a786..42c983821c0311 100644
    --- a/mm/huge_memory.c
    +++ b/mm/huge_memory.c
    @@ -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+9 10 modified
    diff --git a/mm/migrate.c b/mm/migrate.c
    index 4241eb6eca00fc..76142a02192b29 100644
    --- a/mm/migrate.c
    +++ b/mm/migrate.c
    @@ -1383,6 +1383,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
    @@ -1399,15 +1408,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);
    -- 
    cgit 1.3-korg
    
    
    
3bac01168982

mm: fix deferred split queue races during migration

2 files changed · +19 15
  • mm/huge_memory.c+10 5 modified
    diff --git a/mm/huge_memory.c b/mm/huge_memory.c
    index 745eb3d0d4a786..42c983821c0311 100644
    --- a/mm/huge_memory.c
    +++ b/mm/huge_memory.c
    @@ -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+9 10 modified
    diff --git a/mm/migrate.c b/mm/migrate.c
    index 4241eb6eca00fc..76142a02192b29 100644
    --- a/mm/migrate.c
    +++ b/mm/migrate.c
    @@ -1383,6 +1383,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
    @@ -1399,15 +1408,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);
    -- 
    cgit 1.3-korg
    
    
    
cbf75cf212ee

mm: fix deferred split queue races during migration

2 files changed · +19 15
  • mm/huge_memory.c+10 5 modified
    diff --git a/mm/huge_memory.c b/mm/huge_memory.c
    index b298cba853ab95..123d21cded1bfd 100644
    --- a/mm/huge_memory.c
    +++ b/mm/huge_memory.c
    @@ -4456,7 +4456,7 @@ retry:
     				goto next;
     		}
     		if (!folio_trylock(folio))
    -			goto next;
    +			goto requeue;
     		if (!split_folio(folio)) {
     			did_split = true;
     			if (underused)
    @@ -4465,13 +4465,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+9 10 modified
    diff --git a/mm/migrate.c b/mm/migrate.c
    index 66faf9af9dc0ba..0cb434599c30cf 100644
    --- a/mm/migrate.c
    +++ b/mm/migrate.c
    @@ -1383,6 +1383,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
    @@ -1399,15 +1408,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);
    -- 
    cgit 1.3-korg
    
    
    

Vulnerability mechanics

Root cause

"Race condition in migrate_folio_move(): the destination folio was made visible via remove_migration_ptes() before being requeued on the deferred split queue, allowing a concurrent rmap-removal path to trigger a WARN in deferred_split_folio()."

Attack vector

An attacker capable of triggering memory migration (e.g., via move_pages(), cgroup migration pressure, or NUMA balancing) can race a concurrent rmap-removal operation against the migration path. In migrate_folio_move(), the old code called remove_migration_ptes() before deferred_split_folio(), making the destination folio visible to concurrent page-table walks before it was placed on the deferred split queue. A racing rmap-removal could then mark the folio partially mapped and hit the WARN assertion in deferred_split_folio() [patch_id=2660410]. Additionally, if the shrinker's deferred_split_scan() encountered a folio locked by migration, it previously skipped it entirely (goto next), which could silently drop a fully-mapped underused folio from the split queue [patch_id=2660410].

Affected code

The bug is in migrate_folio_move() in mm/migrate.c, where deferred_split_folio() was called after remove_migration_ptes() [patch_id=2660410]. The secondary issue is in deferred_split_scan() in mm/huge_memory.c, where a failed folio_trylock() caused the folio to be skipped entirely (goto next) instead of being requeued [patch_id=2660410].

What the fix does

The fix in mm/migrate.c moves the deferred_split_folio() call to before remove_migration_ptes(), so the destination folio is requeued on the deferred split queue before it becomes visible to concurrent rmap operations [patch_id=2660410]. In mm/huge_memory.c, deferred_split_scan() is changed so that when folio_trylock() fails, the code jumps to a new "requeue" label instead of "next", ensuring the folio is added back to the split queue rather than silently dropped [patch_id=2660410]. This prevents both the WARN splat and the silent loss of underused folios from the split queue.

Preconditions

  • configKernel built with CONFIG_TRANSPARENT_HUGEPAGE and CONFIG_MIGRATION enabled
  • inputTrigger memory migration (e.g., via move_pages(), NUMA balancing, or memory compaction) on a THP folio that is on the deferred split queue
  • networkNo network access required; the attack is local

Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.