mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v2 0/2] mm: don't migrate folios containing hwpoisoned pages
@ 2026-10-26  2:55 Yuanhe Shu
  2026-10-26  2:55 ` [PATCH v2 1/2] mm/compaction: skip " Yuanhe Shu
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Yuanhe Shu @ 2026-10-26  2:55 UTC (permalink / raw)
  To: akpm
  Cc: david, Huang, Ying, Kaitao Cheng, vbabka, linmiaohe,
	nao.horiguchi, ziy, wangkefeng.wang, tujinjiang, mgorman,
	chengkaitao, muchun.song, linux-mm, linux-kernel, xiangzao

v2 of this series.  No changes to the approach; the two deltas are a
wording correction promised in the v1 discussion and a third check
suggested by Kaitao Cheng.

memory_failure() can end up abandoning a folio while PG_hwpoison is
already set on it.  Two ways to get there:

  1. Race: any isolator (compaction, NUMA balancing, move_pages(),
     mbind(), CMA, long-term GUP) clears PG_lru from under
     memory_failure(), which then fails the HWPoisonHandlable()
     check, exhausts the get_any_page() retries and bails out with
     MF_IGNORED - without holding any reference, but with the flag
     set.

  2. No race at all: the THP-split-failure path
     (kill_procs_now() + MF_MSG_UNSPLIT_THP) leaves a large folio
     on the LRU with a PG_hwpoisoned subpage by design.

In both cases the migrator still holds the folio on its migration
list and migrates it like any other: the corrupted data is copied
into a fresh folio with no poison marker, and no hwpoison PTE entry
is installed on the way back, so the userspace mapping is silently
redirected to the corrupted copy - no SIGBUS is ever delivered.

This was reproduced on v7.3-rc4-75-g62f4c998b297 (the base of
this series): an order-0 stress workload (madvise(MADV_HWPOISON)
racing move_pages()) left the poison flag set on 8426 abandoned
folios in a 180-second run; 698 of them were subsequently
migrated, and in every case the address then served the copied
corrupted data with no SIGBUS.  With patch 2, 0 of 9711 were
propagated.

The compaction-side guard is needed regardless of how the race in
memory_failure() itself might be addressed: the steady state
(split failure leaves the folio on the LRU with the flag already
set) is not a race at all.  Deterministically validated 16/16
poisoned mTHP folios migrated unpatched vs 0/16 with patch 1,
with clean controls migrating 16/16 in both runs.

The checks compose with, not replace, copy_mc: folio_mc_copy()
still catches real-DRAM corruption during the copy on archs that
implement it, and the flag checks refuse software-injected poison
on all archs before and after the copy.  Extending the
!ARCH_HAS_COPY_MC fallbacks of copy_mc_user_highpage() and
copy_mc_highpage() with the same flag check - which would also
cover their non-migration callers - was proposed by Huang, Ying
and welcomed by David in the v1 discussion; that is posted as a
separate patch right after this series.

Changes since v1:

- Patch 1: the isolation check's claim is narrowed to folios that
  are already flagged when compaction sees them ("avoids the
  migration of a folio that is already flagged"); a poison landing
  after isolation is patch 2's business, as David pointed out.

- Patch 2: a third check after the copy, before the mapping is
  transferred, suggested by Kaitao Cheng.  It closes the copy
  window itself, which the debugfs corrupt-pfn injector can hit
  since it has no GUP or PTE ordering constraint.  In the v1
  racing workload the pre-copy checks already propagated 0 of
  9711, so the new check is measured-neutral there.

- Both patches carry Assisted-by: LLM, as promised in the v1
  discussion.

Yuanhe Shu (2):
  mm/compaction: skip folios containing hwpoisoned pages
  mm/migrate: refuse to migrate folios containing hwpoisoned pages

 mm/compaction.c |  8 ++++++++
 mm/migrate.c    | 49 ++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 55 insertions(+), 2 deletions(-)

base-commit: 62f4c998b297cf233997a2b4cd6fc2d2df0319c9
-- 
2.43.7

^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH v2 1/2] mm/compaction: skip folios containing hwpoisoned pages
  2026-10-26  2:55 [PATCH v2 0/2] mm: don't migrate folios containing hwpoisoned pages Yuanhe Shu
@ 2026-10-26  2:55 ` Yuanhe Shu
  2026-10-26  2:55 ` [PATCH v2 2/2] mm/migrate: refuse to migrate " Yuanhe Shu
  2026-10-26  9:56 ` [PATCH v2 0/2] mm: don't " David Hildenbrand (Arm)
  2 siblings, 0 replies; 4+ messages in thread
