[PATCH 8/9] mm/memory_hotplug: resize zones under their seqlock

From: Vlastimil Babka (SUSE)

Date: Tue Oct 27 2026 - 12:02:24 EST


From: Josef Bacik <josef@xxxxxxxxxxxxxx>

With CONFIG_DEBUG_VM the page allocator checks that a page which is
freed, split or allocated lies within its zone. The check reads
zone->zone_start_pfn and zone->spanned_pages under zone->span_seqlock, as
memory hotplug can change the two at any time.

Commit 27cacaad16c5 ("mm,memory_hotplug: drop unneeded locking") removed
the write side, since all resizing is serialized by the hotplug lock. That
leaves the reader without protection. When memory below the start of a
zone goes online, the start is lowered first and the size is raised
afterwards. When the lowest memory of a zone goes offline, the size is
reduced first. In between, the zone seems to end before its last pages. If
one of them goes through the allocator at that moment, it is reported as
being outside of the zone and VM_BUG_ON_PAGE() kills the kernel.

Write the two fields in a write section of the seqlock. Interrupts are
disabled as pages are freed from interrupts, too, and the reader would
wait for the writer which it has interrupted.

Fixes: 27cacaad16c5 ("mm,memory_hotplug: drop unneeded locking")
Cc: <stable@xxxxxxxxxxxxxxx>
Assisted-by: LLM
Signed-off-by: Josef Bacik <josef@xxxxxxxxxxxxxx>
Signed-off-by: David Hildenbrand (Arm) <david@xxxxxxxxxx>
Signed-off-by: Vlastimil Babka (SUSE) <vbabka@xxxxxxxxxx>
---
mm/memory_hotplug.c | 44 ++++++++++++++++++++++++++++----------------
1 file changed, 28 insertions(+), 16 deletions(-)

diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
index e773a7d54165..66f181d98ebd 100644
--- a/mm/memory_hotplug.c
+++ b/mm/memory_hotplug.c
@@ -467,6 +467,22 @@ static unsigned long find_biggest_section_pfn(int nid, struct zone *zone,
return 0;
}

+/*
+ * The page allocator checks pages against the span of their zone while the
+ * zone is resized, see page_outside_zone_boundaries(). It must not see the
+ * new value of one field together with the old value of the other one.
+ */
+static void zone_set_span(struct zone *zone, unsigned long start_pfn,
+ unsigned long spanned_pages)
+{
+ unsigned long flags;
+
+ write_seqlock_irqsave(&zone->span_seqlock, flags);
+ zone->zone_start_pfn = start_pfn;
+ zone->spanned_pages = spanned_pages;
+ write_sequnlock_irqrestore(&zone->span_seqlock, flags);
+}
+
static void shrink_zone_span(struct zone *zone, unsigned long start_pfn,
unsigned long end_pfn)
{
@@ -482,13 +498,10 @@ static void shrink_zone_span(struct zone *zone, unsigned long start_pfn,
*/
pfn = find_smallest_section_pfn(nid, zone, end_pfn,
zone_end_pfn(zone));
- if (pfn) {
- zone->spanned_pages = zone_end_pfn(zone) - pfn;
- zone->zone_start_pfn = pfn;
- } else {
- zone->zone_start_pfn = 0;
- zone->spanned_pages = 0;
- }
+ if (pfn)
+ zone_set_span(zone, pfn, zone_end_pfn(zone) - pfn);
+ else
+ zone_set_span(zone, 0, 0);
} else if (zone_end_pfn(zone) == end_pfn) {
/*
* If the section is biggest section in the zone, it need
@@ -499,11 +512,10 @@ static void shrink_zone_span(struct zone *zone, unsigned long start_pfn,
pfn = find_biggest_section_pfn(nid, zone, zone->zone_start_pfn,
start_pfn);
if (pfn)
- zone->spanned_pages = pfn - zone->zone_start_pfn + 1;
- else {
- zone->zone_start_pfn = 0;
- zone->spanned_pages = 0;
- }
+ zone_set_span(zone, zone->zone_start_pfn,
+ pfn - zone->zone_start_pfn + 1);
+ else
+ zone_set_span(zone, 0, 0);
}
}

@@ -696,12 +708,12 @@ static void online_pages_range(unsigned long start_pfn, unsigned long nr_pages)
static void __meminit resize_zone_range(struct zone *zone, unsigned long start_pfn,
unsigned long nr_pages)
{
- unsigned long old_end_pfn = zone_end_pfn(zone);
+ unsigned long end_pfn = max(start_pfn + nr_pages, zone_end_pfn(zone));

- if (zone_is_empty(zone) || start_pfn < zone->zone_start_pfn)
- zone->zone_start_pfn = start_pfn;
+ if (!zone_is_empty(zone) && start_pfn > zone->zone_start_pfn)
+ start_pfn = zone->zone_start_pfn;

- zone->spanned_pages = max(start_pfn + nr_pages, old_end_pfn) - zone->zone_start_pfn;
+ zone_set_span(zone, start_pfn, end_pfn - start_pfn);
}

static void __meminit resize_pgdat_range(struct pglist_data *pgdat, unsigned long start_pfn,

--
2.56.0