[RFC PATCH 19/45] mm: page_alloc: prevent atomic allocations from tainting clean SPBs

Rik van Riel
2026-04-30T20:22:50Z

Non-DIRECT_RECLAIM (atomic) allocations that fail with ALLOC_NOFRAGMENT
previously dropped the flag entirely and retried, allowing them to taint
clean superpageblocks. This was the primary source of taint spreading
observed on production systems.

Two changes to keep atomic allocations within tainted SPBs:

1. Extend Pass 2 in __rmqueue_smallest with a sub-pageblock phase (Pass
2b). The original Pass 2 only finds whole free pageblocks (>= pageblock
order) in tainted SPBs. Pass 2b searches for sub-pageblock-order free
blocks and uses try_to_claim_block to claim the pageblock if it has
enough compatible pages. This finds pages in tainted SPBs that have
fragmented free space but no whole free pageblocks.

2. Add ALLOC_NOFRAG_TAINTED_OK intermediate flag. Instead of going
directly from ALLOC_NOFRAGMENT to no protection, atomic allocations
first try with ALLOC_NOFRAG_TAINTED_OK which allows __rmqueue_steal
to search tainted SPBs only. Clean/empty SPBs remain protected. Only
if steal from tainted SPBs also fails is ALLOC_NOFRAGMENT fully
dropped as a last resort.

Signed-off-by: Rik van Riel <riel@surriel.com>
Assisted-by: Claude:claude-opus-4.7 syzkaller
---
mm/internal.h | 1 +
mm/page_alloc.c | 87 +++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 81 insertions(+), 7 deletions(-)

diff --git a/mm/internal.h b/mm/internal.h
index 02f1c7d36b85..f641795688af 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1413,6 +1413,7 @@ unsigned int reclaim_clean_pages_from_list(struct zone *zone,
#define ALLOC_HIGHATOMIC 0x200 /* Allows access to MIGRATE_HIGHATOMIC */
#define ALLOC_TRYLOCK 0x400 /* Only use spin_trylock in allocation path */
#define ALLOC_KSWAPD 0x800 /* allow waking of kswapd, __GFP_KSWAPD_RECLAIM set */
+#define ALLOC_NOFRAG_TAINTED_OK 0x1000 /* NOFRAGMENT, but allow steal from tainted SPBs */

/* Flags that allow allocations below the min watermark. */
#define ALLOC_RESERVES (ALLOC_NON_BLOCK|ALLOC_MIN_RESERVE|ALLOC_HIGHATOMIC|ALLOC_OOM)
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index 8ce96db50c2f..13bc57592cd5 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -2713,6 +2713,9 @@ static struct page *__rmqueue_from_sb(struct zone *zone, unsigned int order,
*/
static struct page *claim_whole_block(struct zone *zone, struct page *page,
int current_order, int order, int new_type, int old_type);
+static struct page *try_to_claim_block(struct zone *zone, struct page *page,
+ int current_order, int order, int start_type,
+ int block_type, unsigned int alloc_flags);

static __always_inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
@@ -2782,6 +2785,11 @@ struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
* free list (reset by mark_pageblock_free), so the search above
* misses them. Claim them inline to keep non-movable allocations
* concentrated in already-tainted superpageblocks.
+ *
+ * Try whole pageblock orders first (preferred for PCP buddy optimization),
+ * then fall back to sub-pageblock orders. Sub-pageblock claiming uses
+ * try_to_claim_block which checks whether the pageblock has enough
+ * compatible pages to justify claiming it.
*/
if (!movable && !is_migrate_cma(migratetype)) {
for (full = SB_FULL; full < __NR_SB_FULLNESS; full++) {
@@ -2814,6 +2822,43 @@ struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
}
}
}
+ /* Pass 2b: sub-pageblock orders in tainted SPBs */
+ for (full = SB_FULL; full < __NR_SB_FULLNESS; full++) {
+ list_for_each_entry(sb,
+ &zone->spb_lists[SB_TAINTED][full], list) {
+ int co;
+
+ if (!sb->nr_free_pages)
+ continue;
+ for (co = min_t(int, pageblock_order - 1,
+ NR_PAGE_ORDERS - 1);
+ co >= (int)order;
+ --co) {
+ current_order = co;
+ area = &sb->free_area[current_order];
+ page = get_page_from_free_area(
+ area, MIGRATE_MOVABLE);
+ if (!page)
+ continue;
+ if (get_pageblock_isolate(page))
+ continue;
+ if (is_migrate_cma(
+ get_pageblock_migratetype(page)))
+ continue;
+ page = try_to_claim_block(zone, page,
+ current_order, order,
+ migratetype, MIGRATE_MOVABLE,
+ 0);
+ if (!page)
+ continue;
+ trace_mm_page_alloc_zone_locked(
+ page, order, migratetype,
+ pcp_allowed_order(order) &&
+ migratetype < MIGRATE_PCPTYPES);
+ return page;
+ }
+ }
+ }
}

