action_result() is where memory_failure() reports the outcome of a hard offline, so hook it to set the frame's bit in the LINUX_EFI_POISONED_MEMORY bitmap.
Soft-offlined pages reach num_poisoned_pages_inc() through page_handle_poison() and are deliberately left out: they are still functional and were offlined predictively, so recording them would turn a prediction into a permanent loss for every kernel further down the kexec chain. A bit is only ever set, never cleared, given that multiple pages can set the same bit, and it is not trivial to decide if the bit should be unset when a page is unrecorded. Unpoisoning a frame therefore does not hand its unit back to the next kernel. That is a known limitation. The table is EFI ACPI reclaim memory, which becomes E820_TYPE_ACPI and so reaches neither memblock nor the direct map; touching it then faults. Hand its pages to memblock from efi_config_parse_tables() the way the unaccepted memory table already does, and vet the inherited header in the same pass, so everything afterwards can reach a table it can trust with phys_to_virt(). memory_failure() has already taken the frame out of this kernel's allocator, so only the cross-kexec record happens here. Signed-off-by: Breno Leitao <[email protected]> --- drivers/firmware/efi/Makefile | 1 + drivers/firmware/efi/efi.c | 2 + drivers/firmware/efi/poison.c | 115 ++++++++++++++++++++++++++++++++++++++++++ include/linux/efi.h | 8 +++ mm/memory-failure.c | 3 ++ 5 files changed, 129 insertions(+) diff --git a/drivers/firmware/efi/Makefile b/drivers/firmware/efi/Makefile index 8efbcf699e4ff..05d0a490923e5 100644 --- a/drivers/firmware/efi/Makefile +++ b/drivers/firmware/efi/Makefile @@ -43,4 +43,5 @@ obj-$(CONFIG_EFI_EARLYCON) += earlycon.o obj-$(CONFIG_UEFI_CPER_ARM) += cper-arm.o obj-$(CONFIG_UEFI_CPER_X86) += cper-x86.o obj-$(CONFIG_UNACCEPTED_MEMORY) += unaccepted_memory.o +obj-$(CONFIG_EFI_POISONED_MEMORY) += poison.o obj-$(CONFIG_TEE_STMM_EFI) += stmm/tee_stmm_efi.o diff --git a/drivers/firmware/efi/efi.c b/drivers/firmware/efi/efi.c index af1fa443839c4..55b2ee53fc268 100644 --- a/drivers/firmware/efi/efi.c +++ b/drivers/firmware/efi/efi.c @@ -883,6 +883,8 @@ int __init efi_config_parse_tables(const efi_config_table_t *config_tables, } } + efi_poisoned_memory_reserve(); + return 0; } diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c new file mode 100644 index 0000000000000..c18edf111c710 --- /dev/null +++ b/drivers/firmware/efi/poison.c @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Runtime side of the LINUX_EFI_POISONED_MEMORY table: one bit per + * EFI_POISON_UNIT_SIZE, set here as frames go bad, honored by the next kernel. + * + * Copyright (c) 2026 Meta Platforms, Inc. and affiliates. + * Copyright (c) 2026 Breno Leitao <[email protected]> + */ + +#define pr_fmt(fmt) "efi: " fmt + +#include <linux/bitmap.h> +#include <linux/efi.h> +#include <linux/io.h> +#include <linux/log2.h> +#include <linux/memblock.h> +#include <linux/mm.h> +#include <linux/overflow.h> + +static bool __init +efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm) +{ + u64 nbits; + + /* Whole words, and a bit count that can be taken without wrapping. */ + if (!pm->size || !IS_ALIGNED(pm->size, sizeof(unsigned long)) || + check_mul_overflow(pm->size, (u64)BITS_PER_BYTE, &nbits)) + return false; + + if (pm->unit_size < PAGE_SIZE || !is_power_of_2(pm->unit_size)) + return false; + + return IS_ALIGNED(pm->phys_base, pm->unit_size); +} + +/* The table may come from an earlier kernel, so vet it before using it. */ +static bool __init +efi_poison_table_valid(const struct linux_efi_poisoned_memory *pm) +{ + if (pm->version != 1) { + pr_warn("Ignoring poisoned-memory table with version %u\n", + pm->version); + return false; + } + + if (!efi_poison_geometry_valid(pm)) { + pr_warn("Ignoring malformed poisoned-memory table\n"); + return false; + } + + return true; +} + +/* + * Vet the inherited table and hand its pages to memblock, the way the + * unaccepted memory table is handled. It is EFI ACPI reclaim memory, which + * becomes E820_TYPE_ACPI and would otherwise stay out of the direct map, and + * touching it then faults. Called from efi_config_parse_tables(), so + * everything later can reach it with efi_poisoned_memory(). + */ +void __init efi_poisoned_memory_reserve(void) +{ + struct linux_efi_poisoned_memory *pm; + phys_addr_t start, end; + + if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR) + return; + + pm = early_memremap(efi.poisoned_memory, sizeof(*pm)); + if (!pm) { + pr_warn("Could not map poisoned-memory table\n"); + efi.poisoned_memory = EFI_INVALID_TABLE_ADDR; + return; + } + + if (!efi_poison_table_valid(pm)) { + efi.poisoned_memory = EFI_INVALID_TABLE_ADDR; + early_memunmap(pm, sizeof(*pm)); + return; + } + + start = PAGE_ALIGN_DOWN(efi.poisoned_memory); + end = PAGE_ALIGN(efi.poisoned_memory + sizeof(*pm) + pm->size); + early_memunmap(pm, sizeof(*pm)); + + memblock_add(start, end - start); + memblock_reserve(start, end - start); +} + +/* The table, vetted at parse time, or NULL if this boot has none. */ +static struct linux_efi_poisoned_memory *efi_poisoned_memory(void) +{ + if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR) + return NULL; + + return phys_to_virt(efi.poisoned_memory); +} + +/* + * A bit is never cleared: it stands for a whole EFI_POISON_UNIT_SIZE, so an + * unpoison cannot tell whether the unit as a whole is good again. + */ +void efi_hwpoison_record_pfn(unsigned long pfn) +{ + struct linux_efi_poisoned_memory *pm = efi_poisoned_memory(); + phys_addr_t addr = PFN_PHYS(pfn); + u64 unit; + + if (!pm || addr < pm->phys_base) + return; + + unit = (addr - pm->phys_base) / pm->unit_size; + if (unit < pm->size * BITS_PER_BYTE) + set_bit(unit, pm->bitmap); +} diff --git a/include/linux/efi.h b/include/linux/efi.h index efaf63f9a54ed..56402fdccd114 100644 --- a/include/linux/efi.h +++ b/include/linux/efi.h @@ -1286,6 +1286,14 @@ struct linux_efi_poisoned_memory { #define EFI_POISON_UNIT_SIZE SZ_2M +#ifdef CONFIG_EFI_POISONED_MEMORY +void __init efi_poisoned_memory_reserve(void); +void efi_hwpoison_record_pfn(unsigned long pfn); +#else +static inline void efi_poisoned_memory_reserve(void) { } +static inline void efi_hwpoison_record_pfn(unsigned long pfn) { } +#endif + void __init efi_arch_mem_reserve(phys_addr_t addr, u64 size); /* diff --git a/mm/memory-failure.c b/mm/memory-failure.c index a2ca8df501cae..d9b8be696aac3 100644 --- a/mm/memory-failure.c +++ b/mm/memory-failure.c @@ -43,6 +43,7 @@ #include <linux/sched/signal.h> #include <linux/sched/task.h> #include <linux/dax.h> +#include <linux/efi.h> #include <linux/ksm.h> #include <linux/rmap.h> #include <linux/export.h> @@ -1326,6 +1327,8 @@ static int action_result(unsigned long pfn, enum mf_action_page_type type, if (type != MF_MSG_ALREADY_POISONED && type != MF_MSG_PFN_MAP) { num_poisoned_pages_inc(pfn); update_per_node_mf_stats(pfn, result); + /* Only hard offlines are carried over to the next kernel. */ + efi_hwpoison_record_pfn(pfn); } pr_err("%#lx: recovery action for %s: %s\n", -- 2.53.0-Meta
