Skip to contentSkip to conversation

mm: fix PMD level mTHP accounting bugs

PATCH Applied Nico Pache 5 participants 9 messages Jul 17 – Jul 22

Subsystems (3)
lore
Share & tools
lore

Nico Pache

author
#0 lore raw

v2 cover mm: fix PMD level mTHP accounting bugs

While running selftests I noticed the PMD level per-mTHP stats (nr_anon) remained elevated after each run. After further investigation I noticed this accounting error occurs for both the migration.private_anon_htlb_test and the HMM tests.

In the HMM case this is due to folio_add_new_anon_rmap() incrementing the mTHP stats, but never containing a corresponding decrement in free_zone_device_folio(). We solve this by making sure to decrement the counter when freeing device memory.

In the migration case, we are incrementing this counter without first checking whether this folio is a hugetlb folio, which relies on a separate accounting system. We solve this by adding the proper hugetlb check before incrementing this counter.

With these changes in place, the two tests no longer cause elevated PMD level accounting issues.

V2 Changes:
- add RB from Zi
- Drop unnecessary check in __folio_migrate_folio(). HugeTLB has its own migration system in hugetlbfs_migrate_folio().

V1: https://lore.kernel.org/all/[email protected]/

Nico Pache (2):
mm: decrement MTHP_STAT_NR_ANON in free_zone_device_folio() mm/migrate: exclude hugetlb folios from MTHP_STAT_NR_ANON accounting

 mm/memremap.c | 1 +
 mm/migrate.c  | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)

base-commit: e57d6e9e20b551e4910d7a6331a81775c3ad6693

Signature · 1 line
2.55.0
author
#1 lore raw

PATCH v2 1/2 mm: decrement MTHP_STAT_NR_ANON in free_zone_device_folio()

When a zone device folio is mapped as anonymous, folio_add_new_anon_rmap() increments MTHP_STAT_NR_ANON. The corresponding decrement lives in __free_pages_prepare() in page_alloc.c, but zone device folios are freed via free_zone_device_folio() which never calls __free_pages_prepare(). This causes nr_anon to remain permanently elevated after zone device folios are freed.

Add the missing mod_mthp_stat() decrement to free_zone_device_folio() so that the counter is properly balanced.

Fixes 5d65c8d758f2 ("mm: count the number of anonymous THPs per size")
---
 mm/memremap.c | 1 +
 1 file changed, 1 insertion(+)
mm/memremap.c +1 -0
index 81766d822400..accba23aef28 100644
@@ -425,6 +425,7 @@ void free_zone_device_folio(struct folio *folio)
 	mem_cgroup_uncharge(folio);
 
 	if (folio_test_anon(folio)) {
+		mod_mthp_stat(folio_order(folio), MTHP_STAT_NR_ANON, -1);
 		for (i = 0; i < nr; i++)
 			__ClearPageAnonExclusive(folio_page(folio, i));
 	}
Signature · 1 line
2.55.0
#2 lore raw

Re: [PATCH v2 1/2] mm: decrement MTHP_STAT_NR_ANON in free_zone_device_folio()

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [Critical] Large anonymous ZONE_DEVICE folios are leaked on the deferred split list when freed, causing list corruption, Use-After-Free, and permanently elevated MTHP partially-mapped stats. --

--- Patch [1]: [PATCH v2 1/2] mm: decrement MTHP_STAT_NR_ANON in free_zone_device_folio() --- commit cd3d1728ead3da706aada7ea30df9436a9ec5060
Author: Nico Pache <[email protected]>

mm: decrement MTHP_STAT_NR_ANON in free_zone_device_folio()

When a zone device folio is mapped as anonymous, folio_add_new_anon_rmap() increments MTHP_STAT_NR_ANON. The corresponding decrement lives in __free_pages_prepare() in page_alloc.c, but zone device folios are freed via free_zone_device_folio() which never calls __free_pages_prepare(). This causes nr_anon to remain permanently elevated after zone device folios are freed.

Fixes 5d65c8d758f2 ("mm: count the number of anonymous THPs per size")

mm/memremap.c void free_zone_device_folio(struct folio *folio) · line 428

