[RFC PATCH 28/45] mm: page_alloc: keep PCP refill in tainted SPBs across owned pageblocks

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

rmqueue_bulk Phase 2 walks SB_TAINTED superpageblocks looking for
sub-pageblock free fragments, so PCP refill can be satisfied without
tainting a clean SPB. The original Phase 2 abandons a candidate
pageblock entirely if pbd->cpu != 0 (already owned by some CPU), to
avoid two CPUs holding PCPBuddy pages from the same pageblock — which
would let the PCP merge pass corrupt the other CPU's PCP list.

On systems with many CPUs (88+) and many tainted SPBs (~50% on a 16
GiB devvm under stress), nearly every free fragment in a tainted SPB
lives in a pageblock already PCPBuddy-owned by some CPU. Phase 2 skips
through the entire SPB without finding anything usable, the atomic
alloc falls through to the slowpath, and clean SPBs get tainted.

Take the page anyway when the source pageblock is owned, but skip the
ownership claim and PCPBuddy marking. Phase 3 / __rmqueue_smallest
already pull plain non-PCPBuddy pages from owned pageblocks the same
way; the hazard is specifically about two CPUs holding PCPBuddy pages
from the same pageblock, not about a plain non-PCPBuddy page coexisting
with another CPU's PCPBuddy entries. Pass 0 (owned-block recovery) is
only meaningful when we actually claimed ownership, so register on
owned_blocks only when !pb_owned.

Fixes: 266461cd5442 ("mm: page_alloc: adopt partial pageblocks from tainted superpageblocks")

Signed-off-by: Rik van Riel <riel@surriel.com>
Assisted-by: Claude:claude-opus-4.7 syzkaller
---
mm/page_alloc.c | 50 ++++++++++++++++++++++++++++---------------------
1 file changed, 29 insertions(+), 21 deletions(-)

diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index f0fdfe8c9a45..a09660a06ed3 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -4133,6 +4133,7 @@ static bool rmqueue_bulk(struct zone *zone, unsigned int order,
&zone->spb_lists[SB_TAINTED][full], list) {
struct page *page;
int found_order = -1;
+ bool claim_pb;

if (sb->nr_free_pages < pageblock_nr_pages / 4)
continue;
@@ -4156,33 +4157,39 @@ static bool rmqueue_bulk(struct zone *zone, unsigned int order,
continue;

/*
- * Check that this pageblock isn't already
- * owned by another CPU. If it is, two CPUs
- * would have PCPBuddy pages from the same
- * pageblock, and the PCP merge pass could
- * corrupt the other CPU's PCP list.
+ * Found a free fragment in a tainted SPB. Take
+ * it from the buddy.
+ *
+ * If the source pageblock is unowned, claim it:
+ * mark our pages PagePCPBuddy and register the
+ * block on owned_blocks so Pass 0 can recover
+ * remaining fragments on future refills.
+ *
+ * If the source pageblock is already owned by
+ * some CPU (us or another), take the page as a
+ * plain non-PCPBuddy fragment — the same way
+ * Phase 3 / __rmqueue_smallest would. Setting
+ * PagePCPBuddy here would let two CPUs hold
+ * PCPBuddy pages from the same pageblock, and
+ * the PCP merge pass could then corrupt the
+ * other CPU's PCP list.
+ *
+ * Set PB_has_<migratetype> either way (bypasses
+ * page_del_and_expand which normally does the
+ * PB_has tracking); idempotent if already set.
*/
pbd = pfn_to_pageblock(page,
page_to_pfn(page));
- if (pbd->cpu != 0)
- continue;
+ claim_pb = (pbd->cpu == 0);

- /*
- * Found a free chunk in an unowned pageblock.
- * Take it from buddy, claim ownership, and
- * set PCPBuddy. Pass 0 will grab remaining
- * buddy entries on future refills.
- *
- * Set PB_has_<migratetype> since we bypass
- * page_del_and_expand (which normally does
- * PB_has tracking).
- */
del_page_from_free_list(page, zone,
found_order,
migratetype);
__spb_set_has_type(page, migratetype);
- set_pcpblock_owner(page, cpu);
- __SetPagePCPBuddy(page);
+ if (claim_pb) {
+ set_pcpblock_owner(page, cpu);
+ __SetPagePCPBuddy(page);
+ }
pcp_enqueue_tail(pcp, page, migratetype,
found_order);
refilled += 1 << found_order;
@@ -4190,9 +4197,10 @@ static bool rmqueue_bulk(struct zone *zone, unsigned int order,
/*
* Register for Phase 0 recovery so future
* drains from this pageblock can be swept
- * back efficiently.
+ * back efficiently. Only meaningful when we
+ * actually claimed ownership above.
*/
- if (list_empty(&pbd->cpu_node))
+ if (claim_pb && list_empty(&pbd->cpu_node))
list_add(&pbd->cpu_node,
&pcp->owned_blocks);

--
2.52.0