num_poisoned_pages_inc() and num_poisoned_pages_sub() are the single choke point for every poison and unpoison event. Hook them so each hardware-poisoned frame is appended to the LINUX_EFI_POISONED_MEMORY table, and each unpoison tombstones its entry so a frame that is good again is not carried forward. The append mirrors efi_mem_reserve_persistent(): claim a slot in an existing list entry, or allocate and link a new list page -- itself reserved via memreserve so it survives to the next kernel. 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 --- drivers/firmware/efi/Makefile | 1 + drivers/firmware/efi/poison.c | 127 ++++++++++++++++++++++++++++++++++++++++++ include/linux/efi.h | 8 +++ mm/memory-failure.c | 6 +- 4 files changed, 141 insertions(+), 1 deletion(-) 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/poison.c b/drivers/firmware/efi/poison.c new file mode 100644 index 0000000000000..ed3b8cee21ba6 --- /dev/null +++ b/drivers/firmware/efi/poison.c @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Runtime handling for the LINUX_EFI_POISONED_MEMORY configuration table: + * record and clear poisoned frames so the next kexec kernel can keep them out + * of its allocator. + * + * Copyright (c) 2026 Meta Platforms, Inc. and affiliates. + * Copyright (c) 2026 Breno Leitao + */ + +#define pr_fmt(fmt) "efi: " fmt + +#include +#include +#include +#include +#include + +static struct linux_efi_poisoned_memory *efi_poisoned_memory_root __ro_after_init; +static DEFINE_SPINLOCK(efi_poisoned_memory_lock); + +static int __init efi_poisoned_memory_map_root(void) +{ + if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR) + return -ENODEV; + + efi_poisoned_memory_root = memremap(efi.poisoned_memory, + sizeof(*efi_poisoned_memory_root), + MEMREMAP_WB); + if (WARN_ON_ONCE(!efi_poisoned_memory_root)) + return -ENOMEM; + return 0; +} + +static int __init efi_poisoned_memory_root_init(void) +{ + if (efi_poisoned_memory_root) + return 0; + if (efi_poisoned_memory_map_root()) + efi_poisoned_memory_root = (void *)ULONG_MAX; + return 0; +} +early_initcall(efi_poisoned_memory_root_init); + +/* + * Record a hardware-poisoned frame so the next kernel can keep it out of its + * allocator. memory_failure() has already removed it from this kernel, so only + * the cross-kexec record is needed here. + */ +void efi_hwpoison_record_pfn(unsigned long pfn) +{ + phys_addr_t addr = PFN_PHYS(pfn); + struct linux_efi_poisoned_memory *pm; + unsigned long ppm; + int index; + + if (!efi_poisoned_memory_root || + efi_poisoned_memory_root == (void *)ULONG_MAX) + return; + + /* Try to claim a slot in an existing list entry. */ + for (ppm = efi_poisoned_memory_root->next; ppm; ) { + pm = memremap(ppm, sizeof(*pm), MEMREMAP_WB); + if (!pm) + return; + index = atomic_fetch_add_unless(&pm->count, 1, pm->size); + if (index < pm->size) { + pm->entry[index].base = addr; + pm->entry[index].size = PAGE_SIZE; + memunmap(pm); + return; + } + ppm = pm->next; + memunmap(pm); + } + + /* + * No slot free - allocate a new list entry and link it in. The page + * stays allocated for the rest of this boot, and the next kernel + * reserves it while parsing the EFI configuration tables, so no + * separate cross-kexec reservation is needed here. + */ + pm = (void *)__get_free_page(GFP_ATOMIC); + if (!pm) + return; + + pm->size = EFI_POISONED_MEMORY_COUNT(SZ_4K); + atomic_set(&pm->count, 1); + pm->entry[0].base = addr; + pm->entry[0].size = PAGE_SIZE; + + spin_lock(&efi_poisoned_memory_lock); + pm->next = efi_poisoned_memory_root->next; + efi_poisoned_memory_root->next = __pa(pm); + spin_unlock(&efi_poisoned_memory_lock); +} + +/* + * A frame was unpoisoned (typically the hwpoison injector under test). + * Tombstone its entry so the next kernel does not reserve a now-good frame. + */ +void efi_hwpoison_unrecord_pfn(unsigned long pfn) +{ + phys_addr_t addr = PFN_PHYS(pfn); + struct linux_efi_poisoned_memory *pm; + unsigned long ppm; + int i; + + if (!efi_poisoned_memory_root || + efi_poisoned_memory_root == (void *)ULONG_MAX) + return; + + for (ppm = efi_poisoned_memory_root->next; ppm; ) { + pm = memremap(ppm, sizeof(*pm), MEMREMAP_WB); + if (!pm) + return; + for (i = 0; i < atomic_read(&pm->count); i++) { + if (pm->entry[i].base == addr) { + pm->entry[i].size = 0; + memunmap(pm); + return; + } + } + ppm = pm->next; + memunmap(pm); + } +} diff --git a/include/linux/efi.h b/include/linux/efi.h index 7787eb8d4e4c1..bd1b0934881fb 100644 --- a/include/linux/efi.h +++ b/include/linux/efi.h @@ -1292,6 +1292,14 @@ struct linux_efi_poisoned_memory { (((size) - sizeof(struct linux_efi_poisoned_memory)) \ / sizeof_field(struct linux_efi_poisoned_memory, entry[0])) +#ifdef CONFIG_EFI_POISONED_MEMORY +void efi_hwpoison_record_pfn(unsigned long pfn); +void efi_hwpoison_unrecord_pfn(unsigned long pfn); +#else +static inline void efi_hwpoison_record_pfn(unsigned long pfn) { } +static inline void efi_hwpoison_unrecord_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 aaf14608b30e2..03fa921ef1b7a 100644 --- a/mm/memory-failure.c +++ b/mm/memory-failure.c @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -87,13 +88,16 @@ void num_poisoned_pages_inc(unsigned long pfn) { atomic_long_inc(&num_poisoned_pages); memblk_nr_poison_inc(pfn); + efi_hwpoison_record_pfn(pfn); } void num_poisoned_pages_sub(unsigned long pfn, long i) { atomic_long_sub(i, &num_poisoned_pages); - if (pfn != -1UL) + if (pfn != -1UL) { memblk_nr_poison_sub(pfn, i); + efi_hwpoison_unrecord_pfn(pfn); + } } /** -- 2.53.0-Meta