Quoted 9 lines
mm/memremap.c +1 -0
index 81766d8224009..accba23aef28c 100644
@@ -425,6 +425,7 @@ void free_zone_device_folio(struct folio *folio)
 	mem_cgroup_uncharge(folio);
 
 	if (folio_test_anon(folio)) {
+		mod_mthp_stat(folio_order(folio), MTHP_STAT_NR_ANON, -1);

[Severity: Critical]
This isn't a bug introduced by this patch, but since we are handling large anonymous zone device folios here, is there a missing cleanup for the deferred split list?

When a large device coherent folio is partially unmapped, __folio_remove_rmap() can place it on the deferred split list because it is not device private:

__folio_remove_rmap() {
    ...
    if (partially_mapped && folio_test_anon(folio) &&
        !folio_test_partially_mapped(folio) &&
        !folio_is_device_private(folio))
        deferred_split_folio(folio, true);
    ...
}

Later, when the folio's refcount drops to zero, __folio_put() short-circuits directly to free_zone_device_folio() and returns, bypassing the call to folio_unqueue_deferred_split() entirely:

__folio_put() {
    ...
    if (unlikely(folio_is_zone_device(folio))) {
        free_zone_device_folio(folio);
        return;
    }
    ...
    folio_unqueue_deferred_split(folio);
    ...
}

Does free_zone_device_folio() need to call folio_unqueue_deferred_split() before freeing the folio back to the device allocator?

If the freed folio remains on the deferred split list, could the deferred split shrinker iterate over a freed or reused list node? It seems this might cause list corruption or a use-after-free panic, and leave MTHP_STAT_NR_ANON_PARTIALLY_MAPPED permanently elevated.

Quoted 3 lines
mm/memremap.c +0 -0
 		for (i = 0; i < nr; i++)
 			__ClearPageAnonExclusive(folio_page(folio, i));
 	}
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
author
#3 lore raw

PATCH v2 2/2 mm/migrate: exclude hugetlb folios from MTHP_STAT_NR_ANON accounting

In reply to Nico Pache

__folio_migrate_mapping() increments MTHP_STAT_NR_ANON for the destination folio when folio_test_anon(folio) && folio_test_large(folio) is true. However, hugetlb folios satisfy both conditions despite having a completely separate accounting system — they use hugetlb_add_anon_rmap() which does not touch mTHP stats, and their free path also bypasses the mTHP decrement in __free_pages_prepare().

This causes MTHP_STAT_NR_ANON to be incremented on each hugetlb migration without a corresponding decrement, permanently inflating the nr_anon counter.

Add a !folio_test_hugetlb() check to __folio_migrate_mapping() so that only actual mTHP folios are counted.

Fixes 5d65c8d758f2 ("mm: count the number of anonymous THPs per size")
---
 mm/migrate.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
mm/migrate.c +2 -1
index d9b23909d716..dd15a84b2a52 100644
@@ -590,7 +590,8 @@ static int __folio_migrate_mapping(struct address_space *mapping,
 		/* No turning back from here */
 		newfolio->index = folio->index;
 		newfolio->mapping = folio->mapping;
-		if (folio_test_anon(folio) && folio_test_large(folio))
+		if (folio_test_anon(folio) && folio_test_large(folio) &&
+		    !folio_test_hugetlb(folio))
 			mod_mthp_stat(folio_order(folio), MTHP_STAT_NR_ANON, 1);
 		if (folio_test_swapbacked(folio))
 			__folio_set_swapbacked(newfolio);
Signature · 1 line
2.55.0
Reviewed-by on v2 2/2
#4 lore raw

Re: [PATCH v2 2/2] mm/migrate: exclude hugetlb folios from MTHP_STAT_NR_ANON accounting

Quoted 14 lines · Nico Pache

__folio_migrate_mapping() increments MTHP_STAT_NR_ANON for the destination folio when folio_test_anon(folio) && folio_test_large(folio) is true. However, hugetlb folios satisfy both conditions despite having a completely separate accounting system — they use hugetlb_add_anon_rmap() which does not touch mTHP stats, and their free path also bypasses the mTHP decrement in __free_pages_prepare().

This causes MTHP_STAT_NR_ANON to be incremented on each hugetlb migration without a corresponding decrement, permanently inflating the nr_anon counter.

Add a !folio_test_hugetlb() check to __folio_migrate_mapping() so that only actual mTHP folios are counted.

Quoted 6 lines
Fixes 5d65c8d758f2 ("mm: count the number of anonymous THPs per size")

---

LGTM.

maintainer
#5 lore raw
In reply to Nico Pache
Quoted 10 lines · On Fri, 17 Jul 2026 00:44:58 -0600 Nico Pache <[email protected]> wrote:

While running selftests I noticed the PMD level per-mTHP stats (nr_anon) remained elevated after each run. After further investigation I noticed this accounting error occurs for both the migration.private_anon_htlb_test and the HMM tests.

In the HMM case this is due to folio_add_new_anon_rmap() incrementing the mTHP stats, but never containing a corresponding decrement in free_zone_device_folio(). We solve this by making sure to decrement the counter when freeing device memory.

Quoted 7 lines

In the migration case, we are incrementing this counter without first checking whether this folio is a hugetlb folio, which relies on a separate accounting system. We solve this by adding the proper hugetlb check before incrementing this counter.

With these changes in place, the two tests no longer cause elevated PMD level accounting issues.

Thanks, I've updated mm.git's mm-hotfixes-unstable branch to this version.

Sashiko is worried about the existing code ("list corruption or a use-after-free panic"):

	https://sashiko.dev/#/patchset/[email protected]
Quoted 5 lines

V2 Changes:
- add RB from Zi
- Drop unnecessary check in __folio_migrate_folio(). HugeTLB has its own migration system in hugetlbfs_migrate_folio().

Here's how v2 altered mm.git:

 mm/migrate.c |    3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)
