[PATCH 5/6] mm/page_alloc: keep the range online while alloc_contig isolates it

From: Gregory Price (Meta)

Date: Tue Oct 27 2026 - 00:28:25 EST


A gigantic hugetlb allocation racing with memory offline can crash.
alloc_contig_pages() checks that a range is online under zone->lock,
drops the lock, and alloc_contig_range() then reads the zone of its
pages. If the range goes offline in between, a DEBUG_VM kernel faults
on the poisoned memmap, and removal frees it on any kernel:

CPU 1 (alloc_contig_pages) CPU 2 (offline_pages)
pfn_range_valid_contig()
range is online
spin_unlock(&zone->lock)
offline_mem_sections()
remove_pfn_range_from_zone()
page_init_poison()
alloc_contig_range()
start_isolate_page_range()
isolate_single_pageblock()
zone of a poisoned page

Oops: general protection fault, probably for non-canonical address 0xdffffc00000006a2: 0000 [#1] SMP KASAN
RIP: 0010:isolate_single_pageblock+0xe5/0x820
start_isolate_page_range+0x74/0x1a0
alloc_contig_frozen_range_noprof+0x21c/0xcc0
alloc_contig_frozen_pages_noprof+0x4cf/0x700

A range that goes offline during isolation is no better, since
start_isolate_page_range() skips offline blocks.

Check that the range is online, isolate it and check again under
pfn_walk, and look up the zone only then; an isolated range cannot go
offline. This adds one online lookup per pageblock on each side of
isolation.

With a kprobe holding a 1G allocation at start_isolate_page_range()
across the offline, base faults in 3 of 3 runs. With this patch the
offline waits for the allocation in 6 of 6, and both finish.

Fixes: 5e27a2df03b8 ("mm/page_alloc: add alloc_contig_pages()")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Gregory Price (Meta) <gourry@xxxxxxxxxx>
---
mm/page_alloc.c | 33 +++++++++++++++++++++++++++++----
1 file changed, 29 insertions(+), 4 deletions(-)

diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index bb3c7e95f2ed4..82f35816a0711 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -7319,6 +7319,16 @@ static void __free_contig_frozen_range(unsigned long pfn, unsigned long nr_pages
__free_contig_range_common(pfn, nr_pages, /* is_frozen= */ true);
}

+static bool pfn_range_online(unsigned long start, unsigned long end)
+{
+ unsigned long pfn;
+
+ for (pfn = start; pfn < end; pfn += pageblock_nr_pages)
+ if (!pfn_to_online_page(pfn))
+ return false;
+ return true;
+}
+
/**
* alloc_contig_frozen_range() -- tries to allocate given range of frozen pages
* @start: start PFN to allocate
@@ -7352,7 +7362,6 @@ int alloc_contig_frozen_range_noprof(unsigned long start, unsigned long end,
struct compact_control cc = {
.nr_migratepages = 0,
.order = -1,
- .zone = page_zone(pfn_to_page(start)),
.mode = MIGRATE_SYNC,
.ignore_skip_hint = true,
.no_set_skip_hint = true,
@@ -7397,13 +7406,29 @@ int alloc_contig_frozen_range_noprof(unsigned long start, unsigned long end,
*/

/*
- * On failure this has undone its own isolation, and any other
- * isolated blocks in the range belong to someone else, e.g. offline.
+ * Part of the range may go offline under us. Offline cannot poison
+ * the memmap while we hold pfn_walk, but isolation silently skips
+ * blocks that are already offline, so check before and after. On
+ * failure, start_isolate_page_range() has undone its own isolation,
+ * and any other isolated blocks belong to someone else, e.g. offline.
*/
- ret = start_isolate_page_range(start, end, mode);
+ scoped_guard(pfn_walk) {
+ unsigned long first = pageblock_start_pfn(start);
+ unsigned long last = pageblock_align(end);
+
+ if (!pfn_range_online(first, last))
+ return -EBUSY;
+ ret = start_isolate_page_range(start, end, mode);
+ if (!ret && !pfn_range_online(first, last)) {
+ /* Every online isolated block in the range is ours */
+ undo_isolate_page_range(start, end);
+ ret = -EBUSY;
+ }
+ }
if (ret)
return ret;

+ cc.zone = page_zone(pfn_to_page(start));
drain_all_pages(cc.zone);

/*
--
2.53.0-Meta