/* Empty superpageblocks: try before falling back to non-preferred category */
@@ -3566,12 +3611,23 @@ __rmqueue_claim(struct zone *zone, int order, int start_migratetype,
* the block as its current migratetype, potentially causing fragmentation.
*/
static __always_inline struct page *
-__rmqueue_steal(struct zone *zone, int order, int start_migratetype)
+__rmqueue_steal(struct zone *zone, int order, int start_migratetype,
+ unsigned int alloc_flags)
{
struct superpageblock *sb;
int current_order;
struct page *page;
int fallback_mt;
+ unsigned int search_cats;
+
+ /*
+ * When ALLOC_NOFRAG_TAINTED_OK is set, only steal from tainted
+ * SPBs to avoid tainting clean ones. Otherwise search all categories.
+ */
+ if (alloc_flags & ALLOC_NOFRAG_TAINTED_OK)
+ search_cats = SB_SEARCH_PREFERRED;
+ else
+ search_cats = SB_SEARCH_PREFERRED | SB_SEARCH_FALLBACK;

/*
* Search per-superpageblock free lists for fallback migratetypes.
@@ -3581,7 +3637,7 @@ __rmqueue_steal(struct zone *zone, int order, int start_migratetype)
page = __rmqueue_sb_find_fallback(zone, current_order,
start_migratetype,
&fallback_mt,
- SB_SEARCH_PREFERRED | SB_SEARCH_FALLBACK);
+ search_cats);

if (!page)
continue;
@@ -3681,8 +3737,10 @@ __rmqueue(struct zone *zone, unsigned int order, int migratetype,
}
fallthrough;
case RMQUEUE_STEAL:
- if (!(alloc_flags & ALLOC_NOFRAGMENT)) {
- page = __rmqueue_steal(zone, order, migratetype);
+ if (!(alloc_flags & ALLOC_NOFRAGMENT) ||
+ (alloc_flags & ALLOC_NOFRAG_TAINTED_OK)) {
+ page = __rmqueue_steal(zone, order, migratetype,
+ alloc_flags);
if (page) {
*mode = RMQUEUE_STEAL;
return page;
@@ -5301,9 +5359,24 @@ get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
/*
* It's possible on a UMA machine to get through all zones that are
* fragmented. If avoiding fragmentation, reset and try again.
- */
- if (no_fallback && !defrag_mode) {
- alloc_flags &= ~ALLOC_NOFRAGMENT;
+ *
+ * For allocations that can do direct reclaim, keep NOFRAGMENT set
+ * and let the slowpath try reclaim and compaction to free pages in
+ * already-tainted superpageblocks before allowing clean SPBs to be
+ * tainted.
+ *
+ * Atomic allocations cannot reclaim, but try an intermediate step
+ * first: allow steal/claim from tainted SPBs only. This avoids
+ * tainting clean SPBs while still finding pages in tainted ones.
+ * Only drop NOFRAGMENT entirely if that also fails.
+ */
+ if (no_fallback && !defrag_mode &&
+ !(gfp_mask & __GFP_DIRECT_RECLAIM)) {
+ if (!(alloc_flags & ALLOC_NOFRAG_TAINTED_OK)) {
+ alloc_flags |= ALLOC_NOFRAG_TAINTED_OK;
+ goto retry;
+ }
+ alloc_flags &= ~(ALLOC_NOFRAGMENT | ALLOC_NOFRAG_TAINTED_OK);
goto retry;
}

--
2.52.0