[PATCH RFC 2/3] efi: record hardware-poisoned frames into the poisoned-memory table
Breno Leitao
leitao at debian.org
Fri Jul 17 07:03:04 PDT 2026
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 <leitao at debian.org>
---
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 <leitao at debian.org>
+ */
+
+#define pr_fmt(fmt) "efi: " fmt
+
+#include <linux/efi.h>
+#include <linux/io.h>
+#include <linux/mm.h>
+#include <linux/sizes.h>
+#include <linux/spinlock.h>
+
+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 <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>
@@ -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
More information about the kexec
mailing list