From: Yuanhe Shu @ 2026-10-26  2:55 UTC (permalink / raw)
  To: akpm
  Cc: david, Huang, Ying, Kaitao Cheng, vbabka, linmiaohe,
	nao.horiguchi, ziy, wangkefeng.wang, tujinjiang, mgorman,
	chengkaitao, muchun.song, linux-mm, linux-kernel, xiangzao

memory_failure() sets PG_hwpoison on the error page before it manages
to pin the folio via get_hwpoison_page().  Compaction running on
another CPU can isolate that same folio in the meantime, so
HWPoisonHandlable() then fails on !PageLRU(), the retry loop in
get_any_page() exhausts itself and memory_failure() gives up with
MF_IGNORED - while compaction still has the folio on its
migratepages list, flagged PG_hwpoison.

Nothing stops compaction from migrating such a folio afterwards.
The corrupted data is copied into a fresh folio that carries no
poison marker, and no hwpoison PTE entry is installed on the way
back: the userspace mapping is silently redirected to the corrupted
copy and no SIGBUS is ever delivered.

A large folio with a PG_hwpoisoned subpage can also sit on the LRU
without any race at all, via the THP-split-failure path of
memory_failure() (kill_procs_now() + MF_MSG_UNSPLIT_THP); the guard
below catches that case as well.

folio_mc_copy() does not close this hole: it only detects corruption
at copy time and only where ARCH_HAS_COPY_MC is implemented (x86_64
and PPC64; elsewhere copy_mc_highpage() degrades to a plain copy).
Software-injected poison never traps during the copy on any
architecture, and that is what MADV_HWPOISON, the hwpoison-inject
interface and the fuzzers exercise.  An isolation-time check avoids
the migration of a folio that is already flagged, on all
architectures and for both real and simulated poison; a poison
landing after isolation is what patch 2 checks for.
The check itself is two flag tests on the head page (PG_hwpoison
and the folio-level has_hwpoisoned marker), not a walk over
subpages.

Neither page reclaim nor memory hotplug lets such a folio be
copied into a fresh one: reclaim unmaps the poisoned order-0 page,
and skips the hwpoisoned large folios it cannot safely unmap, in
shrink_folio_list()
commit 1b0449544c64 ("mm/vmscan: don't try to reclaim hwpoison folio")
commit 9f1e8cd0b7c4 ("mm/vmscan: fix hwpoisoned large folio handling in shrink_folio_list")
while memory hotplug refuses to migrate them in do_migrate_range()
commit 5f5ee52d4f58 ("mm/hwpoison: introduce folio_contain_hwpoisoned_page() helper").
Skip them in compaction as well, for the same reason reclaim settled
on skipping: the UCE is rare and a race with compaction is rarer
still, so skipping is enough, and a later memory_failure() will
handle the folio if the UCE is triggered again - while a migrated
folio would silently propagate the corruption instead.

Deterministic validation on v7.3-rc4-75-g62f4c998b297: order-4
mTHP folios were brought into the stable "large, on LRU,
PG_hwpoisoned subpage" state by pinning a sibling subpage with
vmsplice and injecting MADV_HWPOISON on another subpage, which
makes memory_failure() take the THP-split-failure path; after
triggering compaction, /proc/kpageflags shows which poisoned
folios moved.  The unpatched kernel migrated 16/16 poisoned
folios and 16/16 clean controls in the same pageblocks; with
this patch, 0/16 poisoned folios were migrated while their
controls still migrated 16/16.

Soft offline is not affected: it only sets the poison marker after
its own migration has succeeded.

Assisted-by: LLM
Signed-off-by: Yuanhe Shu <xiangzao@linux.alibaba.com>
---
 mm/compaction.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/mm/compaction.c b/mm/compaction.c
