[PATCH 1/2] mm/hugetlb: preserve source surplus accounting during demotion
From: Longlong Xia
Date: Sat Aug 22 2026 - 23:44:30 EST
From: Longlong Xia <xialonglong@xxxxxxxxxx>
demote_pool_huge_page() currently removes every source folio as a
persistent folio. A free folio can instead account for one of the
source hstate's surplus pages, for example after a vmemmap restoration
failure. Removing such a folio without adjusting surplus_huge_pages
makes the persistent count underflow, and later subtracting it from
max_huge_pages can underflow that counter as well.
Classify selected folios against the node's surplus count while holding
hugetlb_lock, and preserve that classification on rollback. Track the
number of successfully demoted persistent folios separately so only
those folios reduce the source max_huge_pages target. All successfully
demoted folios still increase the destination target because the new
destination folios are added as persistent pages.
Fixes: 8531fc6f52f5 ("hugetlb: add hugetlb demote page support")
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Longlong Xia <xialonglong@xxxxxxxxxx>
---
mm/hugetlb.c | 35 +++++++++++++++++++++++++++++++----
1 file changed, 31 insertions(+), 4 deletions(-)
diff --git a/mm/hugetlb.c b/mm/hugetlb.c
index ed26105b84de..640df58be4e5 100644
--- a/mm/hugetlb.c
+++ b/mm/hugetlb.c
@@ -3983,6 +3983,7 @@ long demote_pool_huge_page(struct hstate *src, nodemask_t *nodes_allowed,
struct hstate *dst;
long rc = 0;
long nr_demoted = 0;
+ long nr_persistent = 0;
lockdep_assert_held(&hugetlb_lock);
@@ -3995,22 +3996,40 @@ long demote_pool_huge_page(struct hstate *src, nodemask_t *nodes_allowed,
for_each_node_mask_to_free(src, nr_nodes, node, nodes_allowed) {
LIST_HEAD(list);
+ LIST_HEAD(surplus_list);
struct folio *folio, *next;
list_for_each_entry_safe(folio, next, &src->hugepage_freelists[node], lru) {
+ bool adjust_surplus;
+
if (folio_test_hwpoison(folio))
continue;
- remove_hugetlb_folio(src, folio, false);
- list_add(&folio->lru, &list);
+ /* Surplus accounting is maintained per node, not per folio. */
+ adjust_surplus = src->surplus_huge_pages_node[node] > 0;
+ remove_hugetlb_folio(src, folio, adjust_surplus);
+ list_add(&folio->lru, adjust_surplus ? &surplus_list : &list);
+ if (!adjust_surplus)
+ nr_persistent++;
if (++nr_demoted == nr_to_demote)
break;
}
+ if (list_empty(&list) && list_empty(&surplus_list))
+ continue;
+
spin_unlock_irq(&hugetlb_lock);
- rc = demote_free_hugetlb_folios(src, dst, &list);
+ if (!list_empty(&list))
+ rc = demote_free_hugetlb_folios(src, dst, &list);
+ if (!list_empty(&surplus_list)) {
+ long tmp_rc;
+
+ tmp_rc = demote_free_hugetlb_folios(src, dst, &surplus_list);
+ if (rc >= 0)
+ rc = tmp_rc;
+ }
spin_lock_irq(&hugetlb_lock);
@@ -4018,6 +4037,14 @@ long demote_pool_huge_page(struct hstate *src, nodemask_t *nodes_allowed,
list_del(&folio->lru);
add_hugetlb_folio(src, folio, false);
+ nr_demoted--;
+ nr_persistent--;
+ }
+
+ list_for_each_entry_safe(folio, next, &surplus_list, lru) {
+ list_del(&folio->lru);
+ add_hugetlb_folio(src, folio, true);
+
nr_demoted--;
}
@@ -4029,7 +4056,7 @@ long demote_pool_huge_page(struct hstate *src, nodemask_t *nodes_allowed,
* Not absolutely necessary, but for consistency update max_huge_pages
* based on pool changes for the demoted page.
*/
- src->max_huge_pages -= nr_demoted;
+ src->max_huge_pages -= nr_persistent;
dst->max_huge_pages += nr_demoted << (huge_page_order(src) - huge_page_order(dst));
if (rc < 0)
--
2.43.0