mm: fix PMD level mTHP accounting bugs
Subsystems (3)
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
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.
--- mm/memremap.c | 1 + 1 file changed, 1 insertion(+)
mm/memremap.c +1 -0
@@ -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
[email protected]
AI reviewRe: [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.
mm/memremap.c void free_zone_device_folio(struct folio *folio) · line 428
Quoted 9 lines
mm/memremap.c +1
-0@@ -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
-0for (i =0 ; i < nr; i++)__ClearPageAnonExclusive (folio_page (folio, i)); }
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
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.
--- mm/migrate.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-)
mm/migrate.c +2 -1
@@ -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
Re: [PATCH v2 2/2] mm/migrate: exclude hugetlb folios from MTHP_STAT_NR_ANON accounting
Quoted 14 lines · Nico Pache
__folio_migrate_mapping()incrementsMTHP_STAT_NR_ANONfor the destination folio whenfolio_test_anon(folio) && folio_test_large(folio)is true. However, hugetlb folios satisfy both conditions despite having a completely separate accounting system — they usehugetlb_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_ANONto be incremented on each hugetlb migration without a corresponding decrement, permanently inflating thenr_anoncounter.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")Reviewed-by Zi Yan <[email protected]>Co-developed-by David Hildenbrand <[email protected]>Signed-off-by David Hildenbrand <[email protected]>Signed-off-by Nico Pache <[email protected]>---
LGTM.
Andrew Morton
maintainerQuoted 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 infree_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 inhugetlbfs_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))
_
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 infree_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 inhugetlbfs_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))_
David Hildenbrand (Arm)
maintainerQuoted 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 infree_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
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 infree_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.ccould somehow trigger it by selectingMIGRATE_VMA_SELECT_COMPOUND?But in
migrate_device_coherent_folio()we haveWARN_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