[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