[PATCH 5.15.y 6.1.y 6.6.y 1/1] mm/vmscan: flush deferred TLB before freeing large folios

From: Jiayuan Chen

Date: Wed Jul 08 2026 - 00:13:54 EST


From: Jiayuan Chen <jiayuan.chen@xxxxxxxxxx>

In reclaim, shrink_folio_list() unmaps PTEs with a deferred, batched TLB
flush. The batch is only flushed by try_to_unmap_flush() near the end of
the function, just before the order-0 folios collected in @free_folios are
handed back to the allocator.

Large folios don't go through @free_folios -- they're freed inline at the
free_it label via destroy_large_folio(), which runs before that flush. So
a large folio's pages can be returned to the buddy allocator and reused
while another CPU still holds a stale TLB entry for them, and that CPU then
reads or executes through the stale translation into the reused page. For
file-backed large folios (e.g. executable text) this shows up as random
SIGSEGV/SIGILL in user space, with fault addresses that don't match the
code being run.

Flush the deferred batch before freeing a large folio inline, the same way
the order-0 path already waits for the flush.

Upstream this is fixed as a side effect of commit bc2ff4cbc329 ("mm: free
folios in a batch in shrink_folio_list()"), which is a larger change; this
is the minimal fix for -stable.

Reported-by: Yingfu Zhou <yingfu.zhou@xxxxxxxxxx>
Fixes: bd4c82c22c36 ("mm, THP, swap: delay splitting THP after swapped out")
Cc: Jiayuan Chen <jiayuan.chen@xxxxxxxxx>
Signed-off-by: Jiayuan Chen <jiayuan.chen@xxxxxxxxxx>
---
destroy_compound_page was recently renamed to destroy_large_folio.
So it would be conflict when this patch was applied to 5.15/6.1
---
mm/vmscan.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/mm/vmscan.c b/mm/vmscan.c
index aba757e5c597..8eb498351d9b 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -2123,10 +2123,12 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
* Is there need to periodically free_folio_list? It would
* appear not as the counts should be low
*/
- if (unlikely(folio_test_large(folio)))
+ if (unlikely(folio_test_large(folio))) {
+ try_to_unmap_flush();
destroy_large_folio(folio);
- else
+ } else {
list_add(&folio->lru, &free_folios);
+ }
continue;

activate_locked_split:
--
2.43.0