mm/migrate.c renamed from mm/migrate.c~b +1 -2
@@ -624,8 +624,7 @@ static int __folio_migrate_mapping(struc
 	 */
 	newfolio->index = folio->index;
 	newfolio->mapping = folio->mapping;
-	if (folio_test_anon(folio) && folio_test_large(folio) &&
-	    !folio_test_hugetlb(folio))
+	if (folio_test_anon(folio) && folio_test_large(folio))
 		mod_mthp_stat(folio_order(folio), MTHP_STAT_NR_ANON, 1);
 	folio_ref_add(newfolio, nr); /* add cache reference */
 	if (folio_test_swapbacked(folio))

_

author
#6 lore raw
Quoted 18 lines · Andrew Morton
Quoted 15 lines · On Fri, 17 Jul 2026 00:44:58 -0600 Nico Pache <[email protected]> wrote:

While running selftests I noticed the PMD level per-mTHP stats (nr_anon) remained elevated after each run. After further investigation I noticed this accounting error occurs for both the migration.private_anon_htlb_test and the HMM tests.

In the HMM case this is due to folio_add_new_anon_rmap() incrementing the mTHP stats, but never containing a corresponding decrement in free_zone_device_folio(). We solve this by making sure to decrement the counter when freeing device memory.

In the migration case, we are incrementing this counter without first checking whether this folio is a hugetlb folio, which relies on a separate accounting system. We solve this by adding the proper hugetlb check before incrementing this counter.

Quoted 5 lines
Quoted 2 lines

With these changes in place, the two tests no longer cause elevated PMD level accounting issues.

Thanks, I've updated mm.git's mm-hotfixes-unstable branch to this version.

Thanks!

Quoted 4 lines

Sashiko is worried about the existing code ("list corruption or a use-after-free panic"):

        https://sashiko.dev/#/patchset/[email protected]

https://sashiko.dev/#/patchset/[email protected]

I dug into this, although I'm not very familiar with device memory.

It seems we have two options:

a) free_zone_device_folio() unqueue — unqueue zone device folios at free time, preventing the corruptions/use-after-free.

b) deferred_split_folio() zone-device guard — block at queue time; covers all callers; stopping the shrinker from ever touching/splitting a live device folio; makes (a) redundant for this bug, but it could still be kept as defense.

I added a few people who may be able to help.

Thanks,
-- Nico

Quoted 26 lines
Quoted 6 lines

V2 Changes:
- add RB from Zi
- Drop unnecessary check in __folio_migrate_folio(). HugeTLB has its own migration system in hugetlbfs_migrate_folio().

Here's how v2 altered mm.git:

 mm/migrate.c |    3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)
