[PATCH 0/2] mm: fix two allocator races with zonelist and cpuset updates

From: Gregory Price (Meta)

Date: Wed Oct 14 2026 - 18:25:07 EST


Two small fixes for lockless allocator checks that can act on an
inconsistent view of state another CPU is updating.

Neither of these are stable-worthy.

Patch 1: the zonelist_update_seq retry is compiled out without
CONFIG_MEMORY_HOTREMOVE, but memory hot-add can also cause the race.
A __GFP_THISNODE allocation can observe this and fail to allocate.

Patch 2: cpuset_nodemask_valid_mems_allowed() compares a mempolicy
nodemask with mems_allowed without mems_allowed_seq. A concurrent
cpuset update can cause a spurious empty intersection and end up
relaxing an MPOL_BIND needlessly. This only covers task policies.

Reproducers
===========

Both races were reproduced on an unmodified kernel in a QEMU guest with
three NUMA nodes:
node 0: 3G, CPUs 0-1
node 1: 1G ZONE_NORMAL, CPU 2
node 2: 1G ZONE_NORMAL, CPU 3

And a 512M pc-dimm cold-plugged on node 1, left offline at boot
(CONFIG_ACPI_HOTPLUG_MEMORY=y, CONFIG_MEMORY_HOTREMOVE=n).

The windows are a few instructions wide, so I used a test module to
widens them with a kprobe placed on a single instruction inside the
window. This basically emulates an interrupt or vCPU preemption.

Patch 1 (zonelist):

Online node 1's first memory block as ZONE_MOVABLE, which rebuilds
node 1's zonelists.

Stall the rebuild after it writes ZONE_MOVABLE into the first entry
but before it writes ZONE_NORMAL into the second.

On another CPU, allocate GFP_KERNEL | __GFP_THISNODE from node 1.

Before: the allocation fails, 3/3 runs.
After: the allocation waits for the rebuild and succeeds, 3/3 runs.

Patch 2 (cpuset):

Put a task in a cpuset with mems={0}
set_mempolicy(2) the task to MPOL_BIND={0}
Run the task on node2 (yes this is a little flagrant).

Fill node 0 with HugeTLB pages to make allocations fail.
Stall between the two loads in cpuset_nodemask_valid_mems_allowed().

While stalled, change cpuset.mems={1,2}, this rebinds the task
mempolicy to MPOL_BIND={1}.

The check compares the old mems_allowed (0) with the new policy (1),
finds no overlap, and drops the nodemask.

Before: the allocation lands on node 2, 3/3 runs.
After: the allocation lands on node 1, 3/3 runs.

Gregory Price (Meta) (2):
mm/page_alloc: detect zonelist rebuilds from memory hot-add
cgroup/cpuset: use mems_allowed_seq when checking a policy nodemask

kernel/cgroup/cpuset.c | 13 ++++++++++++-
mm/page_alloc.c | 4 ++--
2 files changed, 14 insertions(+), 3 deletions(-)

--
2.53.0-Meta