The Linux Kernel Mailing List
 help / color / mirror / Atom feed
* [PATCH v3] kexec: keep the next kernel off hardware-poisoned pages
@ 2026-08-03 12:41 Breno Leitao
  2026-08-04 14:36 ` Kiryl Shutsemau
  0 siblings, 1 reply; 3+ messages in thread
From: Breno Leitao @ 2026-08-03 12:41 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
	Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
	Suren Baghdasaryan, Michal Hocko, Baoquan He, Pasha Tatashin,
	Pratyush Yadav, Miaohe Lin, Naoya Horiguchi, pratyush
  Cc: linux-mm, linux-kernel, kexec, rmikey, riel, kernel-team,
	Kiryl Shutsemau, Breno Leitao

Memory failures (such as unrecoverable ECCs errors) are getting more and
more common. The kernel knows how to handle it while running, marking it
as poisoned (and SIGBUS user tasks).

Poisoned memory is removed from the buddy allocator, but, not from
other places. A current problem is that kexec will load new kernel
on top of a bad/poisoned memory, which is undesirable.

If the next kernel's image, initrd or purgatory lands on poisoned frame,
the relocation copy writes to the bad memory and the machine checks
during the kexec.

Skip hardware-poisoned frames when placing segments: check them in the
kexec_file hole finder so it lays the next kernel down on good memory,
and reject a poisoned destination in sanity_check_segment_list() for
the kexec_load path, which cannot relocate.

Suggested-by: Kiryl Shutsemau <kas@kernel.org>
Signed-off-by: Breno Leitao <leitao@debian.org>
---
Tested on x86_64 under virtme-ng, poisoning frames through
/sys/kernel/debug/hwpoison/corrupt-pfn:

 - kexec_file_load(): poisoning a page inside the range the previous load
   picked moves the kernel from 0x13b000000 to 0x138000000, and moves the
   crash kernel's elfcorehdr, the only bottom-up user on x86, from
   0xaf000000 to 0xaf001000.
 - kexec_load(): a one page segment aimed at a poisoned frame fails with
   EADDRNOTAVAIL, while the same segment on a clean frame still loads.
 - No splat with CONFIG_DEBUG_ATOMIC_SLEEP=y, and the box kexecs into the
   relocated kernel.
---
Changes in v3:
- Return the address of the last poisoned page in the range, or
  PHYS_ADDR_MAX when it is clean, instead of a bool plus an output
  parameter. Renamed to range_last_hwpoison(). (Pratyush Yadav)
- Add cond_resched() to the scan loop, as a segment can span half of
  memory. (Sashiko)
- Link to v2: https://patch.msgid.link/20260730-kexec_posioned-v2-1-f92d18551f64@debian.org

Changes in v2:
- Change from pfn_to_page() to pfn_to_online_page(). (Miaohe Lin)
- Return the poisoned address once we find a hit, to avoid the O(n^2)
  rescan. (Sashiko)
- Link to v1: https://patch.msgid.link/20260728-kexec_posioned-v1-1-160c81d180fe@debian.org

To: Andrew Morton <akpm@linux-foundation.org>
To: David Hildenbrand <david@kernel.org>
To: Lorenzo Stoakes <ljs@kernel.org>
To: "Liam R. Howlett" <liam@infradead.org>
To: Vlastimil Babka <vbabka@kernel.org>
To: Mike Rapoport <rppt@kernel.org>
To: Suren Baghdasaryan <surenb@google.com>
To: Michal Hocko <mhocko@suse.com>
To: Baoquan He <baoquan.he@linux.dev>
To: Pasha Tatashin <pasha.tatashin@soleen.com>
To: Pratyush Yadav <pratyush@kernel.org>
To: Miaohe Lin <linmiaohe@huawei.com>
To: Naoya Horiguchi <nao.horiguchi@gmail.com>
Cc: linux-mm@kvack.org
Cc: linux-kernel@vger.kernel.org
Cc: kexec@lists.infradead.org
---
 include/linux/mm.h  |  7 +++++++
 kernel/kexec_core.c | 13 +++++++++++++
 kernel/kexec_file.c | 18 ++++++++++++++++++
 mm/memory-failure.c | 25 +++++++++++++++++++++++++
 4 files changed, 63 insertions(+)

diff --git a/include/linux/mm.h b/include/linux/mm.h
index 7fabe6c66b4b7..fa4f55eff2415 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -5192,6 +5192,7 @@ extern const struct attribute_group memory_failure_attr_group;
 extern void memory_failure_queue(unsigned long pfn, int flags);
 void num_poisoned_pages_inc(unsigned long pfn);
 void num_poisoned_pages_sub(unsigned long pfn, long i);
+phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size);
 #else
 static inline void memory_failure_queue(unsigned long pfn, int flags)
 {
@@ -5204,6 +5205,12 @@ static inline void num_poisoned_pages_inc(unsigned long pfn)
 static inline void num_poisoned_pages_sub(unsigned long pfn, long i)
 {
 }
+
+static inline phys_addr_t range_last_hwpoison(phys_addr_t start,
+					      unsigned long size)
+{
+	return PHYS_ADDR_MAX;
+}
 #endif
 
 #if defined(CONFIG_MEMORY_FAILURE) && defined(CONFIG_MEMORY_HOTPLUG)
diff --git a/kernel/kexec_core.c b/kernel/kexec_core.c
index dc770b9a6d053..f2a519a4a6d93 100644
--- a/kernel/kexec_core.c
+++ b/kernel/kexec_core.c
@@ -212,6 +212,19 @@ int sanity_check_segment_list(struct kimage *image)
 	}
 #endif
 
+	/*
+	 * Reject destinations that land on hardware-poisoned memory: the
+	 * relocation copy would machine-check on the bad frame.
+	 */
+	for (i = 0; i < nr_segments; i++) {
+		phys_addr_t poison;
+
+		poison = range_last_hwpoison(image->segment[i].mem,
+					     image->segment[i].memsz);
+		if (poison != PHYS_ADDR_MAX)
+			return -EADDRNOTAVAIL;
+	}
+
 	/*
 	 * The destination addresses are searched from system RAM rather than
 	 * being allocated from the buddy allocator, so they are not guaranteed
diff --git a/kernel/kexec_file.c b/kernel/kexec_file.c
index 59fb9d71e9d86..530c919412a92 100644
--- a/kernel/kexec_file.c
+++ b/kernel/kexec_file.c
@@ -475,6 +475,7 @@ static int locate_mem_hole_top_down(unsigned long start, unsigned long end,
 {
 	struct kimage *image = kbuf->image;
 	unsigned long temp_start, temp_end;
+	phys_addr_t poison;
 
 	temp_end = min(end, kbuf->buf_max);
 	temp_start = temp_end - kbuf->memsz + 1;
@@ -504,6 +505,15 @@ static int locate_mem_hole_top_down(unsigned long start, unsigned long end,
 			continue;
 		}
 
+		poison = range_last_hwpoison(temp_start, kbuf->memsz);
+		if (poison != PHYS_ADDR_MAX) {
+			/* we hit a poisoned page */
+			if (poison < kbuf->memsz)
+				return 0;
+			temp_start = poison - kbuf->memsz;
+			continue;
+		}
+
 		/* We found a suitable memory range */
 		break;
 	} while (1);
@@ -520,6 +530,7 @@ static int locate_mem_hole_bottom_up(unsigned long start, unsigned long end,
 {
 	struct kimage *image = kbuf->image;
 	unsigned long temp_start, temp_end;
+	phys_addr_t poison;
 
 	temp_start = max(start, kbuf->buf_min);
 
@@ -546,6 +557,13 @@ static int locate_mem_hole_bottom_up(unsigned long start, unsigned long end,
 			continue;
 		}
 
+		poison = range_last_hwpoison(temp_start, kbuf->memsz);
+		if (poison != PHYS_ADDR_MAX) {
+			/* we hit a poisoned page */
+			temp_start = poison + PAGE_SIZE;
+			continue;
+		}
+
 		/* We found a suitable memory range */
 		break;
 	} while (1);
diff --git a/mm/memory-failure.c b/mm/memory-failure.c
index a8b03e2920ba8..ef0e989c25d93 100644
--- a/mm/memory-failure.c
+++ b/mm/memory-failure.c
@@ -96,6 +96,31 @@ void num_poisoned_pages_sub(unsigned long pfn, long i)
 		memblk_nr_poison_sub(pfn, i);
 }
 
+/*
+ * Return the address of the last hardware-poisoned online page in
+ * [start, start + size), or PHYS_ADDR_MAX if the range is clean.
+ */
+phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size)
+{
+	phys_addr_t poison = PHYS_ADDR_MAX;
+	unsigned long pfn, end_pfn;
+
+	if (!size || !atomic_long_read(&num_poisoned_pages))
+		return poison;
+
+	end_pfn = PHYS_PFN(start + size - 1);
+	for (pfn = PHYS_PFN(start); pfn <= end_pfn; pfn++) {
+		struct page *page = pfn_to_online_page(pfn);
+
+		if (page && PageHWPoison(page))
+			poison = PFN_PHYS(pfn);
+
+		cond_resched();
+	}
+
+	return poison;
+}
+
 /**
  * MF_ATTR_RO - Create sysfs entry for each memory failure statistics.
  * @_name: name of the file in the per NUMA sysfs directory.

---
base-commit: c5e32e86ca02b003f86e095d379b38148999293d
change-id: 20260727-kexec_posioned-72bb0a4143a0

Best regards,
--  
Breno Leitao <leitao@debian.org>


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

* Re: [PATCH v3] kexec: keep the next kernel off hardware-poisoned pages
  2026-08-03 12:41 [PATCH v3] kexec: keep the next kernel off hardware-poisoned pages Breno Leitao
@ 2026-08-04 14:36 ` Kiryl Shutsemau
  2026-08-06 14:59   ` Breno Leitao
  0 siblings, 1 reply; 3+ messages in thread
From: Kiryl Shutsemau @ 2026-08-04 14:36 UTC (permalink / raw)
  To: Breno Leitao
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
	Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
	Suren Baghdasaryan, Michal Hocko, Baoquan He, Pasha Tatashin,
	Pratyush Yadav, Miaohe Lin, Naoya Horiguchi, linux-mm,
	linux-kernel, kexec, rmikey, riel, kernel-team

On Mon, Aug 03, 2026 at 05:41:10AM -0700, Breno Leitao wrote:
> @@ -504,6 +505,15 @@ static int locate_mem_hole_top_down(unsigned long start, unsigned long end,
>  			continue;
>  		}
>  
> +		poison = range_last_hwpoison(temp_start, kbuf->memsz);
> +		if (poison != PHYS_ADDR_MAX) {
> +			/* we hit a poisoned page */
> +			if (poison < kbuf->memsz)
> +				return 0;
> +			temp_start = poison - kbuf->memsz;
> +			continue;
> +		}
> +

Hm. Don't we want range_first_hwpoison() for top-down walk? Otherwise
the end of range would land on poison.

>  		/* We found a suitable memory range */
>  		break;
>  	} while (1);

...

> diff --git a/mm/memory-failure.c b/mm/memory-failure.c
> index a8b03e2920ba8..ef0e989c25d93 100644
> --- a/mm/memory-failure.c
> +++ b/mm/memory-failure.c
> @@ -96,6 +96,31 @@ void num_poisoned_pages_sub(unsigned long pfn, long i)
>  		memblk_nr_poison_sub(pfn, i);
>  }
>  
> +/*
> + * Return the address of the last hardware-poisoned online page in
> + * [start, start + size), or PHYS_ADDR_MAX if the range is clean.
> + */
> +phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size)
> +{
> +	phys_addr_t poison = PHYS_ADDR_MAX;
> +	unsigned long pfn, end_pfn;
> +
> +	if (!size || !atomic_long_read(&num_poisoned_pages))
> +		return poison;
> +
> +	end_pfn = PHYS_PFN(start + size - 1);
> +	for (pfn = PHYS_PFN(start); pfn <= end_pfn; pfn++) {
> +		struct page *page = pfn_to_online_page(pfn);
> +
> +		if (page && PageHWPoison(page))
> +			poison = PFN_PHYS(pfn);

Oh... I think it will not work for hugetlb pages. It will give
false-negative.

We cannot just set the bit hugetlb pages as we don't always have memory
for tail page -- look at HugeTLB Vmemmap Optimization (HVO). Hugetlb
uses a trick to encode poison page. See code that uses _hugetlb_hwpoison
in struct folio.

I think we need special-case hugetlb here. (One more reminder why I hate
HugeTLB).

> +
> +		cond_resched();
> +	}
> +
> +	return poison;
> +}
> +
>  /**
>   * MF_ATTR_RO - Create sysfs entry for each memory failure statistics.
>   * @_name: name of the file in the per NUMA sysfs directory.
> 
-- 
  Kiryl Shutsemau / Kirill A. Shutemov

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

* Re: [PATCH v3] kexec: keep the next kernel off hardware-poisoned pages
  2026-08-04 14:36 ` Kiryl Shutsemau
