From c711461303af9f796b293b5ac06f49a885e46e71 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Wed, 12 Aug 2026 04:31:51 -0700 Subject: [PATCH 1/5] 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/5] 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. From 2792b4b15f148b142a4302706410e520c8049c00 Mon Sep 17 00:00:00 2001 From: Mukesh Pilaniya Date: Fri, 21 Aug 2026 19:19:57 +0530 Subject: [PATCH 3/5] kexec: return -ENOEXEC from image probe functions on mismatch Several kexec_file_load() image probe functions return -EINVAL when they do not recognize the image format. A probe function that rejects an image should return -ENOEXEC to indicate that the image is not a recognized executable format. -EINVAL implies a problem with the syscall parameters, not with image recognition. kexec_image_probe_default() iterates through registered loaders and returns the last probe's error code to the caller. That error propagates as the kexec_file_load() return value to userspace. Returning -EINVAL from a probe when no loader matches is semantically incorrect and misleads userspace about the nature of the failure. Return -ENOEXEC from all probe functions and their helpers when the image format is not recognized. Signed-off-by: Mukesh Pilaniya Reviewed-by: Philipp Rudo Reviewed-by: Pratyush Yadav Reviewed-by: Bradley Morgan Link: https://patch.msgid.link/20260821-mpilaniy-v3-1-76b355399d2e@redhat.com Signed-off-by: Mike Rapoport (Microsoft) --- arch/arm64/kernel/kexec_image.c | 4 ++-- arch/loongarch/kernel/kexec_efi.c | 4 ++-- arch/riscv/kernel/kexec_image.c | 4 ++-- kernel/kexec_elf.c | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/arch/arm64/kernel/kexec_image.c b/arch/arm64/kernel/kexec_image.c index b70f4df15a1a..101259874d44 100644 --- a/arch/arm64/kernel/kexec_image.c +++ b/arch/arm64/kernel/kexec_image.c @@ -25,10 +25,10 @@ static int image_probe(const char *kernel_buf, unsigned long kernel_len) (const struct arm64_image_header *)(kernel_buf); if (!h || (kernel_len < sizeof(*h))) - return -EINVAL; + return -ENOEXEC; if (memcmp(&h->magic, ARM64_IMAGE_MAGIC, sizeof(h->magic))) - return -EINVAL; + return -ENOEXEC; return 0; } diff --git a/arch/loongarch/kernel/kexec_efi.c b/arch/loongarch/kernel/kexec_efi.c index 5ee78ebb1546..28a1d0420ba3 100644 --- a/arch/loongarch/kernel/kexec_efi.c +++ b/arch/loongarch/kernel/kexec_efi.c @@ -24,12 +24,12 @@ static int efi_kexec_probe(const char *kernel_buf, unsigned long kernel_len) if (!h || (kernel_len < sizeof(*h))) { kexec_dprintk("No LoongArch image header.\n"); - return -EINVAL; + return -ENOEXEC; } if (!loongarch_header_check_dos_sig(h)) { kexec_dprintk("No LoongArch PE image header.\n"); - return -EINVAL; + return -ENOEXEC; } return 0; diff --git a/arch/riscv/kernel/kexec_image.c b/arch/riscv/kernel/kexec_image.c index 51dc89259f16..963a25f5b55b 100644 --- a/arch/riscv/kernel/kexec_image.c +++ b/arch/riscv/kernel/kexec_image.c @@ -20,7 +20,7 @@ static int image_probe(const char *kernel_buf, unsigned long kernel_len) const struct riscv_image_header *h = (const struct riscv_image_header *)kernel_buf; if (!h || kernel_len < sizeof(*h)) - return -EINVAL; + return -ENOEXEC; /* According to Documentation/arch/riscv/boot-image-header.rst, * use "magic2" field to check when version >= 0.2. @@ -28,7 +28,7 @@ static int image_probe(const char *kernel_buf, unsigned long kernel_len) if (h->version >= RISCV_HEADER_VERSION && memcmp(&h->magic2, RISCV_IMAGE_MAGIC2, sizeof(h->magic2))) - return -EINVAL; + return -ENOEXEC; return 0; } diff --git a/kernel/kexec_elf.c b/kernel/kexec_elf.c index 3a5c25b2adc9..89a444a00693 100644 --- a/kernel/kexec_elf.c +++ b/kernel/kexec_elf.c @@ -172,7 +172,7 @@ static int elf_read_ehdr(const char *buf, size_t len, struct elfhdr *ehdr) default: pr_debug("Unknown ELF class.\n"); - return -EINVAL; + return -ENOEXEC; } return elf_is_ehdr_sane(ehdr, len) ? 0 : -ENOEXEC; @@ -236,7 +236,7 @@ static int elf_read_phdr(const char *buf, size_t len, default: pr_debug("Unknown ELF class.\n"); - return -EINVAL; + return -ENOEXEC; } return elf_is_phdr_sane(phdr, len) ? 0 : -ENOEXEC; From b7c94f4d0961e3ea8ce0463b6eda1bb2cf9f2694 Mon Sep 17 00:00:00 2001 From: Mukesh Pilaniya Date: Fri, 21 Aug 2026 19:19:58 +0530 Subject: [PATCH 4/5] kexec: fix error propagation in kexec_image_probe_default() kexec_image_probe_default() overwrites ret with each loader's probe return value and returns whatever the last loader returned when no probe matches. The error code reaching userspace depends on whichever loader happens to be last in kexec_file_loaders[]. Only continue to the next loader when a probe returns -ENOEXEC. Propagate any other error such as -ENOMEM immediately. Return -ENOEXEC only when all loaders have been tried and none matched. Suggested-by: Pratyush Yadav Signed-off-by: Mukesh Pilaniya Reviewed-by: Pratyush Yadav Reviewed-by: Philipp Rudo Link: https://patch.msgid.link/20260821-mpilaniy-v3-2-76b355399d2e@redhat.com Signed-off-by: Mike Rapoport (Microsoft) --- kernel/kexec_file.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/kernel/kexec_file.c b/kernel/kexec_file.c index 59fb9d71e9d8..c11a815e2235 100644 --- a/kernel/kexec_file.c +++ b/kernel/kexec_file.c @@ -68,17 +68,19 @@ int kexec_image_probe_default(struct kimage *image, void *buf, unsigned long buf_len) { const struct kexec_file_ops * const *fops; - int ret = -ENOEXEC; for (fops = &kexec_file_loaders[0]; *fops && (*fops)->probe; ++fops) { - ret = (*fops)->probe(buf, buf_len); - if (!ret) { + int err = (*fops)->probe(buf, buf_len); + + if (!err) { image->fops = *fops; - return ret; + return 0; } + if (err != -ENOEXEC) + return err; } - return ret; + return -ENOEXEC; } static void *kexec_image_load_default(struct kimage *image) From 06ce2863e7d28bcee539b90c87960edda742fe18 Mon Sep 17 00:00:00 2001 From: Sourabh Jain Date: Sat, 5 Sep 2026 20:28:35 +0530 Subject: [PATCH 5/5] kexec_file: remove unused arch_kexec_locate_mem_hole hook No architecture provides an implementation of arch_kexec_locate_mem_hole(). Use kexec_locate_mem_hole() directly and remove the unused fallback wrapper from the kexec header. This simplifies the kexec memory allocation path without changing its behavior. No functional changes intended. The arch hook was introduced by commit f891f19736bd ("kexec_file: Allow archs to handle special regions while locating memory hole"), and its last user was removed by commit 6e5250eaa665 ("powerpc/crash: use generic APIs to locate memory hole for kdump"). Cc: Albert Ou Cc: Andrew Morton Cc: Mike Rapoport Cc: Palmer Dabbelt Cc: Pasha Tatashin Cc: Paul Walmsley Cc: Pratyush Yadav Cc: linux-riscv@lists.infradead.org Cc: kexec@lists.infradead.org Acked-by: Baoquan He Reviewed-by: Mukesh Pilaniya Signed-off-by: Sourabh Jain Link: https://patch.msgid.link/20260905145835.501635-1-sourabhjain@linux.ibm.com Signed-off-by: Mike Rapoport (Microsoft) --- arch/riscv/kernel/kexec_elf.c | 2 +- include/linux/kexec.h | 15 --------------- kernel/kexec_file.c | 2 +- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/arch/riscv/kernel/kexec_elf.c b/arch/riscv/kernel/kexec_elf.c index 3e9a32acb8f2..ca5abbad59e5 100644 --- a/arch/riscv/kernel/kexec_elf.c +++ b/arch/riscv/kernel/kexec_elf.c @@ -91,7 +91,7 @@ static int elf_find_pbase(struct kimage *image, unsigned long kernel_len, kbuf.memsz = ALIGN(kernel_len, PAGE_SIZE); kbuf.cma = NULL; kbuf.top_down = false; - ret = arch_kexec_locate_mem_hole(&kbuf); + ret = kexec_locate_mem_hole(&kbuf); if (!ret) { *old_pbase = lowest_paddr; *new_pbase = kbuf.mem; diff --git a/include/linux/kexec.h b/include/linux/kexec.h index 0af8ae4fdd08..e5f1cfc11fef 100644 --- a/include/linux/kexec.h +++ b/include/linux/kexec.h @@ -259,21 +259,6 @@ int kexec_kernel_verify_pe_sig(const char *kernel, unsigned long kernel_len); extern int kexec_add_buffer(struct kexec_buf *kbuf); int kexec_locate_mem_hole(struct kexec_buf *kbuf); -#ifndef arch_kexec_locate_mem_hole -/** - * arch_kexec_locate_mem_hole - Find free memory to place the segments. - * @kbuf: Parameters for the memory search. - * - * On success, kbuf->mem will have the start address of the memory region found. - * - * Return: 0 on success, negative errno on error. - */ -static inline int arch_kexec_locate_mem_hole(struct kexec_buf *kbuf) -{ - return kexec_locate_mem_hole(kbuf); -} -#endif - #ifndef arch_kexec_apply_relocations_add /* * arch_kexec_apply_relocations_add - apply relocations of type RELA diff --git a/kernel/kexec_file.c b/kernel/kexec_file.c index 8b0fc8a5d3c3..da26123fbedf 100644 --- a/kernel/kexec_file.c +++ b/kernel/kexec_file.c @@ -802,7 +802,7 @@ int kexec_add_buffer(struct kexec_buf *kbuf) kbuf->cma = NULL; /* Walk the RAM ranges and allocate a suitable range for the buffer */ - ret = arch_kexec_locate_mem_hole(kbuf); + ret = kexec_locate_mem_hole(kbuf); if (ret) return ret;