From c711461303af9f796b293b5ac06f49a885e46e71 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Wed, 12 Aug 2026 04:31:51 -0700 Subject: [PATCH 1/2] kexec_file: stop the top-down search before it underflows locate_mem_hole_top_down() walks candidates downwards by subtracting PAGE_SIZE whenever the window conflicts with an existing segment or with an architecture exclude range. Nothing stops that subtraction at zero, so a search that reaches the bottom of the address space wraps temp_start around and continues. The walk starts inside the range being scanned and only moves down, so bail out once a candidate ends up above end. That covers every step in the loop rather than the subtractions alone, and it matches locate_mem_hole_bottom_up(), which already bounds its candidate on both sides. This is a better check than subtracting with check_sub_overflow(), given that we would have 3 subtractions in this block, and this single fix would take care of them all (instead of three check_sub_overflow()). Fixes: cb1052581e2b ("kexec: implementation of new syscall kexec_file_load") Signed-off-by: Breno Leitao Reviewed-by: Bradley Morgan Link: https://patch.msgid.link/20260812-kexec_posioned-v6-1-e477887086f0@debian.org Signed-off-by: Mike Rapoport (Microsoft) --- kernel/kexec_file.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kernel/kexec_file.c b/kernel/kexec_file.c index 59fb9d71e9d8..01a64d98fbcd 100644 --- a/kernel/kexec_file.c +++ b/kernel/kexec_file.c @@ -484,7 +484,9 @@ static int locate_mem_hole_top_down(unsigned long start, unsigned long end, /* align down start */ temp_start = ALIGN_DOWN(temp_start, kbuf->buf_align); - if (temp_start < start || temp_start < kbuf->buf_min) + /* A candidate above the range means the walk wrapped around */ + if (temp_start < start || temp_start < kbuf->buf_min || + temp_start > end) return 0; temp_end = temp_start + kbuf->memsz - 1; From c5c9a4e14bfa7163ef77c95272c7949ae1501134 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Wed, 12 Aug 2026 04:31:52 -0700 Subject: [PATCH 2/2] kexec: keep the next kernel off hardware-poisoned pages 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 a poisoned frame, the relocation copy puts it on memory that is known bad. The error happens on the first read from a bad page, and that is what we want to avoid. Skip hardware-poisoned frames that were detected by the memory failure subsystem earlier when placing kexec segments. To do so, add a helper that reports the first or the last poisoned page in a range: memory is walked top-down by locate_mem_hole_top_down() and bottom-up by locate_mem_hole_bottom_up(), so each direction needs a different answer to stay clear of the poison. kexec_load() gets its destinations from userspace and cannot move them, so there sanity_check_segment_list() just rejects a segment that happens to have a poisoned page. is_page_hwpoison() also covers hugetlb, so a poisoned hugetlb folio is skipped as a whole. Suggested-by: Kiryl Shutsemau Signed-off-by: Breno Leitao Reviewed-by: Kiryl Shutsemau (Meta) Reviewed-by: Pratyush Yadav Reviewed-by: Bradley Morgan Reviewed-by: Miaohe Lin Link: https://patch.msgid.link/20260812-kexec_posioned-v6-2-e477887086f0@debian.org Signed-off-by: Mike Rapoport (Microsoft) --- include/linux/mm.h | 14 ++++++++++++++ kernel/kexec_core.c | 10 ++++++++++ kernel/kexec_file.c | 16 ++++++++++++++++ mm/memory-failure.c | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/include/linux/mm.h b/include/linux/mm.h index dd09c438fa23..f0ff4c9c5489 100644 --- a/include/linux/mm.h +++ b/include/linux/mm.h @@ -5254,6 +5254,8 @@ 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_first_hwpoison(phys_addr_t start, unsigned long size); +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) { @@ -5266,6 +5268,18 @@ 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_first_hwpoison(phys_addr_t start, + unsigned long size) +{ + return PHYS_ADDR_MAX; +} + +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 dc770b9a6d05..7ee8c9f078f6 100644 --- a/kernel/kexec_core.c +++ b/kernel/kexec_core.c @@ -212,6 +212,16 @@ 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++) { + if (range_first_hwpoison(image->segment[i].mem, + image->segment[i].memsz) != PHYS_ADDR_MAX) + return -EHWPOISON; + } + /* * 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 01a64d98fbcd..8b0fc8a5d3c3 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; @@ -506,6 +507,13 @@ static int locate_mem_hole_top_down(unsigned long start, unsigned long end, continue; } + poison = range_first_hwpoison(temp_start, kbuf->memsz); + if (poison != PHYS_ADDR_MAX) { + /* we hit a poisoned page */ + temp_start = poison - kbuf->memsz; + continue; + } + /* We found a suitable memory range */ break; } while (1); @@ -522,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); @@ -548,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 a8b03e2920ba..a2ca8df501ca 100644 --- a/mm/memory-failure.c +++ b/mm/memory-failure.c @@ -96,6 +96,46 @@ void num_poisoned_pages_sub(unsigned long pfn, long i) memblk_nr_poison_sub(pfn, i); } +/* + * Return the first or the last hardware-poisoned online page in [start, + * start + size), or PHYS_ADDR_MAX if the range is clean. + */ +static phys_addr_t range_hwpoison(phys_addr_t start, unsigned long size, + bool first) +{ + 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 && is_page_hwpoison(page)) { + if (first) + return PFN_PHYS(pfn); + + poison = PFN_PHYS(pfn); + } + + cond_resched(); + } + + return poison; +} + +phys_addr_t range_first_hwpoison(phys_addr_t start, unsigned long size) +{ + return range_hwpoison(start, size, true); +} + +phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size) +{ + return range_hwpoison(start, size, false); +} + /** * MF_ATTR_RO - Create sysfs entry for each memory failure statistics. * @_name: name of the file in the per NUMA sysfs directory.