@ 2026-08-06 14:59   ` Breno Leitao
  0 siblings, 0 replies; 3+ messages in thread
From: Breno Leitao @ 2026-08-06 14:59 UTC (permalink / raw)
  To: Kiryl Shutsemau
  Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
	Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
	Suren Baghdasaryan, Michal Hocko, Baoquan He, Pasha Tatashin,
	Pratyush Yadav, Miaohe Lin, Naoya Horiguchi, linux-mm,
	linux-kernel, kexec, rmikey, riel, kernel-team

On Tue, Aug 04, 2026 at 03:36:47PM +0100, Kiryl Shutsemau wrote:
> On Mon, Aug 03, 2026 at 05:41:10AM -0700, Breno Leitao wrote:
> > @@ -504,6 +505,15 @@ static int locate_mem_hole_top_down(unsigned long start, unsigned long end,
> >  			continue;
> >  		}
> >  
> > +		poison = range_last_hwpoison(temp_start, kbuf->memsz);
> > +		if (poison != PHYS_ADDR_MAX) {
> > +			/* we hit a poisoned page */
> > +			if (poison < kbuf->memsz)
> > +				return 0;
> > +			temp_start = poison - kbuf->memsz;
> > +			continue;
> > +		}
> > +
> 
> Hm. Don't we want range_first_hwpoison() for top-down walk? Otherwise
> the end of range would land on poison.

Sure, In this case would have both,  range_last_hwpoison() for
locate_mem_hole_bottom_up() and range_first_hwpoison() for
locate_mem_hole_top_down().

> > +	end_pfn = PHYS_PFN(start + size - 1);
> > +	for (pfn = PHYS_PFN(start); pfn <= end_pfn; pfn++) {
> > +		struct page *page = pfn_to_online_page(pfn);
> > +
> > +		if (page && PageHWPoison(page))
> > +			poison = PFN_PHYS(pfn);
> 
> Oh... I think it will not work for hugetlb pages. It will give
> false-negative.
> 
> We cannot just set the bit hugetlb pages as we don't always have memory
> for tail page -- look at HugeTLB Vmemmap Optimization (HVO). Hugetlb
> uses a trick to encode poison page. See code that uses _hugetlb_hwpoison
> in struct folio.
> 
> I think we need special-case hugetlb here. (One more reminder why I hate
> HugeTLB).

Confirmed -- hugetlb_update_hwpoison() sets the flag on the folio and
keeps the bad subpages on the raw_hwp_list, so a per-pfn scan only sees
the head and misses the tails.

Let me add this special case for our beloved hugeTLB.

Thanks for the review,
--breno

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

end of thread, other threads:[~2026-08-06 14:59 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-03 12:41 [PATCH v3] kexec: keep the next kernel off hardware-poisoned pages Breno Leitao
2026-08-04 14:36 ` Kiryl Shutsemau
2026-08-06 14:59   ` Breno Leitao

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