[PATCH v2 5/6] efi: record hardware-poisoned frames into the poisoned-memory table
From: Breno Leitao
Date: Fri Aug 21 2026 - 06:18:35 EST
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.
memory_failure() has already taken the frame out of this kernel's
allocator, so only the cross-kexec record happens here.
Suggested-by: Kiryl Shutsemau <kas@xxxxxxxxxx>
Signed-off-by: Breno Leitao <leitao@xxxxxxxxxx>
---
drivers/firmware/efi/Makefile | 1 +
drivers/firmware/efi/poison.c | 130 ++++++++++++++++++++++++++++++++++++++++++
include/linux/efi.h | 6 ++
mm/memory-failure.c | 3 +
4 files changed, 140 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/poison.c b/drivers/firmware/efi/poison.c
new file mode 100644
index 0000000000000..2f47293a4e45f
--- /dev/null
+++ b/drivers/firmware/efi/poison.c
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Runtime handling for the LINUX_EFI_POISONED_MEMORY configuration table: a
+ * bitmap with one bit per EFI_POISON_UNIT_SIZE of physical memory that records
+ * hardware-poisoned frames so the next kexec kernel can keep them out of its
+ * allocator. The stub installs the (empty) bitmap; this kernel sets bits at
+ * runtime.
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2026 Breno Leitao <leitao@xxxxxxxxxx>
+ */
+
+#define pr_fmt(fmt) "efi: " fmt
+
+#include <linux/bitmap.h>
+#include <linux/efi.h>
+#include <linux/io.h>
+#include <linux/log2.h>
+#include <linux/mm.h>
+#include <linux/overflow.h>
+
+static struct linux_efi_poisoned_memory *efi_poison __ro_after_init;
+
+/* A non-empty bitmap on a power-of-2 grid that phys_base actually sits on. */
+static bool __init
+efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)
+{
+ if (!pm->size)
+ 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 range the bitmap claims to describe has to fit in a u64. */
+static bool __init
+efi_poison_range_valid(const struct linux_efi_poisoned_memory *pm)
+{
+ u64 nbits, span;
+
+ if (check_mul_overflow(pm->size, (u64)BITS_PER_BYTE, &nbits))
+ return false;
+ if (check_mul_overflow(nbits, (u64)pm->unit_size, &span))
+ return false;
+
+ return !check_add_overflow(pm->phys_base, span, &span);
+}
+
+/*
+ * The table may have been installed by an earlier kernel in the kexec chain,
+ * so check its geometry before doing any arithmetic with 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) || !efi_poison_range_valid(pm)) {
+ pr_warn("Ignoring malformed poisoned-memory table\n");
+ return false;
+ }
+
+ return true;
+}
+
+static int __init efi_poison_init(void)
+{
+ struct linux_efi_poisoned_memory *pm;
+ u64 size;
+
+ if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
+ return 0;
+
+ /* Map the header to learn the bitmap size, then map the whole table. */
+ pm = memremap(efi.poisoned_memory, sizeof(*pm), MEMREMAP_WB);
+ if (WARN_ON_ONCE(!pm))
+ return 0;
+ if (!efi_poison_table_valid(pm)) {
+ memunmap(pm);
+ return 0;
+ }
+ size = pm->size;
+ memunmap(pm);
+
+ efi_poison = memremap(efi.poisoned_memory, sizeof(*pm) + size,
+ MEMREMAP_WB);
+ WARN_ON_ONCE(!efi_poison);
+ return 0;
+}
+early_initcall(efi_poison_init);
+
+/* Bitmap unit covering @pfn, or -1 if the pfn falls outside the table. */
+static long efi_poison_unit(unsigned long pfn)
+{
+ phys_addr_t addr = PFN_PHYS(pfn);
+ u64 unit;
+
+ if (addr < efi_poison->phys_base)
+ return -1;
+ unit = (addr - efi_poison->phys_base) / efi_poison->unit_size;
+ if (unit >= (u64)efi_poison->size * BITS_PER_BYTE)
+ return -1;
+ return unit;
+}
+
+/*
+ * Record a hardware-poisoned frame so the next kernel keeps its unit out of the
+ * allocator. memory_failure() has already removed the frame from this kernel.
+ *
+ * A bit is never cleared: one bit 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)
+{
+ long unit;
+
+ if (!efi_poison)
+ return;
+
+ unit = efi_poison_unit(pfn);
+ if (unit < 0)
+ return;
+
+ set_bit(unit, efi_poison->bitmap);
+}
diff --git a/include/linux/efi.h b/include/linux/efi.h
index ce0980a5bb81b..f03b1bb576133 100644
--- a/include/linux/efi.h
+++ b/include/linux/efi.h
@@ -1284,6 +1284,12 @@ struct linux_efi_poisoned_memory {
#define EFI_POISON_UNIT_SIZE SZ_2M
+#ifdef CONFIG_EFI_POISONED_MEMORY
+void efi_hwpoison_record_pfn(unsigned long pfn);
+#else
+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 aaf14608b30e2..357a72ffda625 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>
@@ -1286,6 +1287,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