index a049415512c6..491adcbc5313 100644
--- a/mm/compaction.c
+++ b/mm/compaction.c
@@ -1093,6 +1093,14 @@ isolate_migratepages_block(struct compact_control *cc, unsigned long low_pfn,
 		if (unlikely(!folio))
 			goto isolate_fail;
 
+		/*
+		 * Migrating would copy the corrupted data into a fresh
+		 * folio with no poison marker; skip, as memory hotplug
+		 * refuses to migrate such folios for the same reason.
+		 */
+		if (folio_contain_hwpoisoned_page(folio))
+			goto isolate_fail_put;
+
 		/*
 		 * Migration will fail if an anonymous page is pinned in memory,
 		 * so avoid taking lru_lock and isolating it unnecessarily in an
-- 
2.43.7


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH v2 2/2] mm/migrate: refuse to migrate folios containing hwpoisoned pages
  2026-10-26  2:55 [PATCH v2 0/2] mm: don't migrate folios containing hwpoisoned pages Yuanhe Shu
  2026-10-26  2:55 ` [PATCH v2 1/2] mm/compaction: skip " Yuanhe Shu
@ 2026-10-26  2:55 ` Yuanhe Shu
  2026-10-26  9:56 ` [PATCH v2 0/2] mm: don't " David Hildenbrand (Arm)
  2 siblings, 0 replies; 4+ messages in thread
From: Yuanhe Shu @ 2026-10-26  2:55 UTC (permalink / raw)
  To: akpm
  Cc: david, Huang, Ying, Kaitao Cheng, vbabka, linmiaohe,
	nao.horiguchi, ziy, wangkefeng.wang, tujinjiang, mgorman,
	chengkaitao, muchun.song, linux-mm, linux-kernel, xiangzao

All callers of migrate_pages() without their own poison handling -
move_pages(2), mbind(), NUMA balancing, alloc_contig_range() (CMA)
and long-term GUP - can race memory_failure() the same way compaction
does (see patch 1): the migration isolates a folio and clears
PG_lru, memory_failure() sets PG_hwpoison, then fails to pin it and
gives up with MF_IGNORED, and the pending migration copies the
corrupted data into a fresh folio that carries no poison marker;
the userspace mapping ends up silently serving the corrupted copy
with no SIGBUS.

This was reproduced on v7.3-rc4-75-g62f4c998b297: an order-0 stress
workload running madvise(MADV_HWPOISON) concurrently with
move_pages() left the poison flag set on 8426 abandoned folios in
a 180-second run, identified via /proc/kpageflags; 698 of them
were subsequently migrated, and in every case the address then
served the copied corrupted data with no SIGBUS.

Refuse such migrations in the migration core, with three checks:

- migrate_folio_unmap() checks at entry, so a folio that is already
  poisoned when the batch reaches it fails immediately, without
  allocating a destination folio or unmapping it.

- migrate_folio_move() checks again immediately before the copy,
  because migrate_pages_batch() unmaps a whole chunk of folios
  before copying any of them, so a poison can land in between.  A
  GUP-based path such as madvise(MADV_HWPOISON) takes its
  reference while the folio is still mapped, i.e. before the
  unmap, and memory_failure() sets PG_hwpoison before releasing
  that reference, so a racing poison is visible by the time
  migration reaches the copy, unless the injecting thread is
  delayed between taking the reference and setting the flag.  A
  hardware MCE can likewise land in the final instructions
  between the check and the copy; on x86_64 folio_mc_copy()
  additionally catches real-DRAM corruption at copy time.

- __migrate_folio() and migrate_huge_page_move_mapping() check
  again after the copy, before the mapping is transferred: the
  debugfs corrupt-pfn injector has no GUP or PTE ordering
  constraint, so it can set the flag while the copy is in
  progress, and only this recheck catches that window.  Failing
  there unwinds exactly like a copy failure.

All checks fail with -EHWPOISON, a permanent failure: the folio
goes back on the LRU with the poison flag intact, for reclaim or
a later memory_failure() to deal with - the same reasoning reclaim
settled on in
commit 9f1e8cd0b7c4 ("mm/vmscan: fix hwpoisoned large folio handling in shrink_folio_list").
The hugetlb path gets the same checks: its unmap-to-copy window is
single-folio and much narrower than the batched path, but the
checks cost bit tests.

Measured with the workload above (180-second runs; the count of
abandoned folios varies between runs with the race rate): the
unpatched kernel propagated 698 of 8426 abandoned folios; with the
pre-copy checks alone, 0 of 9711 were propagated.  The post-copy
recheck closes the remaining injector window.

Soft offline is unaffected: it sets the poison marker only after its
own migration has succeeded.  Memory hotplug offlining is
unaffected: it filters poisoned folios out before calling
migrate_pages().  Reclaim is unaffected: poisoned folios go through
its own unmap and skip paths, not migrate_pages().

A check at the copy site was previously proposed by Kaitao Cheng,
for the hugetlb path and for move_to_new_folio(); that discussion
stalled over the remaining race with memory_failure().  The
GUP-reference argument and the measurement above address that
concern for injection paths, and the post-copy recheck here is his
suggestion.

Assisted-by: LLM
Suggested-by: Kaitao Cheng <kaitao.cheng@linux.dev>
Signed-off-by: Yuanhe Shu <xiangzao@linux.alibaba.com>
Link: https://lore.kernel.org/r/20260707090136.52904-1-kaitao.cheng@linux.dev/
---
 mm/migrate.c | 49 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 47 insertions(+), 2 deletions(-)

diff --git a/mm/migrate.c b/mm/migrate.c
index 15b45832bcfa..208a3991ca06 100644
--- a/mm/migrate.c
+++ b/mm/migrate.c
@@ -749,6 +749,14 @@ int migrate_huge_page_move_mapping(struct address_space *mapping,
 	if (unlikely(rc))
 		return rc;
 
+	/*
+	 * Recheck after the copy: an injector can set the flag while the
+	 * copy is in progress.  The mapping has not been transferred yet,
+	 * so failing here is the same unwinding as a copy failure.
+	 */
+	if (unlikely(folio_contain_hwpoisoned_page(src)))
+		return -EHWPOISON;
+
 	xas_lock_irq(&xas);
 	if (!folio_ref_freeze(src, expected_count)) {
 		xas_unlock_irq(&xas);
@@ -881,6 +889,14 @@ static int __migrate_folio(struct address_space *mapping, struct folio *dst,
 	if (unlikely(rc))
 		return rc;
 
+	/*
+	 * Recheck after the copy: an injector can set the flag while the
+	 * copy is in progress.  The mapping has not been transferred yet,
+	 * so failing here is the same unwinding as a copy failure.
+	 */
+	if (unlikely(folio_contain_hwpoisoned_page(src)))
+		return -EHWPOISON;
+
 	rc = __folio_migrate_mapping(mapping, dst, src, expected_count);
 	if (rc)
 		return rc;
@@ -1222,6 +1238,15 @@ static int migrate_folio_unmap(new_folio_t get_new_folio,
 	bool locked = false;
 	bool dst_locked = false;
 
+	if (unlikely(folio_contain_hwpoisoned_page(src))) {
+		/*
+		 * Fail permanently before allocating dst: the folio
+		 * goes back on the LRU for a later memory_failure().
+		 */
+		migrate_folio_undo_src(src, 0, NULL, false, ret);
+		return -EHWPOISON;
+	}
+
 	dst = get_new_folio(src, private);
 	if (!dst)
 		return -ENOMEM;
@@ -1376,6 +1401,15 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	prev = dst->lru.prev;
 	list_del(&dst->lru);
 
+	if (unlikely(folio_contain_hwpoisoned_page(src))) {
+		/*
+		 * Recheck before the copy: a poison can land while the
+		 * batch is still unmapping the rest of its chunk.
+		 */
+		rc = -EHWPOISON;
+		goto out;
+	}
+
 	if (unlikely(page_has_movable_ops(&src->page))) {
 		rc = migrate_movable_ops_page(&dst->page, &src->page, mode);
 		if (rc)
@@ -1521,6 +1555,12 @@ static int unmap_and_move_hugetlb_folio(new_folio_t get_new_folio,
 		goto out_unlock;
 	}
 
+	if (unlikely(folio_contain_hwpoisoned_page(src))) {
+		/* Same reasoning as in migrate_folio_unmap(). */
+		rc = -EHWPOISON;
+		goto out_unlock;
+	}
+
 	if (folio_test_anon(src))
 		anon_vma = folio_get_anon_vma(src);
 
@@ -1546,8 +1586,13 @@ static int unmap_and_move_hugetlb_folio(new_folio_t get_new_folio,
 		was_mapped = 1;
 	}
 
-	if (!folio_mapped(src))
-		rc = move_to_new_folio(dst, src, mode);
+	if (!folio_mapped(src)) {
+		/* Same reasoning as in migrate_folio_move(). */
+		if (unlikely(folio_contain_hwpoisoned_page(src)))
+			rc = -EHWPOISON;
+		else
+			rc = move_to_new_folio(dst, src, mode);
+	}
 
 	if (was_mapped)
 		remove_migration_ptes(src, !rc ? dst : src, ttu);
-- 
2.43.7


^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH v2 0/2] mm: don't migrate folios containing hwpoisoned pages
  2026-10-26  2:55 [PATCH v2 0/2] mm: don't migrate folios containing hwpoisoned pages Yuanhe Shu
  2026-10-26  2:55 ` [PATCH v2 1/2] mm/compaction: skip " Yuanhe Shu
  2026-10-26  2:55 ` [PATCH v2 2/2] mm/migrate: refuse to migrate " Yuanhe Shu
@ 2026-10-26  9:56 ` David Hildenbrand (Arm)
  2 siblings, 0 replies; 4+ messages in thread
From: David Hildenbrand (Arm) @ 2026-10-26  9:56 UTC (permalink / raw)
  To: Yuanhe Shu, akpm
  Cc: Huang, Ying, Kaitao Cheng, vbabka, linmiaohe, nao.horiguchi, ziy,
	wangkefeng.wang, tujinjiang, mgorman, chengkaitao, muchun.song,
	linux-mm, linux-kernel

On 10/26/26 03:55, Yuanhe Shu wrote:
> v2 of this series.  No changes to the approach; the two deltas are a
> wording correction promised in the v1 discussion and a third check
> suggested by Kaitao Cheng.
> 
> memory_failure() can end up abandoning a folio while PG_hwpoison is
> already set on it.  Two ways to get there:
> 
>   1. Race: any isolator (compaction, NUMA balancing, move_pages(),
>      mbind(), CMA, long-term GUP) clears PG_lru from under
>      memory_failure(), which then fails the HWPoisonHandlable()
>      check, exhausts the get_any_page() retries and bails out with
>      MF_IGNORED - without holding any reference, but with the flag
>      set.
> 
>   2. No race at all: the THP-split-failure path
>      (kill_procs_now() + MF_MSG_UNSPLIT_THP) leaves a large folio
>      on the LRU with a PG_hwpoisoned subpage by design.
> 
> In both cases the migrator still holds the folio on its migration
> list and migrates it like any other: the corrupted data is copied
> into a fresh folio with no poison marker, and no hwpoison PTE entry
> is installed on the way back, so the userspace mapping is silently
> redirected to the corrupted copy - no SIGBUS is ever delivered.
> 
> This was reproduced on v7.3-rc4-75-g62f4c998b297 (the base of
> this series): an order-0 stress workload (madvise(MADV_HWPOISON)
> racing move_pages()) left the poison flag set on 8426 abandoned
> folios in a 180-second run; 698 of them were subsequently
> migrated, and in every case the address then served the copied
> corrupted data with no SIGBUS.  With patch 2, 0 of 9711 were
> propagated.
> 
> The compaction-side guard is needed regardless of how the race in
> memory_failure() itself might be addressed: the steady state
> (split failure leaves the folio on the LRU with the flag already
> set) is not a race at all.  Deterministically validated 16/16
> poisoned mTHP folios migrated unpatched vs 0/16 with patch 1,
> with clean controls migrating 16/16 in both runs.
> 
> The checks compose with, not replace, copy_mc: folio_mc_copy()
> still catches real-DRAM corruption during the copy on archs that
> implement it, and the flag checks refuse software-injected poison
> on all archs before and after the copy.  Extending the
> !ARCH_HAS_COPY_MC fallbacks of copy_mc_user_highpage() and
> copy_mc_highpage() with the same flag check - which would also
> cover their non-migration callers - was proposed by Huang, Ying
> and welcomed by David in the v1 discussion; that is posted as a
> separate patch right after this series.

With that patch in place, why do we still need these changes here?

-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-10-26  9:57 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-26  2:55 [PATCH v2 0/2] mm: don't migrate folios containing hwpoisoned pages Yuanhe Shu
2026-10-26  2:55 ` [PATCH v2 1/2] mm/compaction: skip " Yuanhe Shu
2026-10-26  2:55 ` [PATCH v2 2/2] mm/migrate: refuse to migrate " Yuanhe Shu
2026-10-26  9:56 ` [PATCH v2 0/2] mm: don't " David Hildenbrand (Arm)

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®