mm/migrate.c renamed from mm/migrate.c~b +1 -2
@@ -624,8 +624,7 @@ static int __folio_migrate_mapping(struc
         */
        newfolio->index = folio->index;
        newfolio->mapping = folio->mapping;
-       if (folio_test_anon(folio) && folio_test_large(folio) &&
-           !folio_test_hugetlb(folio))
+       if (folio_test_anon(folio) && folio_test_large(folio))
                mod_mthp_stat(folio_order(folio), MTHP_STAT_NR_ANON, 1);
        folio_ref_add(newfolio, nr); /* add cache reference */
        if (folio_test_swapbacked(folio))

_

#7 lore raw
Quoted 28 lines · Nico Pache
Quoted 18 lines · Andrew Morton
Quoted 15 lines · On Fri, 17 Jul 2026 00:44:58 -0600 Nico Pache <[email protected]> wrote:

While running selftests I noticed the PMD level per-mTHP stats (nr_anon) remained elevated after each run. After further investigation I noticed this accounting error occurs for both the migration.private_anon_htlb_test and the HMM tests.

In the HMM case this is due to folio_add_new_anon_rmap() incrementing the mTHP stats, but never containing a corresponding decrement in free_zone_device_folio(). We solve this by making sure to decrement the counter when freeing device memory.

In the migration case, we are incrementing this counter without first checking whether this folio is a hugetlb folio, which relies on a separate accounting system. We solve this by adding the proper hugetlb check before incrementing this counter.

Quoted 5 lines
Quoted 2 lines

With these changes in place, the two tests no longer cause elevated PMD level accounting issues.

Thanks, I've updated mm.git's mm-hotfixes-unstable branch to this version.

Thanks!

Quoted 1 line
Quoted 7 lines
Quoted 3 lines

Sashiko is worried about the existing code ("list corruption or a use-after-free panic"):

        https://sashiko.dev/#/patchset/[email protected]

https://sashiko.dev/#/patchset/[email protected]

I dug into this, although I'm not very familiar with device memory.

In general: Don't let a tool waste your time on unrelated things if it wasn't you that asked the tool for it. Someone else asked the tool to find unrelated things.

I thought we only support anonymous THP for device-private, not device-coherent.

Maybe lib/test_hmm.c could somehow trigger it by selecting MIGRATE_VMA_SELECT_COMPOUND?

But in migrate_device_coherent_folio() we have

	WARN_ON_ONCE(folio_test_large(folio));

So this would already be pretty broken and I wouldn't spend any more time on it.

Signature · 3 lines
Cheers,

David
author
#8 lore raw
Quoted 34 lines · David Hildenbrand (Arm)
Quoted 32 lines · Nico Pache
Quoted 18 lines · Andrew Morton
Quoted 15 lines · On Fri, 17 Jul 2026 00:44:58 -0600 Nico Pache <[email protected]> wrote:

While running selftests I noticed the PMD level per-mTHP stats (nr_anon) remained elevated after each run. After further investigation I noticed this accounting error occurs for both the migration.private_anon_htlb_test and the HMM tests.

In the HMM case this is due to folio_add_new_anon_rmap() incrementing the mTHP stats, but never containing a corresponding decrement in free_zone_device_folio(). We solve this by making sure to decrement the counter when freeing device memory.

In the migration case, we are incrementing this counter without first checking whether this folio is a hugetlb folio, which relies on a separate accounting system. We solve this by adding the proper hugetlb check before incrementing this counter.

Quoted 5 lines
Quoted 2 lines

With these changes in place, the two tests no longer cause elevated PMD level accounting issues.

Thanks, I've updated mm.git's mm-hotfixes-unstable branch to this version.

Thanks!

Quoted 4 lines

Sashiko is worried about the existing code ("list corruption or a use-after-free panic"):

        https://sashiko.dev/#/patchset/[email protected]
Quoted 7 lines
Quoted 3 lines

https://sashiko.dev/#/patchset/[email protected]

I dug into this, although I'm not very familiar with device memory.

In general: Don't let a tool waste your time on unrelated things if it wasn't you that asked the tool for it. Someone else asked the tool to find unrelated things.

Ok sounds good. It flagged the same issue in both of my submissions so I figured I'd investigate. It also helped me better understand the code by forcing me to dig.

Quoted 2 lines

I thought we only support anonymous THP for device-private, not device-coherent.

Device-coherent has PMD support; albiet I dont think there are many users of using the MIGRATE_VMA_SELECT_COMPOUND flag.

I think Matthew is fixing this from a different perspective. Teach the split code how to handle this.
https://lore.kernel.org/lkml/[email protected]/#t

Zi made some comments about it there.

-- Nico

Quoted 18 lines

Maybe lib/test_hmm.c could somehow trigger it by selecting MIGRATE_VMA_SELECT_COMPOUND?

But in migrate_device_coherent_folio() we have

        WARN_ON_ONCE(folio_test_large(folio));

So this would already be pretty broken and I wouldn't spend any more time on it.

--
Cheers,

David