[PATCH 2/2] mm: memcg: settle memory.high debt after large folio swapin
From: Qinyun Tan
Date: Thu Sep 03 2026 - 23:54:34 EST
On SWP_SYNCHRONOUS_IO swap devices (zram) with mTHP swapin enabled,
swapping a range back in from within a single kernel entry -- the
populate loop of mlock() or MADV_POPULATE_READ, or any GUP-driven
population -- drives a memcg's usage from memory.high straight up to
memory.max with zero reclaim and zero penalty sleep.
This defeats the containment memory.high is supposed to provide: the
high..max buffer that userspace OOM handlers (oomd, Kubernetes) rely
on as their reaction window is consumed in well under a second, and
when the swapped-in pages are mlocked the burst ends in memcg OOM.
The cause is the swapin instance of the problem fixed by the
previous patch for anonymous THP faults. memory.high is enforced
either on return to userspace, which a populate loop does not reach
between faults, or synchronously in try_charge_memcg() for large
overcharges, which is gated on gfpflags_allow_blocking(). Since
commit 242d12c98174 ("mm: support large folios swap-in for sync io
devices"), swapping in a large folio charges it with the gfp derived
from vma_thp_gfp_mask() (nowadays via __swap_cache_alloc(), which
overrides the caller's GFP_HIGHUSER_MOVABLE for order > 0 before
calling mem_cgroup_swapin_charge_folio()). With the default
defrag=madvise and no MADV_HUGEPAGE the resulting gfp is
GFP_TRANSHUGE_LIGHT based and does not allow blocking. That is the
right policy for the physical allocation, but try_charge_memcg()
also reads it as "this context cannot sleep" and skips the
synchronous enforcement, even though swapin fault context sleeps
just fine. Order-0 swapin is unaffected: it charges with the
caller's GFP_HIGHUSER_MOVABLE and throttles as expected.
Fix this by settling the debt at the end of do_swap_page(), where
sleeping is known to be safe: the folio lock, the page table lock
and the swap device reference have all been dropped, only the
mmap/VMA read lock is held -- the same context in which the order-0
charge path already throttles today. This is a no-op read of
current->memcg_nr_pages_over_high when there is no debt.
The charge gfp is deliberately kept coupled to the allocation gfp so
the fail-fast fallback to order-0 at memory.max is preserved,
matching the previous patch.
Verified on zram swap with hugepages-64kB/enabled=always and zswap
disabled at boot: memory.high=30M, memory.max=140M, then a single
MADV_POPULATE_READ over a 200M swapped-out range (~3200 64k large
folio swapins, confirmed via mTHP swpin stats). Without this patch
the populate bursts through the whole high..max buffer in 0.24s and
memory.events max goes 0->1. With it, max stays 0 across repeated
runs and usage is held at memory.high by reclaim throughout (high
0->~680). Populate time is unchanged (0.22s): settling on every
fault keeps the overage within one charge batch, reclaim keeps up
with the swapin rate and the penalty sleep never needs to engage.
The penalty-sleep regime (mlock'd populate, so swapped-in pages are
immediately unreclaimable) was verified separately: without this
patch the locked burst blows through the entire high..max buffer
(~132M of 64k swapins) within 0.1s and ends in memcg OOM; with it,
usage climbs from memory.high at a quadratically decaying pace (33M
after 5s, reproducibly) and never approaches memory.max within the
observation window.
Fixes: 242d12c98174 ("mm: support large folios swap-in for sync io devices")
Cc: <stable@xxxxxxxxxxxxxxx>
Signed-off-by: Qinyun Tan <qinyuntan@xxxxxxxxxxxxxxxxx>
---
mm/memory.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/mm/memory.c b/mm/memory.c
index 8b0c2c735d3d..dd56b43d3aa3 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -5267,6 +5267,14 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
out:
if (si)
put_swap_device(si);
+ /*
+ * Large folio swapin charges with the THP allocation gfp, which may
+ * not allow blocking, making try_charge skip its synchronous
+ * memory.high throttling. Settle any over-high debt here instead,
+ * where sleeping is safe: the folio lock, the page table lock and
+ * the swap device reference have all been dropped.
+ */
+ mem_cgroup_handle_over_high(GFP_KERNEL);
return ret;
out_nomap:
if (vmf->pte)
--
2.55.0