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 <[email protected]>
---
 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 <[email protected]>
+ */
+
+#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


Reply via email to