* [PATCH v3 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table
2026-08-26 12:03 [PATCH v3 0/5] efi: mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
@ 2026-08-26 12:03 ` Breno Leitao
2026-08-28 13:47 ` Kiryl Shutsemau
2026-08-26 12:03 ` [PATCH v3 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
` (3 subsequent siblings)
4 siblings, 1 reply; 10+ messages in thread
From: Breno Leitao @ 2026-08-26 12:03 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kas, kexec, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
kernel-team
Hardware-poisoned page frames are tracked only in the running kernel's
data structures, so a kexec loses them and the next kernel doesn't have
this information, thus, tripping into them again.
Add an EFI configuration table to carry that information across kexec.
It is a bitmap with one bit per EFI_POISON_UNIT_SIZE (2MiB) of physical
memory, modeled on the LINUX_EFI_UNACCEPTED_MEMORY table, and it rides
the EFI system table to every kernel in the chain.
The Kconfig symbol has no prompt. There is nothing for a user to decide,
so it is on wherever it can be, and it only costs 64K per TiB of RAM on
a kernel that already has the EFI stub and MEMORY_FAILURE. It is
restricted to 64BIT because the unit arithmetic would need div_u64() on
32-bit, and there is no 32-bit EFI configuration with MEMORY_FAILURE to
test that on.
Suggested-by: Kiryl Shutsemau <kas@kernel.org>
Signed-off-by: Breno Leitao <leitao@debian.org>
---
drivers/firmware/efi/Kconfig | 8 ++++++++
drivers/firmware/efi/efi.c | 6 ++++++
include/linux/efi.h | 13 +++++++++++++
3 files changed, 27 insertions(+)
diff --git a/drivers/firmware/efi/Kconfig b/drivers/firmware/efi/Kconfig
index 29e0729299f5b..aafcd41bc0063 100644
--- a/drivers/firmware/efi/Kconfig
+++ b/drivers/firmware/efi/Kconfig
@@ -263,6 +263,14 @@ config EFI_COCO_SECRET
virt/coco/efi_secret module to access the secrets, which in turn
allows userspace programs to access the injected secrets.
+config EFI_POISONED_MEMORY
+ def_bool y
+ depends on EFI_STUB && MEMORY_FAILURE && 64BIT
+ help
+ Record page frames that are hardware-poisoned while this kernel runs
+ into an EFI configuration table, and honor that table early on the
+ next kernel so a kexec does not hand known-bad RAM back out.
+
config OVMF_DEBUG_LOG
bool "Expose OVMF firmware debug log via sysfs"
depends on EFI
diff --git a/drivers/firmware/efi/efi.c b/drivers/firmware/efi/efi.c
index 0327a39d31fa5..111e60479211a 100644
--- a/drivers/firmware/efi/efi.c
+++ b/drivers/firmware/efi/efi.c
@@ -55,6 +55,9 @@ struct efi __read_mostly efi = {
#ifdef CONFIG_UNACCEPTED_MEMORY
.unaccepted = EFI_INVALID_TABLE_ADDR,
#endif
+#ifdef CONFIG_EFI_POISONED_MEMORY
+ .poisoned_memory = EFI_INVALID_TABLE_ADDR,
+#endif
};
EXPORT_SYMBOL(efi);
@@ -646,6 +649,9 @@ static const efi_config_table_type_t common_tables[] __initconst = {
#ifdef CONFIG_UNACCEPTED_MEMORY
{LINUX_EFI_UNACCEPTED_MEM_TABLE_GUID, &efi.unaccepted, "Unaccepted" },
#endif
+#ifdef CONFIG_EFI_POISONED_MEMORY
+ {LINUX_EFI_POISONED_MEMORY_TABLE_GUID, &efi.poisoned_memory, "POISON" },
+#endif
#ifdef CONFIG_EFI_GENERIC_STUB
{LINUX_EFI_PRIMARY_DISPLAY_TABLE_GUID, &primary_display_table },
#endif
diff --git a/include/linux/efi.h b/include/linux/efi.h
index b3c83516593d1..c7b4a37f760ec 100644
--- a/include/linux/efi.h
+++ b/include/linux/efi.h
@@ -23,6 +23,7 @@
#include <linux/pstore.h>
#include <linux/range.h>
#include <linux/reboot.h>
+#include <linux/sizes.h>
#include <linux/uuid.h>
#include <asm/page.h>
@@ -422,6 +423,7 @@ void efi_native_runtime_setup(void);
#define LINUX_EFI_COCO_SECRET_AREA_GUID EFI_GUID(0xadf956ad, 0xe98c, 0x484c, 0xae, 0x11, 0xb5, 0x1c, 0x7d, 0x33, 0x64, 0x47)
#define LINUX_EFI_BOOT_MEMMAP_GUID EFI_GUID(0x800f683f, 0xd08b, 0x423a, 0xa2, 0x93, 0x96, 0x5c, 0x3c, 0x6f, 0xe2, 0xb4)
#define LINUX_EFI_UNACCEPTED_MEM_TABLE_GUID EFI_GUID(0xd5d1de3c, 0x105c, 0x44f9, 0x9e, 0xa9, 0xbc, 0xef, 0x98, 0x12, 0x00, 0x31)
+#define LINUX_EFI_POISONED_MEMORY_TABLE_GUID EFI_GUID(0xaf828a15, 0x0ef4, 0x439a, 0xb8, 0x6a, 0xd6, 0xd6, 0x9e, 0xaf, 0xba, 0xfa)
#define RISCV_EFI_BOOT_PROTOCOL_GUID EFI_GUID(0xccd15fec, 0x6f73, 0x4eec, 0x83, 0x95, 0x3e, 0x69, 0xe4, 0xb9, 0x40, 0xbf)
@@ -650,6 +652,7 @@ extern struct efi {
unsigned long mokvar_table; /* MOK variable config table */
unsigned long coco_secret; /* Confidential computing secret table */
unsigned long unaccepted; /* Unaccepted memory table */
+ unsigned long poisoned_memory; /* Hardware-poisoned memory table */
efi_get_time_t *get_time;
efi_set_time_t *set_time;
@@ -1271,6 +1274,16 @@ struct linux_efi_memreserve {
#define EFI_MEMRESERVE_COUNT(size) (((size) - sizeof(struct linux_efi_memreserve)) \
/ sizeof_field(struct linux_efi_memreserve, entry[0]))
+/* Bit N covers unit N of physical address space, counting from address 0. */
+struct linux_efi_poisoned_memory {
+ u32 version;
+ u32 unit_size; /* bytes of phys space per bitmap bit */
+ u64 size; /* bitmap size in bytes */
+ unsigned long bitmap[];
+};
+
+#define EFI_POISON_UNIT_SIZE SZ_2M
+
void __init efi_arch_mem_reserve(phys_addr_t addr, u64 size);
/*
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 10+ messages in thread* Re: [PATCH v3 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table
2026-08-26 12:03 ` [PATCH v3 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
@ 2026-08-28 13:47 ` Kiryl Shutsemau
0 siblings, 0 replies; 10+ messages in thread
From: Kiryl Shutsemau @ 2026-08-28 13:47 UTC (permalink / raw)
To: Breno Leitao
Cc: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kexec, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-efi, linux-kernel,
linux-mm, rmikey, riel, kernel-team
On Wed, Aug 26, 2026 at 05:03:52AM -0700, Breno Leitao wrote:
> @@ -1271,6 +1274,16 @@ struct linux_efi_memreserve {
> #define EFI_MEMRESERVE_COUNT(size) (((size) - sizeof(struct linux_efi_memreserve)) \
> / sizeof_field(struct linux_efi_memreserve, entry[0]))
>
> +/* Bit N covers unit N of physical address space, counting from address 0. */
> +struct linux_efi_poisoned_memory {
> + u32 version;
> + u32 unit_size; /* bytes of phys space per bitmap bit */
> + u64 size; /* bitmap size in bytes */
> + unsigned long bitmap[];
> +};
struct efi_unaccepted_memory has 'phys_base'. Do we want it here too?
It can be helpful if physical memory starts very high for some reason.
I believe it is common for risc-v.
Tenstorrent Blackhole card has 4G of RAM based at 0x400030000000.
Counting from address 0 that is a 4M bitmap for 4G of RAM. :-/
--
Kiryl Shutsemau / Kirill A. Shutemov
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v3 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-08-26 12:03 [PATCH v3 0/5] efi: mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-08-26 12:03 ` [PATCH v3 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
@ 2026-08-26 12:03 ` Breno Leitao
2026-08-28 13:59 ` Kiryl Shutsemau
2026-08-26 12:03 ` [PATCH v3 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table Breno Leitao
` (2 subsequent siblings)
4 siblings, 1 reply; 10+ messages in thread
From: Breno Leitao @ 2026-08-26 12:03 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kas, kexec, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
kernel-team
A EFI config table can only be installed while boot services are still
up, so the stub has to create it; the running kernel can only flip bits
in a table that already exists.
Size the bitmap from the top of usable RAM, which efi_get_ram_top()
works out by walking the UEFI memory map, since the stub has no max_pfn.
Bit N covers unit N counting from address 0, so the table tracks
max_pfn. Memory the firmware hot-adds later sits above it and is not
carried across a kexec.
One table has to serve every architecture, so efi_get_ram_top() takes
the union of the memory types they turn into RAM: what setup_e820() maps
to E820_TYPE_RAM on x86, plus the EFI_ACPI_RECLAIM_MEMORY and
EFI_PERSISTENT_MEMORY that is_usable_memory() accepts on arm64. Sizing
wide only costs bitmap bytes; sizing narrow silently drops the records
for every frame above the top.
At one bit per 2M that is 64K per TiB, and 256M at the 4PB x86
architectural maximum. The 2M granule is called "unit" here, and the
table carries it so the granule can change later without breaking the
kernels already reading it.
Allocate it as EFI_ACPI_RECLAIM_MEMORY so the next kernel does not take
it for free RAM, and install it empty.
A table installed by an earlier boot rides the system table across kexec
and is reused as-is.
x86 does not go through efi_stub_common(), so the generic stub and the
x86 stub each need the call; on x86 it has to come before exit_boot(),
which is the last point a configuration table can be installed.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
drivers/firmware/efi/libstub/efi-stub-helper.c | 106 +++++++++++++++++++++++++
drivers/firmware/efi/libstub/efi-stub.c | 1 +
drivers/firmware/efi/libstub/efistub.h | 6 ++
drivers/firmware/efi/libstub/x86-stub.c | 2 +
4 files changed, 115 insertions(+)
diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
index f27f2e1f00199..b8b80846239d0 100644
--- a/drivers/firmware/efi/libstub/efi-stub-helper.c
+++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
@@ -774,3 +774,109 @@ void efi_remap_image(unsigned long image_base, unsigned alloc_size,
efi_warn("Failed to remap data region non-executable\n");
}
}
+
+#ifdef CONFIG_EFI_POISONED_MEMORY
+/* Erring wide only costs bitmap bytes, so the type list is permissive. */
+static efi_status_t efi_get_ram_top(u64 *top)
+{
+ struct efi_boot_memmap *map __free(efi_pool) = NULL;
+ efi_status_t status;
+ u64 ram_top = 0;
+ int i, nr_desc;
+
+ status = efi_get_memory_map(&map, false);
+ if (status != EFI_SUCCESS)
+ return status;
+
+ nr_desc = map->map_size / map->desc_size;
+ for (i = 0; i < nr_desc; i++) {
+ efi_memory_desc_t *d;
+
+ d = efi_memdesc_ptr((unsigned long)map->map, map->desc_size, i);
+ switch (d->type) {
+ case EFI_LOADER_CODE:
+ case EFI_LOADER_DATA:
+ case EFI_BOOT_SERVICES_CODE:
+ case EFI_BOOT_SERVICES_DATA:
+ case EFI_CONVENTIONAL_MEMORY:
+ case EFI_UNACCEPTED_MEMORY:
+ case EFI_ACPI_RECLAIM_MEMORY:
+ case EFI_PERSISTENT_MEMORY:
+ ram_top = max(ram_top,
+ d->phys_addr + d->num_pages * EFI_PAGE_SIZE);
+ break;
+ default:
+ break;
+ }
+ }
+ if (!ram_top)
+ return EFI_NOT_FOUND;
+
+ *top = ram_top;
+ return EFI_SUCCESS;
+}
+
+/* Whole words: the kernel reaches the bitmap with set_bit(). */
+static u64 efi_poison_bitmap_size(u64 ram_top)
+{
+ u64 bytes = DIV_ROUND_UP(DIV_ROUND_UP(ram_top, EFI_POISON_UNIT_SIZE),
+ BITS_PER_BYTE);
+
+ return round_up(bytes, sizeof(unsigned long));
+}
+
+/* ACPI reclaim memory, so the next kernel does not treat it as free RAM. */
+static struct linux_efi_poisoned_memory *efi_poison_alloc(u64 bitmap_size)
+{
+ struct linux_efi_poisoned_memory *pm;
+ efi_status_t status;
+
+ status = efi_bs_call(allocate_pool, EFI_ACPI_RECLAIM_MEMORY,
+ sizeof(*pm) + bitmap_size, (void **)&pm);
+ if (status != EFI_SUCCESS)
+ return NULL;
+
+ pm->version = 1;
+ pm->unit_size = EFI_POISON_UNIT_SIZE;
+ pm->size = bitmap_size;
+ memset(pm->bitmap, 0, bitmap_size);
+
+ return pm;
+}
+
+void install_poisoned_memory_table(void)
+{
+ efi_guid_t poisoned_memory_table_guid = LINUX_EFI_POISONED_MEMORY_TABLE_GUID;
+ struct linux_efi_poisoned_memory *pm;
+ u64 ram_top, bitmap_size;
+ efi_status_t status;
+
+ /* A table installed by an earlier boot rides the system table across kexec. */
+ pm = get_efi_config_table(poisoned_memory_table_guid);
+ if (pm) {
+ if (pm->version != 1)
+ efi_err("Unknown version of poisoned-memory table\n");
+ return;
+ }
+
+ if (efi_get_ram_top(&ram_top) != EFI_SUCCESS) {
+ efi_err("Failed to size the poisoned-memory table!\n");
+ return;
+ }
+
+ bitmap_size = efi_poison_bitmap_size(ram_top);
+
+ pm = efi_poison_alloc(bitmap_size);
+ if (!pm) {
+ efi_err("Failed to allocate poisoned-memory table!\n");
+ return;
+ }
+
+ status = efi_bs_call(install_configuration_table,
+ &poisoned_memory_table_guid, pm);
+ if (status != EFI_SUCCESS) {
+ efi_bs_call(free_pool, pm);
+ efi_err("Failed to install poisoned-memory config table!\n");
+ }
+}
+#endif
diff --git a/drivers/firmware/efi/libstub/efi-stub.c b/drivers/firmware/efi/libstub/efi-stub.c
index 42d6073bcd062..008635eb5027a 100644
--- a/drivers/firmware/efi/libstub/efi-stub.c
+++ b/drivers/firmware/efi/libstub/efi-stub.c
@@ -179,6 +179,7 @@ efi_status_t efi_stub_common(efi_handle_t handle,
EFI_RT_SUPPORTED_SET_VIRTUAL_ADDRESS_MAP);
install_memreserve_table();
+ install_poisoned_memory_table();
status = efi_boot_kernel(handle, image, image_addr, cmdline_ptr);
diff --git a/drivers/firmware/efi/libstub/efistub.h b/drivers/firmware/efi/libstub/efistub.h
index fd91fc15ec810..44436869c4efe 100644
--- a/drivers/firmware/efi/libstub/efistub.h
+++ b/drivers/firmware/efi/libstub/efistub.h
@@ -1169,6 +1169,12 @@ efi_enable_reset_attack_mitigation(void) { }
void efi_retrieve_eventlog(void);
+#ifdef CONFIG_EFI_POISONED_MEMORY
+void install_poisoned_memory_table(void);
+#else
+static inline void install_poisoned_memory_table(void) { }
+#endif
+
struct sysfb_display_info *alloc_primary_display(void);
struct sysfb_display_info *__alloc_primary_display(void);
void free_primary_display(struct sysfb_display_info *dpy);
diff --git a/drivers/firmware/efi/libstub/x86-stub.c b/drivers/firmware/efi/libstub/x86-stub.c
index cef32e2c82d8f..f90de11bc8855 100644
--- a/drivers/firmware/efi/libstub/x86-stub.c
+++ b/drivers/firmware/efi/libstub/x86-stub.c
@@ -1023,6 +1023,8 @@ void __noreturn efi_stub_entry(efi_handle_t handle,
setup_unaccepted_memory();
+ install_poisoned_memory_table();
+
status = exit_boot(boot_params, handle);
if (status != EFI_SUCCESS) {
efi_err("exit_boot() failed!\n");
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 10+ messages in thread* Re: [PATCH v3 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-08-26 12:03 ` [PATCH v3 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
@ 2026-08-28 13:59 ` Kiryl Shutsemau
0 siblings, 0 replies; 10+ messages in thread
From: Kiryl Shutsemau @ 2026-08-28 13:59 UTC (permalink / raw)
To: Breno Leitao
Cc: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kexec, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-efi, linux-kernel,
linux-mm, rmikey, riel, kernel-team
On Wed, Aug 26, 2026 at 05:03:53AM -0700, Breno Leitao wrote:
> A EFI config table can only be installed while boot services are still
> up, so the stub has to create it; the running kernel can only flip bits
> in a table that already exists.
>
> Size the bitmap from the top of usable RAM, which efi_get_ram_top()
> works out by walking the UEFI memory map, since the stub has no max_pfn.
> Bit N covers unit N counting from address 0, so the table tracks
> max_pfn. Memory the firmware hot-adds later sits above it and is not
> carried across a kexec.
>
> One table has to serve every architecture, so efi_get_ram_top() takes
> the union of the memory types they turn into RAM: what setup_e820() maps
> to E820_TYPE_RAM on x86, plus the EFI_ACPI_RECLAIM_MEMORY and
> EFI_PERSISTENT_MEMORY that is_usable_memory() accepts on arm64. Sizing
> wide only costs bitmap bytes; sizing narrow silently drops the records
> for every frame above the top.
x86 and ARM doesn't seem to agree what RAM is. On x86 it is by ->type
and on ARM it is by ->attribute (anything WB/WC/WT).
is_usable_memory() only decides nomap. Everything is_memory() lets in
lands in memblock.memory, and max_pfn is PFN_DOWN(memblock_end_of_DRAM()),
so the top of RAM there can be a type that is not on your list.
--
Kiryl Shutsemau / Kirill A. Shutemov
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v3 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
2026-08-26 12:03 [PATCH v3 0/5] efi: mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-08-26 12:03 ` [PATCH v3 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
2026-08-26 12:03 ` [PATCH v3 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
@ 2026-08-26 12:03 ` Breno Leitao
2026-08-28 14:11 ` Kiryl Shutsemau
2026-08-26 12:03 ` [PATCH v3 4/5] mm/memory-failure: add a helper to poison a frame at boot Breno Leitao
2026-08-26 12:03 ` [PATCH v3 5/5] mm/memory-failure: efi: replay the poisioned page in the next kernel Breno Leitao
4 siblings, 1 reply; 10+ messages in thread
From: Breno Leitao @ 2026-08-26 12:03 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kas, kexec, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
kernel-team
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@kernel.org>
Signed-off-by: Breno Leitao <leitao@debian.org>
---
drivers/firmware/efi/Makefile | 1 +
drivers/firmware/efi/poison.c | 126 ++++++++++++++++++++++++++++++++++++++++++
include/linux/efi.h | 6 ++
mm/memory-failure.c | 3 +
4 files changed, 136 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..d6855e712832c
--- /dev/null
+++ b/drivers/firmware/efi/poison.c
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Runtime side of the LINUX_EFI_POISONED_MEMORY table: one bit per
+ * EFI_POISON_UNIT_SIZE, set here as frames go bad, honored by the next kernel.
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2026 Breno Leitao <leitao@debian.org>
+ */
+
+#define pr_fmt(fmt) "efi: " fmt
+
+#include <linux/bitmap.h>
+#include <linux/efi.h>
+#include <linux/io.h>
+#include <linux/log2.h>
+#include <linux/memblock.h>
+#include <linux/mm.h>
+#include <linux/overflow.h>
+
+static struct linux_efi_poisoned_memory *efi_poison __ro_after_init;
+static u64 efi_poison_nbits __ro_after_init;
+
+static u64 __init
+efi_poison_usable_size(const struct linux_efi_poisoned_memory *pm)
+{
+ u64 nr_units = DIV_ROUND_UP(PFN_PHYS(max_pfn), pm->unit_size);
+ u64 bytes = DIV_ROUND_UP(nr_units, BITS_PER_BYTE);
+
+ /* Whole words: the bitmap is reached an unsigned long at a time. */
+ return min(round_up(bytes, sizeof(unsigned long)), pm->size);
+}
+
+static bool __init
+efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)
+{
+ if (!pm->size || !IS_ALIGNED(pm->size, sizeof(unsigned long)))
+ return false;
+
+ return pm->unit_size >= PAGE_SIZE && is_power_of_2(pm->unit_size);
+}
+
+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;
+
+ return !check_mul_overflow(nbits, (u64)pm->unit_size, &span);
+}
+
+/* The table may come from an earlier kernel, so vet it before using 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;
+
+ 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 = efi_poison_usable_size(pm);
+ memunmap(pm);
+ if (!size)
+ return 0;
+
+ efi_poison = memremap(efi.poisoned_memory, sizeof(*pm) + size,
+ MEMREMAP_WB);
+ if (WARN_ON_ONCE(!efi_poison))
+ return 0;
+
+ efi_poison_nbits = size * BITS_PER_BYTE;
+ return 0;
+}
+early_initcall(efi_poison_init);
+
+static long efi_poison_unit(unsigned long pfn)
+{
+ u64 unit = PFN_PHYS(pfn) / efi_poison->unit_size;
+
+ if (unit >= efi_poison_nbits)
+ return -1;
+ return unit;
+}
+
+/*
+ * A bit is never cleared: it 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 c7b4a37f760ec..d579d75372248 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
^ permalink raw reply related [flat|nested] 10+ messages in thread* Re: [PATCH v3 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
2026-08-26 12:03 ` [PATCH v3 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table Breno Leitao
@ 2026-08-28 14:11 ` Kiryl Shutsemau
0 siblings, 0 replies; 10+ messages in thread
From: Kiryl Shutsemau @ 2026-08-28 14:11 UTC (permalink / raw)
To: Breno Leitao
Cc: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kexec, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-efi, linux-kernel,
linux-mm, rmikey, riel, kernel-team
On Wed, Aug 26, 2026 at 05:03:54AM -0700, Breno Leitao wrote:
> diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
> new file mode 100644
> index 0000000000000..d6855e712832c
> --- /dev/null
> +++ b/drivers/firmware/efi/poison.c
> @@ -0,0 +1,126 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Runtime side of the LINUX_EFI_POISONED_MEMORY table: one bit per
> + * EFI_POISON_UNIT_SIZE, set here as frames go bad, honored by the next kernel.
> + *
> + * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
> + * Copyright (c) 2026 Breno Leitao <leitao@debian.org>
> + */
> +
> +#define pr_fmt(fmt) "efi: " fmt
> +
> +#include <linux/bitmap.h>
> +#include <linux/efi.h>
> +#include <linux/io.h>
> +#include <linux/log2.h>
> +#include <linux/memblock.h>
Is it leftover? I don't see any memblock usage.
> +#include <linux/mm.h>
> +#include <linux/overflow.h>
> +
> +static struct linux_efi_poisoned_memory *efi_poison __ro_after_init;
> +static u64 efi_poison_nbits __ro_after_init;
> +
> +static u64 __init
> +efi_poison_usable_size(const struct linux_efi_poisoned_memory *pm)
> +{
> + u64 nr_units = DIV_ROUND_UP(PFN_PHYS(max_pfn), pm->unit_size);
> + u64 bytes = DIV_ROUND_UP(nr_units, BITS_PER_BYTE);
> +
> + /* Whole words: the bitmap is reached an unsigned long at a time. */
> + return min(round_up(bytes, sizeof(unsigned long)), pm->size);
Hm. Why bother to clamp to max_pfn? What's what's wrong with just using
pm->size directly?
> +}
> +
--
Kiryl Shutsemau / Kirill A. Shutemov
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v3 4/5] mm/memory-failure: add a helper to poison a frame at boot
2026-08-26 12:03 [PATCH v3 0/5] efi: mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (2 preceding siblings ...)
2026-08-26 12:03 ` [PATCH v3 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table Breno Leitao
@ 2026-08-26 12:03 ` Breno Leitao
2026-08-26 12:03 ` [PATCH v3 5/5] mm/memory-failure: efi: replay the poisioned page in the next kernel Breno Leitao
4 siblings, 0 replies; 10+ messages in thread
From: Breno Leitao @ 2026-08-26 12:03 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kas, kexec, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
kernel-team
A kernel that inherits a record of hardware-poisoned frames from an
earlier kernel needs a way to apply it. memory_failure() does not fit: it
takes mf_mutex, prints a line per frame, and a 2M unit is 512 of them.
Add hwpoison_boot_pfn(). A frame that is free takes the same route
memory_failure() takes for a free page: take_page_off_buddy(), the flag,
the refcount and the counter. A frame the kernel is already sitting on is
only flagged, since it cannot be taken away from whoever reserved it.
free_pages_prepare() drops such a frame if it is ever handed back, so it
does not reach the allocator either way.
The accounting cannot go through num_poisoned_pages_inc(). Its per memory
block half divides by sections_per_block, which memory_dev_init() only
sets up from driver_init(), so where this runs it is still zero and the
boot dies on a divide by zero. Bump the global counter directly; the block
counters stay short by these frames.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
include/linux/mm.h | 1 +
mm/memory-failure.c | 24 ++++++++++++++++++++++++
2 files changed, 25 insertions(+)
diff --git a/include/linux/mm.h b/include/linux/mm.h
index 32bb723ffbb92..52a00a0e090c7 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -5104,6 +5104,7 @@ extern const struct attribute_group memory_failure_attr_group;
extern void memory_failure_queue(unsigned long pfn, int flags);
void num_poisoned_pages_inc(unsigned long pfn);
void num_poisoned_pages_sub(unsigned long pfn, long i);
+bool __init hwpoison_boot_pfn(unsigned long pfn);
#else
static inline void memory_failure_queue(unsigned long pfn, int flags)
{
diff --git a/mm/memory-failure.c b/mm/memory-failure.c
index 357a72ffda625..713fb2f8e0332 100644
--- a/mm/memory-failure.c
+++ b/mm/memory-failure.c
@@ -97,6 +97,30 @@ void num_poisoned_pages_sub(unsigned long pfn, long i)
memblk_nr_poison_sub(pfn, i);
}
+static void update_per_node_mf_stats(unsigned long pfn, enum mf_result result);
+
+bool __init hwpoison_boot_pfn(unsigned long pfn)
+{
+ struct page *page = pfn_to_online_page(pfn);
+
+ if (!page || PageHWPoison(page))
+ return false;
+
+ if (is_free_buddy_page(page)) {
+ if (!take_page_off_buddy(page))
+ return false;
+ page_ref_inc(page);
+ } else if (!PageReserved(page)) {
+ return false;
+ }
+
+ SetPageHWPoison(page);
+ update_per_node_mf_stats(pfn, MF_RECOVERED);
+ atomic_long_inc(&num_poisoned_pages);
+
+ return true;
+}
+
/**
* MF_ATTR_RO - Create sysfs entry for each memory failure statistics.
* @_name: name of the file in the per NUMA sysfs directory.
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH v3 5/5] mm/memory-failure: efi: replay the poisioned page in the next kernel
2026-08-26 12:03 [PATCH v3 0/5] efi: mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (3 preceding siblings ...)
2026-08-26 12:03 ` [PATCH v3 4/5] mm/memory-failure: add a helper to poison a frame at boot Breno Leitao
@ 2026-08-26 12:03 ` Breno Leitao
2026-08-28 14:35 ` Kiryl Shutsemau
4 siblings, 1 reply; 10+ messages in thread
From: Breno Leitao @ 2026-08-26 12:03 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kas, kexec, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
kernel-team
The bitmap an earlier kernel filled in rides the EFI system table into
this one, but nothing reads it back until now.
Add efi_offline_poisoned_memory(), which vets the table header, copies
the geometry out of it, and hands every frame of every set unit to
hwpoison_boot_pfn(). The bitmap runs to megabytes on a large machine, so
it is mapped and walked a page at a time rather than in one go.
Call it through hwpoison_init_boot() from mm_core_init() right after
memblock_free_all(), the first point at which the recorded frames have
struct pages. They end up in the state a frame poisoned by this kernel
would be in, so the placement check that already understands PG_hwpoison
covers them too.
Suggested-by: Kiryl Shutsemau <kas@kernel.org>
Signed-off-by: Breno Leitao <leitao@debian.org>
---
drivers/firmware/efi/poison.c | 113 +++++++++++++++++++++++++++++++++++++++++-
include/linux/efi.h | 2 +
include/linux/mm.h | 5 ++
mm/memory-failure.c | 6 +++
mm/mm_init.c | 1 +
5 files changed, 126 insertions(+), 1 deletion(-)
diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
index d6855e712832c..179843277457f 100644
--- a/drivers/firmware/efi/poison.c
+++ b/drivers/firmware/efi/poison.c
@@ -9,14 +9,20 @@
#define pr_fmt(fmt) "efi: " fmt
-#include <linux/bitmap.h>
#include <linux/efi.h>
#include <linux/io.h>
#include <linux/log2.h>
#include <linux/memblock.h>
+#include <linux/minmax.h>
#include <linux/mm.h>
#include <linux/overflow.h>
+struct efi_poison_geometry {
+ u64 table; /* phys address of the table */
+ u64 unit_size;
+ u64 bitmap_size;
+};
+
static struct linux_efi_poisoned_memory *efi_poison __ro_after_init;
static u64 efi_poison_nbits __ro_after_init;
@@ -124,3 +130,108 @@ void efi_hwpoison_record_pfn(unsigned long pfn)
set_bit(unit, efi_poison->bitmap);
}
+
+static bool __init efi_poison_read_geometry(u64 ppm,
+ struct efi_poison_geometry *g)
+{
+ struct linux_efi_poisoned_memory *pm;
+ bool valid;
+
+ pm = early_memremap(ppm, sizeof(*pm));
+ if (!pm) {
+ pr_warn("Could not map poisoned-memory table\n");
+ return false;
+ }
+
+ valid = efi_poison_table_valid(pm);
+ if (valid) {
+ g->table = ppm;
+ g->unit_size = pm->unit_size;
+ g->bitmap_size = efi_poison_usable_size(pm);
+ } else {
+ /* Keep the runtime side off a table this pass rejected. */
+ efi.poisoned_memory = EFI_INVALID_TABLE_ADDR;
+ }
+
+ early_memunmap(pm, sizeof(*pm));
+
+ return valid;
+}
+
+static unsigned long __init
+efi_poison_offline_unit(const struct efi_poison_geometry *g, u64 unit)
+{
+ unsigned long pfn = PHYS_PFN(unit * g->unit_size);
+ unsigned long i, nr_pages = 0;
+
+ for (i = 0; i < g->unit_size >> PAGE_SHIFT; i++)
+ nr_pages += hwpoison_boot_pfn(pfn + i);
+
+ return nr_pages;
+}
+
+static long __init efi_poison_walk_chunk(const struct efi_poison_geometry *g,
+ u64 off, unsigned long *nr_units)
+{
+ u64 chunk = min_t(u64, PAGE_SIZE, g->bitmap_size - off);
+ unsigned long bit, nbits = chunk * BITS_PER_BYTE;
+ unsigned long *map, nr_pages = 0;
+
+ map = early_memremap(g->table + offsetof(struct linux_efi_poisoned_memory,
+ bitmap) + off, chunk);
+ if (!map)
+ return -1;
+
+ for_each_set_bit(bit, map, nbits) {
+ nr_pages += efi_poison_offline_unit(g, off * BITS_PER_BYTE + bit);
+ (*nr_units)++;
+ }
+
+ early_memunmap(map, chunk);
+
+ return nr_pages;
+}
+
+static long __init efi_poison_walk(const struct efi_poison_geometry *g,
+ unsigned long *nr_units)
+{
+ unsigned long nr_pages = 0;
+ u64 off;
+
+ for (off = 0; off < g->bitmap_size; off += PAGE_SIZE) {
+ long nr = efi_poison_walk_chunk(g, off, nr_units);
+
+ if (nr < 0) {
+ pr_warn("Could not map poisoned-memory bitmap\n");
+ return -1;
+ }
+ nr_pages += nr;
+ }
+
+ return nr_pages;
+}
+
+void __init efi_offline_poisoned_memory(void)
+{
+ struct efi_poison_geometry g;
+ unsigned long nr_units = 0;
+ long nr_pages, expected;
+
+ if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
+ return;
+
+ if (!efi_poison_read_geometry(efi.poisoned_memory, &g))
+ return;
+
+ nr_pages = efi_poison_walk(&g, &nr_units);
+ if (nr_pages < 0)
+ return;
+
+ if (nr_pages)
+ pr_info("poisoned %ld page(s) inherited across kexec\n", nr_pages);
+
+ expected = nr_units * (g.unit_size >> PAGE_SHIFT);
+ if (nr_pages < expected)
+ pr_warn("%ld inherited poisoned page(s) could not be taken out of use\n",
+ expected - nr_pages);
+}
diff --git a/include/linux/efi.h b/include/linux/efi.h
index d579d75372248..03ba40ff70e7e 100644
--- a/include/linux/efi.h
+++ b/include/linux/efi.h
@@ -1285,8 +1285,10 @@ struct linux_efi_poisoned_memory {
#define EFI_POISON_UNIT_SIZE SZ_2M
#ifdef CONFIG_EFI_POISONED_MEMORY
+void efi_offline_poisoned_memory(void);
void efi_hwpoison_record_pfn(unsigned long pfn);
#else
+static inline void efi_offline_poisoned_memory(void) { }
static inline void efi_hwpoison_record_pfn(unsigned long pfn) { }
#endif
diff --git a/include/linux/mm.h b/include/linux/mm.h
index 52a00a0e090c7..092220943531e 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -5104,12 +5104,17 @@ extern const struct attribute_group memory_failure_attr_group;
extern void memory_failure_queue(unsigned long pfn, int flags);
void num_poisoned_pages_inc(unsigned long pfn);
void num_poisoned_pages_sub(unsigned long pfn, long i);
+void __init hwpoison_init_boot(void);
bool __init hwpoison_boot_pfn(unsigned long pfn);
#else
static inline void memory_failure_queue(unsigned long pfn, int flags)
{
}
+static inline void hwpoison_init_boot(void)
+{
+}
+
static inline void num_poisoned_pages_inc(unsigned long pfn)
{
}
diff --git a/mm/memory-failure.c b/mm/memory-failure.c
index 713fb2f8e0332..1d6a472267bdb 100644
--- a/mm/memory-failure.c
+++ b/mm/memory-failure.c
@@ -121,6 +121,12 @@ bool __init hwpoison_boot_pfn(unsigned long pfn)
return true;
}
+/* The EFI table is the only source of inherited poison today. */
+void __init hwpoison_init_boot(void)
+{
+ efi_offline_poisoned_memory();
+}
+
/**
* MF_ATTR_RO - Create sysfs entry for each memory failure statistics.
* @_name: name of the file in the per NUMA sysfs directory.
diff --git a/mm/mm_init.c b/mm/mm_init.c
index ddda9d6837f32..f94fa221da0b8 100644
--- a/mm/mm_init.c
+++ b/mm/mm_init.c
@@ -2667,6 +2667,7 @@ void __init mm_core_init(void)
kho_memory_init();
memblock_free_all();
+ hwpoison_init_boot();
mem_init();
kmem_cache_init();
/*
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 10+ messages in thread* Re: [PATCH v3 5/5] mm/memory-failure: efi: replay the poisioned page in the next kernel
2026-08-26 12:03 ` [PATCH v3 5/5] mm/memory-failure: efi: replay the poisioned page in the next kernel Breno Leitao
@ 2026-08-28 14:35 ` Kiryl Shutsemau
0 siblings, 0 replies; 10+ messages in thread
From: Kiryl Shutsemau @ 2026-08-28 14:35 UTC (permalink / raw)
To: Breno Leitao
Cc: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kexec, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-efi, linux-kernel,
linux-mm, rmikey, riel, kernel-team
On Wed, Aug 26, 2026 at 05:03:56AM -0700, Breno Leitao wrote:
> diff --git a/mm/mm_init.c b/mm/mm_init.c
> index ddda9d6837f32..f94fa221da0b8 100644
> --- a/mm/mm_init.c
> +++ b/mm/mm_init.c
> @@ -2667,6 +2667,7 @@ void __init mm_core_init(void)
> kho_memory_init();
>
> memblock_free_all();
> + hwpoison_init_boot();
> mem_init();
> kmem_cache_init();
> /*
I don't think it works with deferred page init.
With CONFIG_DEFERRED_STRUCT_PAGE_INIT the struct pages above
first_deferred_pfn are not initialized here. memmap_init_range() breaks
out at defer_init() and the rest is done by page_alloc_init_late(), way
past this point.
But I think the shape is wrong here, not just the placement. We should
flag poisoned pages on the first add to buddy, not after the buddy is
initialized.
__free_pages_core() already consults the unaccepted table. Adding youre
case there seems logical. And all three paths -- memblock_free_pages(),
deferred_free_pages() and generic_online_page() -- gets there.
No need in special-casing for deferred page init.
It also closes the window where the page allocator is live before the
frames are taken out of it.
And patch 4 seems to be redundant in such shape.
--
Kiryl Shutsemau / Kirill A. Shutemov
^ permalink raw reply [flat|nested] 10+ messages in thread