* [PATCH v4 0/5] mm/memory-failure: keep hardware-poisoned pages out of the next kexec
@ 2026-09-09 13:05 Breno Leitao
2026-09-09 13:05 ` [PATCH v4 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
` (4 more replies)
0 siblings, 5 replies; 15+ messages in thread
From: Breno Leitao @ 2026-09-09 13:05 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, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Brendan Jackman, Johannes Weiner, Zi Yan
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, kernel-team
Problem:
========
When a page is hard-offlined due to an uncorrectable memory error (multi
bit ECC), memory_failure() sets PG_hwpoison, unmaps it, removes it from
the buddy allocator. This information is not carried to the next kernel
that is kexeced. The new kernel kexecs and trip over that bad memory
bank _again_.
Why now:
========
Several industry trends make this increasingly important:
1) DRAM is getting more expensive
2) soldered / on-package memory (LPDDR, HBM) is becoming more common, so a
failing part can no longer simply be swapped;
3) memory is kept in service far longer (at Meta, DRAM lifetime is being
drastically extended).
4) It is more and more common to kexec instead of full reboot
5) Increase of memory per system with CXL
What was done already:
======================
In order make linux deal better with the problem above, I've done
already fixed a bunch of stuff in this area, such as:
1) Panic on unrecoverable errors, instead of "printk and carry over":
https://lore.kernel.org/all/20260630-ecc_panic-v10-0-c6ed5b62eea2@debian.org/
2) Respect poisoned memory at kexec time
https://lore.kernel.org/all/20260812-kexec_posioned-v6-0-e477887086f0@debian.org/
Now, the natural follow up is to carry the poisoned memory information
to the next kexec kernel, avoiding tripping over a known "bad page".
Proposed Solution:
==================
Carry the poisoned frames to the next kernel in a new EFI configuration
table, LINUX_EFI_POISONED_MEMORY.
The table is a bitmap with one bit per 2MB of physical memory, modeled
on LINUX_EFI_UNACCEPTED_MEMORY. The stub sizes it from the EFI memory
map and installs it empty while boot services are up -- a running kernel
cannot install a configuration table, it can only flip bits -- and a
table inherited from an earlier boot is reused as-is. That is one
fixed-size allocation, 64KB per TiB of the span the memory map describes,
with no list to grow at runtime and no chain to trust at parse time.
The mechanism is architecture independent, so x86 and arm64 use the same
code.
Each hard offline sets the bit for its unit. Soft-offlined pages are not
recorded: they are still functional, and were offlined predictively.
The next kernel takes the table into use from efi_config_parse_tables(),
which vets the inherited header and hands the table's pages to memblock.
It is EFI ACPI reclaim memory, which x86 leaves out of memblock and so
out of the direct map; the unaccepted memory table is handled the same
way for the same reason.
The frames themselves are poisoned in __free_pages_core(), as each block
reaches the buddy allocator. That is the one point every producer passes
through -- memblock_free_pages() early, deferred_free_pages() once the
deferred struct pages are up, and generic_online_page() at any later
hotplug -- and it is already where the unaccepted memory table is
consulted. A block covering a recorded frame is dropped whole rather than
freed, so the frames never enter the allocator instead of being taken
back out of it. They end up in the state a frame poisoned by this kernel
would be in, so everything that already understands PG_hwpoison covers
them -- including the kexec segment placement check from the series
linked above, which is what keeps the next kernel image off these frames.
Blocks run up to MAX_PAGE_ORDER and a unit is 2MB, so dropping the block
writes off more than the unit itself, and MemFree falls by more than
HardwareCorrupted accounts for.
Granularity is the trade-off: one bad 4KB frame costs a whole 2MB unit in
every later kernel of the chain. In exchange, a row or column fault --
roughly a quarter of the DRAM faults reported in [1], and potentially
thousands of 4KB pages scattered over gigabytes -- collapses into a bit
or two.
A bit is never cleared, which is a known limitation: it stands for a
whole unit, so an unpoison of one frame cannot tell whether the unit as a
whole is good again.
Known limitations:
==================
In order to keep this patchset digestible, I am making some trade-offs,
thus, this feature has the following limitations:
- Memory hot-added after boot is not covered: the bitmap spans the RAM
the EFI memory map describes, and a frame above it is not recorded.
Same gap the unaccepted-memory table has.
- Memory preserved across a KHO handover is not covered.
kho_preserved_memory_reserve() marks it MEMBLOCK_RSRV_NOINIT, so
memmap_init_reserved_pages() leaves those struct pages uninitialised,
and a frame there is neither free nor PageReserved when the bitmap is
applied. Its record is dropped, and the frame goes back to the
allocator once the owner releases it.
- Without a memblock reservation, early boot can allocate over a
recorded frame before the flag is applied. The frame does not reach
the allocator afterwards, but the memmap or page tables may end up
sitting on it.
- The per memory block hardware-poison counter does not include
inherited frames. memblk_nr_poison_inc() finds the block by pfn and
memory_dev_init() has not built it yet, so the count is dropped and
the block is later created reading zero. This is not a big deal,
since I do not expect the kexeced kernel to try to unpoison the
page that came from previous kernel. The only downside is the fact
that kernel B will not have a proper counting of how many pages
are poisoned (they will be invisible -- aka 0).
All of the limitations above can be fixed in follow up work. I am trying
to keep this patchset the foundation, with that work done on top.
The series is five patches:
1) add the LINUX_EFI_POISONED_MEMORY table
2) size, build and install it from both stub entry paths
3) record poisoned frames into the table from the memory_failure() path,
and hand the table to memblock so it can be reached later
4) answer whether a range covers a recorded frame
5) apply it from __free_pages_core(), as each block reaches the buddy
allocator
This was initially discussed at
https://lore.kernel.org/all/ajut_LDQGYCShApx@gmail.com/
A special thanks to Kiryl Shutsemau, for feedbacks and suggestions.
[1] https://arxiv.org/abs/2408.15302
To: Ard Biesheuvel <ardb@kernel.org>
To: Ilias Apalodimas <ilias.apalodimas@linaro.org>
To: Miaohe Lin <linmiaohe@huawei.com>
To: Naoya Horiguchi <nao.horiguchi@gmail.com>
To: Andrew Morton <akpm@linux-foundation.org>
Cc: linux-efi@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-mm@kvack.org
Cc: rmikey@meta.com
To: kas@kernel.org
Cc: riel@surriel.com
To: kexec@lists.infradead.org
Signed-off-by: Breno Leitao <leitao@debian.org>
---
Changes in v4:
- Poison the inherited frames from __free_pages_core(), as each block
reaches the buddy allocator, instead of walking the bitmap once from
mm_core_init(): with CONFIG_DEFERRED_STRUCT_PAGE_INIT most struct pages
are not initialised there and the poison was silently lost (Kiryl)
- Hand the table's pages to memblock at parse time, the way
8dbe33956d96 does for the unaccepted memory table; it is ACPI reclaim
memory and touching it through the direct map faulted
- Add phys_base to the table, so a machine whose RAM starts high does not
pay for the hole below it (Kiryl)
- Size the bitmap from every descriptor in the memory map rather than a
descriptor-type list, which was x86-only reasoning (Kiryl)
- Drop the max_pfn clamp on the inherited bitmap size (Kiryl)
- Drop the stale memblock.h include (Kiryl)
- Link to v3: https://patch.msgid.link/20260826-hwpoison-kho-v3-0-6f79c4b605bc@debian.org
Changes in v3:
- Poison the inherited frames from mm_core_init() instead of reserving
them in memblock, so everything that keys off PG_hwpoison sees them,
the kexec segment placement check included (Kiryl)
- Reserve nothing in memblock: the page flag is enough, and reserving
per unit that early runs into the fixed region array (Kiryl)
- Drop the 1MB cap on the table and the unit coarsening that went with
it (Kiryl)
- Size the table from the memory types arm64 turns into RAM as well, not
just the set setup_e820() maps to E820_TYPE_RAM (Kiryl)
- Make CONFIG_EFI_POISONED_MEMORY unprompted, so it is on wherever its
dependencies allow and nobody has to decide (Kiryl, Pratyush)
- Fold the top-of-RAM helper into its only caller and make it static,
rather than adding a generic libstub API
- Fold the table build and its installation into one patch
- Spell out the known limitations in this cover letter
- Link to v2: https://patch.msgid.link/20260821-hwpoison-kho-v2-0-5743791e48e6@debian.org
Changes in v2:
- Replace the growable linked list of 4KB entries with a fixed-size
bitmap, one bit per 2MB, modeled on the unaccepted-memory table (Kiryl)
- Record hard offlines only, by hooking action_result() instead of
num_poisoned_pages_inc(), which also fires for soft offline (Kiryl)
- Allocate the table as EFI_ACPI_RECLAIM_MEMORY, so it is not System RAM
in the next kernel, and reuse an inherited table instead of installing
a second one
- Validate the geometry of an inherited table before using it
- Cap the table at 1MB, coarsening the unit instead of growing it
- Restrict to 64-bit, as the unaccepted-memory table effectively is
- Never clear a bit: an unpoison no longer un-records the unit
- Split the table definition and the stub installer into separate patches
- Link to v1: https://patch.msgid.link/20260717-hwpoison-kho-v1-0-9c5eda551998@debian.org
To: Ard Biesheuvel <ardb@kernel.org>
To: Ilias Apalodimas <ilias.apalodimas@linaro.org>
To: Miaohe Lin <linmiaohe@huawei.com>
To: Naoya Horiguchi <nao.horiguchi@gmail.com>
To: Andrew Morton <akpm@linux-foundation.org>
To: David Hildenbrand <david@kernel.org>
To: Lorenzo Stoakes <ljs@kernel.org>
To: "Liam R. Howlett" <liam@infradead.org>
To: Vlastimil Babka <vbabka@kernel.org>
To: Mike Rapoport <rppt@kernel.org>
To: Suren Baghdasaryan <surenb@google.com>
To: Michal Hocko <mhocko@suse.com>
To: Thomas Gleixner <tglx@kernel.org>
To: Ingo Molnar <mingo@redhat.com>
To: Borislav Petkov <bp@alien8.de>
To: Dave Hansen <dave.hansen@linux.intel.com>
To: x86@kernel.org
To: "H. Peter Anvin" <hpa@zytor.com>
To: Brendan Jackman <brendan.jackman@linux.dev>
To: Johannes Weiner <hannes@cmpxchg.org>
To: Zi Yan <ziy@nvidia.com>
Cc: linux-efi@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-mm@kvack.org
Cc: harry@kernel.org
---
Breno Leitao (5):
mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table
mm/memory-failure: libstub: install the poisoned-memory EFI table
mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
mm/memory-failure: efi: answer whether a range is poisoned
mm/memory-failure: keep inherited poisoned frames out of the buddy allocator
arch/x86/platform/efi/efi.c | 3 +
drivers/firmware/efi/Kconfig | 8 ++
drivers/firmware/efi/Makefile | 1 +
drivers/firmware/efi/efi.c | 8 ++
drivers/firmware/efi/libstub/efi-stub-helper.c | 100 ++++++++++++++++++
drivers/firmware/efi/libstub/efi-stub.c | 1 +
drivers/firmware/efi/libstub/efistub.h | 6 ++
drivers/firmware/efi/libstub/x86-stub.c | 2 +
drivers/firmware/efi/poison.c | 141 +++++++++++++++++++++++++
include/linux/efi.h | 22 ++++
include/linux/mm.h | 19 ++++
mm/memory-failure.c | 18 ++++
mm/page_alloc.c | 25 +++++
13 files changed, 354 insertions(+)
---
base-commit: a9d7ced84989ec05be09b4b8428759ef60450a0f
change-id: 20260622-hwpoison-kho-fc9db2ada8ba
Best regards,
--
Breno Leitao <leitao@debian.org>
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v4 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table
2026-09-09 13:05 [PATCH v4 0/5] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
@ 2026-09-09 13:05 ` Breno Leitao
2026-09-09 13:13 ` sashiko-bot
2026-09-09 13:05 ` [PATCH v4 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
` (3 subsequent siblings)
4 siblings, 1 reply; 15+ messages in thread
From: Breno Leitao @ 2026-09-09 13:05 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, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Brendan Jackman, Johannes Weiner, Zi Yan
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, 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 starting at phys_base, modeled on the LINUX_EFI_UNACCEPTED_MEMORY
table, and it rides the EFI system table to every kernel in the chain.
Basing the bitmap keeps a machine whose RAM starts high from paying for
the hole below it.
List the table in the x86 efi_tables[] too, so an SME host maps it
unencrypted like every other EFI table.
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.
This is a similar approach as used as unaccepted memory.
Suggested-by: Kiryl Shutsemau <kas@kernel.org>
Signed-off-by: Breno Leitao <leitao@debian.org>
---
arch/x86/platform/efi/efi.c | 3 +++
drivers/firmware/efi/Kconfig | 8 ++++++++
drivers/firmware/efi/efi.c | 6 ++++++
include/linux/efi.h | 14 ++++++++++++++
4 files changed, 31 insertions(+)
diff --git a/arch/x86/platform/efi/efi.c b/arch/x86/platform/efi/efi.c
index 0c39adb96b912..2b37b96a36e09 100644
--- a/arch/x86/platform/efi/efi.c
+++ b/arch/x86/platform/efi/efi.c
@@ -93,6 +93,9 @@ static const unsigned long * const efi_tables[] = {
#ifdef CONFIG_UNACCEPTED_MEMORY
&efi.unaccepted,
#endif
+#ifdef CONFIG_EFI_POISONED_MEMORY
+ &efi.poisoned_memory,
+#endif
};
u64 efi_setup; /* efi setup_data physical address */
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 6d987d7f97781..af1fa443839c4 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);
@@ -677,6 +680,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 c35446a0b66fa..efaf63f9a54ed 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;
@@ -1272,6 +1275,17 @@ 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 the unit at @phys_base + N * @unit_size. */
+struct linux_efi_poisoned_memory {
+ u32 version;
+ u32 unit_size; /* bytes of phys space per bitmap bit */
+ u64 phys_base; /* address the first bit covers */
+ 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 [flat|nested] 15+ messages in thread
* [PATCH v4 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-09-09 13:05 [PATCH v4 0/5] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-09-09 13:05 ` [PATCH v4 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
@ 2026-09-09 13:05 ` Breno Leitao
2026-09-09 13:19 ` sashiko-bot
2026-09-09 14:47 ` Ard Biesheuvel
2026-09-09 13:05 ` [PATCH v4 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table Breno Leitao
` (2 subsequent siblings)
4 siblings, 2 replies; 15+ messages in thread
From: Breno Leitao @ 2026-09-09 13:05 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, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Brendan Jackman, Johannes Weiner, Zi Yan
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, 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 span the UEFI memory map describes, which
efi_get_ram_range() walks since the stub has no max_pfn. Bit 0 covers
the bottom of that span, recorded in phys_base, so a machine whose RAM
starts high does not pay for the hole below it. Memory the firmware
hot-adds later sits outside the span and is not carried across a kexec.
One table has to serve every architecture, and they do not agree on what
becomes RAM: x86 decides by descriptor type, arm64 by attribute. So
efi_get_ram_range() does not filter at all and spans every descriptor in
the map. Sizing wide only costs bitmap bytes; sizing narrow silently
drops the records for every frame outside the span.
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 | 100 +++++++++++++++++++++++++
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, 109 insertions(+)
diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
index 48f93f7758e9e..5cbe675491333 100644
--- a/drivers/firmware/efi/libstub/efi-stub-helper.c
+++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
@@ -774,3 +774,103 @@ 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
+/*
+ * Find the base and top of the memory, so, we can create the bitmap for
+ * the full range.
+ */
+static efi_status_t efi_get_ram_range(u64 *base, u64 *top)
+{
+ struct efi_boot_memmap *map __free(efi_pool) = NULL;
+ u64 ram_base = ULLONG_MAX, ram_top = 0;
+ efi_status_t status;
+ 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);
+ ram_base = min(ram_base, d->phys_addr);
+ ram_top = max(ram_top,
+ d->phys_addr + d->num_pages * EFI_PAGE_SIZE);
+ }
+ if (!ram_top || ram_base == ULLONG_MAX)
+ return EFI_NOT_FOUND;
+
+ *base = round_down(ram_base, EFI_POISON_UNIT_SIZE);
+ *top = round_up(ram_top, EFI_POISON_UNIT_SIZE);
+
+ return EFI_SUCCESS;
+}
+
+/* The size of the bitmap */
+static u64 efi_poison_bitmap_size(u64 span)
+{
+ u64 bytes = DIV_ROUND_UP(DIV_ROUND_UP(span, EFI_POISON_UNIT_SIZE),
+ BITS_PER_BYTE);
+
+ return round_up(bytes, sizeof(unsigned long));
+}
+
+static struct linux_efi_poisoned_memory *efi_poison_alloc(u64 phys_base,
+ 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->phys_base = phys_base;
+ pm->size = bitmap_size;
+ memset(pm->bitmap, 0, bitmap_size);
+
+ return pm;
+}
+
+/* This needs to be done while boot service is still active */
+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_base, 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_range(&ram_base, &ram_top) != EFI_SUCCESS) {
+ efi_err("Failed to size the poisoned-memory table!\n");
+ return;
+ }
+
+ bitmap_size = efi_poison_bitmap_size(ram_top - ram_base);
+ pm = efi_poison_alloc(ram_base, 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 235c9738da2d6..22a315e2814a1 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 0bae0f06b6763..3136132b9628a 100644
--- a/drivers/firmware/efi/libstub/x86-stub.c
+++ b/drivers/firmware/efi/libstub/x86-stub.c
@@ -1024,6 +1024,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 [flat|nested] 15+ messages in thread
* [PATCH v4 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
2026-09-09 13:05 [PATCH v4 0/5] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-09-09 13:05 ` [PATCH v4 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
2026-09-09 13:05 ` [PATCH v4 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
@ 2026-09-09 13:05 ` Breno Leitao
2026-09-09 13:21 ` sashiko-bot
2026-09-09 13:05 ` [PATCH v4 4/5] mm/memory-failure: efi: answer whether a range is poisoned Breno Leitao
2026-09-09 13:05 ` [PATCH v4 5/5] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator Breno Leitao
4 siblings, 1 reply; 15+ messages in thread
From: Breno Leitao @ 2026-09-09 13:05 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, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Brendan Jackman, Johannes Weiner, Zi Yan
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, 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.
The table is EFI ACPI reclaim memory, which becomes E820_TYPE_ACPI and so
reaches neither memblock nor the direct map; touching it then faults.
Hand its pages to memblock from efi_config_parse_tables() the way the
unaccepted memory table already does, and vet the inherited header in the
same pass, so everything afterwards can reach a table it can trust with
phys_to_virt().
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@debian.org>
---
drivers/firmware/efi/Makefile | 1 +
drivers/firmware/efi/efi.c | 2 +
drivers/firmware/efi/poison.c | 115 ++++++++++++++++++++++++++++++++++++++++++
include/linux/efi.h | 8 +++
mm/memory-failure.c | 3 ++
5 files changed, 129 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/efi.c b/drivers/firmware/efi/efi.c
index af1fa443839c4..55b2ee53fc268 100644
--- a/drivers/firmware/efi/efi.c
+++ b/drivers/firmware/efi/efi.c
@@ -883,6 +883,8 @@ int __init efi_config_parse_tables(const efi_config_table_t *config_tables,
}
}
+ efi_poisoned_memory_reserve();
+
return 0;
}
diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
new file mode 100644
index 0000000000000..c18edf111c710
--- /dev/null
+++ b/drivers/firmware/efi/poison.c
@@ -0,0 +1,115 @@
+// 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 bool __init
+efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)
+{
+ u64 nbits;
+
+ /* Whole words, and a bit count that can be taken without wrapping. */
+ if (!pm->size || !IS_ALIGNED(pm->size, sizeof(unsigned long)) ||
+ check_mul_overflow(pm->size, (u64)BITS_PER_BYTE, &nbits))
+ 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 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)) {
+ pr_warn("Ignoring malformed poisoned-memory table\n");
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * Vet the inherited table and hand its pages to memblock, the way the
+ * unaccepted memory table is handled. It is EFI ACPI reclaim memory, which
+ * becomes E820_TYPE_ACPI and would otherwise stay out of the direct map, and
+ * touching it then faults. Called from efi_config_parse_tables(), so
+ * everything later can reach it with efi_poisoned_memory().
+ */
+void __init efi_poisoned_memory_reserve(void)
+{
+ struct linux_efi_poisoned_memory *pm;
+ phys_addr_t start, end;
+
+ if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
+ return;
+
+ pm = early_memremap(efi.poisoned_memory, sizeof(*pm));
+ if (!pm) {
+ pr_warn("Could not map poisoned-memory table\n");
+ efi.poisoned_memory = EFI_INVALID_TABLE_ADDR;
+ return;
+ }
+
+ if (!efi_poison_table_valid(pm)) {
+ efi.poisoned_memory = EFI_INVALID_TABLE_ADDR;
+ early_memunmap(pm, sizeof(*pm));
+ return;
+ }
+
+ start = PAGE_ALIGN_DOWN(efi.poisoned_memory);
+ end = PAGE_ALIGN(efi.poisoned_memory + sizeof(*pm) + pm->size);
+ early_memunmap(pm, sizeof(*pm));
+
+ memblock_add(start, end - start);
+ memblock_reserve(start, end - start);
+}
+
+/* The table, vetted at parse time, or NULL if this boot has none. */
+static struct linux_efi_poisoned_memory *efi_poisoned_memory(void)
+{
+ if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
+ return NULL;
+
+ return phys_to_virt(efi.poisoned_memory);
+}
+
+/*
+ * 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)
+{
+ struct linux_efi_poisoned_memory *pm = efi_poisoned_memory();
+ phys_addr_t addr = PFN_PHYS(pfn);
+ u64 unit;
+
+ if (!pm || addr < pm->phys_base)
+ return;
+
+ unit = (addr - pm->phys_base) / pm->unit_size;
+ if (unit < pm->size * BITS_PER_BYTE)
+ set_bit(unit, pm->bitmap);
+}
diff --git a/include/linux/efi.h b/include/linux/efi.h
index efaf63f9a54ed..56402fdccd114 100644
--- a/include/linux/efi.h
+++ b/include/linux/efi.h
@@ -1286,6 +1286,14 @@ struct linux_efi_poisoned_memory {
#define EFI_POISON_UNIT_SIZE SZ_2M
+#ifdef CONFIG_EFI_POISONED_MEMORY
+void __init efi_poisoned_memory_reserve(void);
+void efi_hwpoison_record_pfn(unsigned long pfn);
+#else
+static inline void efi_poisoned_memory_reserve(void) { }
+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 a2ca8df501cae..d9b8be696aac3 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>
@@ -1326,6 +1327,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 [flat|nested] 15+ messages in thread
* [PATCH v4 4/5] mm/memory-failure: efi: answer whether a range is poisoned
2026-09-09 13:05 [PATCH v4 0/5] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (2 preceding siblings ...)
2026-09-09 13:05 ` [PATCH v4 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table Breno Leitao
@ 2026-09-09 13:05 ` Breno Leitao
2026-09-09 13:17 ` sashiko-bot
2026-09-09 13:05 ` [PATCH v4 5/5] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator Breno Leitao
4 siblings, 1 reply; 15+ messages in thread
From: Breno Leitao @ 2026-09-09 13:05 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, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Brendan Jackman, Johannes Weiner, Zi Yan
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, kernel-team
The bitmap an earlier kernel filled in gets in this one through EFI,
but nothing reads it back yet.
Introduce range_contains_poisoned_memory(), which the page allocator
will use to ask about a block before handing it out.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
drivers/firmware/efi/poison.c | 26 ++++++++++++++++++++++++++
include/linux/mm.h | 14 ++++++++++++++
2 files changed, 40 insertions(+)
diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
index c18edf111c710..e16d43f4438ee 100644
--- a/drivers/firmware/efi/poison.c
+++ b/drivers/firmware/efi/poison.c
@@ -96,6 +96,32 @@ static struct linux_efi_poisoned_memory *efi_poisoned_memory(void)
return phys_to_virt(efi.poisoned_memory);
}
+/* Does the range cover a unit an earlier kernel recorded as bad? */
+bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size)
+{
+ struct linux_efi_poisoned_memory *pm = efi_poisoned_memory();
+ u64 first, last, nbits;
+
+ if (!pm)
+ return false;
+
+ nbits = pm->size * BITS_PER_BYTE;
+
+ if (start + size <= pm->phys_base)
+ return false;
+ if (start < pm->phys_base)
+ start = pm->phys_base;
+
+ first = (start - pm->phys_base) / pm->unit_size;
+ if (first >= nbits)
+ return false;
+
+ last = (start + size - 1 - pm->phys_base) / pm->unit_size;
+ last = min(last, nbits - 1);
+
+ return find_next_bit(pm->bitmap, last + 1, first) <= last;
+}
+
/*
* 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.
diff --git a/include/linux/mm.h b/include/linux/mm.h
index 274fa880077c5..b68824fcfbef1 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -5387,6 +5387,20 @@ static inline bool pfn_is_unaccepted_memory(unsigned long pfn)
return range_contains_unaccepted_memory(pfn << PAGE_SHIFT, PAGE_SIZE);
}
+#ifdef CONFIG_EFI_POISONED_MEMORY
+
+bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size);
+
+#else
+
+static inline bool range_contains_poisoned_memory(phys_addr_t start,
+ unsigned long size)
+{
+ return false;
+}
+
+#endif
+
void vma_pgtable_walk_begin(struct vm_area_struct *vma);
void vma_pgtable_walk_end(struct vm_area_struct *vma);
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v4 5/5] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator
2026-09-09 13:05 [PATCH v4 0/5] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (3 preceding siblings ...)
2026-09-09 13:05 ` [PATCH v4 4/5] mm/memory-failure: efi: answer whether a range is poisoned Breno Leitao
@ 2026-09-09 13:05 ` Breno Leitao
2026-09-09 13:24 ` sashiko-bot
4 siblings, 1 reply; 15+ messages in thread
From: Breno Leitao @ 2026-09-09 13:05 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, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Brendan Jackman, Johannes Weiner, Zi Yan
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, kernel-team
When the pages are being given to the allocator, check if they are
poisoned, and mark them as such.
Similar to unaccepted memory, hook it in __free_pages_core(), and thus
the frames never enter the allocator, rather than being taken back out
of it.
hwpoison_boot_page() leaves a frame in the state a frame poisoned by this
kernel would be in, so everything that already understands PG_hwpoison
covers it, the kexec segment placement check included.
Suggested-by: Kiryl Shutsemau <kas@kernel.org>
Signed-off-by: Breno Leitao <leitao@debian.org>
---
include/linux/mm.h | 5 +++++
mm/memory-failure.c | 15 +++++++++++++++
mm/page_alloc.c | 25 +++++++++++++++++++++++++
3 files changed, 45 insertions(+)
diff --git a/include/linux/mm.h b/include/linux/mm.h
index b68824fcfbef1..9d9f2e8fdc136 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -5225,6 +5225,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);
+void __meminit hwpoison_boot_page(struct page *page);
phys_addr_t range_first_hwpoison(phys_addr_t start, unsigned long size);
phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size);
#else
@@ -5232,6 +5233,10 @@ static inline void memory_failure_queue(unsigned long pfn, int flags)
{
}
+static inline void hwpoison_boot_page(struct page *page)
+{
+}
+
static inline void num_poisoned_pages_inc(unsigned long pfn)
{
}
diff --git a/mm/memory-failure.c b/mm/memory-failure.c
index d9b8be696aac3..f6afdb2a89a94 100644
--- a/mm/memory-failure.c
+++ b/mm/memory-failure.c
@@ -137,6 +137,21 @@ phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size)
return range_hwpoison(start, size, false);
}
+static void update_per_node_mf_stats(unsigned long pfn, enum mf_result result);
+
+/* Not num_poisoned_pages_inc(): its per block half divides by zero this early. */
+void __meminit hwpoison_boot_page(struct page *page)
+{
+ if (PageHWPoison(page))
+ return;
+
+ SetPageHWPoison(page);
+ set_page_count(page, 1);
+ /* The page has been completely isolated == MF_RECOVERED */
+ update_per_node_mf_stats(page_to_pfn(page), MF_RECOVERED);
+ atomic_long_inc(&num_poisoned_pages);
+}
+
/**
* 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/page_alloc.c b/mm/page_alloc.c
index 404896b53003e..9e2ce833fd409 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -1579,6 +1579,19 @@ static void __free_pages_ok(struct page *page, unsigned int order,
free_one_page(zone, page, pfn, order, fpi_flags);
}
+/* Flag the frames an earlier kernel recorded as bad. */
+static void __meminit poison_block(struct page *page, unsigned int order)
+{
+ unsigned long i, nr_pages = 1UL << order;
+
+ for (i = 0; i < nr_pages; i++) {
+ struct page *p = page + i;
+
+ if (range_contains_poisoned_memory(page_to_phys(p), PAGE_SIZE))
+ hwpoison_boot_page(p);
+ }
+}
+
void __meminit __free_pages_core(struct page *page, unsigned int order,
enum meminit_context context)
{
@@ -1613,6 +1626,18 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,
atomic_long_add(nr_pages, &page_zone(page)->managed_pages);
}
+ /* First: a block parked by __free_unaccepted() never returns here. */
+ if (range_contains_poisoned_memory(page_to_phys(page),
+ PAGE_SIZE << order)) {
+ poison_block(page, order);
+ /*
+ * TODO: free the frames in the block that are not poisoned.
+ * They stay out of the allocator and still count in
+ * managed_pages, so a unit costs up to a block.
+ */
+ return;
+ }
+
if (page_contains_unaccepted(page, order)) {
if (order == MAX_PAGE_ORDER && __free_unaccepted(page))
return;
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v4 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table
2026-09-09 13:05 ` [PATCH v4 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
@ 2026-09-09 13:13 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-09-09 13:13 UTC (permalink / raw)
To: Andrew Morton, Ingo Molnar, Borislav Petkov, Michal Hocko,
Zi Yan, H. Peter Anvin, Dave Hansen, Naoya Horiguchi, Miaohe Lin,
Mike Rapoport, kas, Suren Baghdasaryan, x86, Vlastimil Babka,
Brendan Jackman, Johannes Weiner, Lorenzo Stoakes,
Ard Biesheuvel, David Hildenbrand, Liam R. Howlett,
Thomas Gleixner, Ilias Apalodimas, Breno Leitao
Cc: rmikey, linux-kernel, riel, harry, linux-efi, kernel-team, kexec,
linux-mm
> 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 starting at phys_base, modeled on the LINUX_EFI_UNACCEPTED_MEMORY
> table, and it rides the EFI system table to every kernel in the chain.
> Basing the bitmap keeps a machine whose RAM starts high from paying for
> the hole below it.
>
> List the table in the x86 efi_tables[] too, so an SME host maps it
> unencrypted like every other EFI table.
>
> 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.
>
> This is a similar approach as used as unaccepted memory.
>
> Suggested-by: Kiryl Shutsemau <kas@kernel.org>
> Signed-off-by: Breno Leitao <leitao@debian.org>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260909-hwpoison-kho-v4-0-359313564495@debian.org?part=1
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v4 4/5] mm/memory-failure: efi: answer whether a range is poisoned
2026-09-09 13:05 ` [PATCH v4 4/5] mm/memory-failure: efi: answer whether a range is poisoned Breno Leitao
@ 2026-09-09 13:17 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-09-09 13:17 UTC (permalink / raw)
To: H. Peter Anvin, Borislav Petkov, Michal Hocko, Zi Yan,
Andrew Morton, Breno Leitao, David Hildenbrand, Naoya Horiguchi,
Liam R. Howlett, Miaohe Lin, Lorenzo Stoakes, Ard Biesheuvel,
Vlastimil Babka, Thomas Gleixner, Ingo Molnar, Brendan Jackman,
Suren Baghdasaryan, Mike Rapoport, x86, Dave Hansen,
Ilias Apalodimas, Johannes Weiner, kas
Cc: linux-kernel, rmikey, harry, linux-mm, kernel-team, riel, kexec,
linux-efi
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Adjusting `start` without proportionally shrinking `size` shifts the checked memory range out of bounds, causing false positive poison detection.
--
commit b492314fda14ef47eff8ff738b90a4749a490163
Author: Breno Leitao <leitao@debian.org>
mm/memory-failure: efi: answer whether a range is poisoned
This commit introduces range_contains_poisoned_memory() which allows the
page allocator to query whether a memory block overlaps with regions
recorded as poisoned by an earlier kernel via EFI.
> diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
> index c18edf111c710..e16d43f4438ee 100644
> --- a/drivers/firmware/efi/poison.c
> +++ b/drivers/firmware/efi/poison.c
[ ... ]
> +/* Does the range cover a unit an earlier kernel recorded as bad? */
> +bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size)
> +{
> + struct linux_efi_poisoned_memory *pm = efi_poisoned_memory();
> + u64 first, last, nbits;
> +
> + if (!pm)
> + return false;
> +
> + nbits = pm->size * BITS_PER_BYTE;
> +
> + if (start + size <= pm->phys_base)
> + return false;
> + if (start < pm->phys_base)
> + start = pm->phys_base;
> +
> + first = (start - pm->phys_base) / pm->unit_size;
> + if (first >= nbits)
> + return false;
> +
> + last = (start + size - 1 - pm->phys_base) / pm->unit_size;
[Severity: High]
When start is clamped to pm->phys_base earlier in this function, size is
not proportionally decreased. Does this result in an end boundary calculation
that exceeds the originally requested range?
If the allocator queries a block that starts before the EFI poisoned memory
table (start < pm->phys_base) but extends into it, the unadjusted size added
to the newly shifted start will push the last bit beyond the intended region.
Could this out-of-bounds query cause false positive poison detections for
healthy memory that resides just after the requested block?
> + last = min(last, nbits - 1);
> +
> + return find_next_bit(pm->bitmap, last + 1, first) <= last;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260909-hwpoison-kho-v4-0-359313564495@debian.org?part=4
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v4 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-09-09 13:05 ` [PATCH v4 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
@ 2026-09-09 13:19 ` sashiko-bot
2026-09-09 14:00 ` Breno Leitao
2026-09-09 14:47 ` Ard Biesheuvel
1 sibling, 1 reply; 15+ messages in thread
From: sashiko-bot @ 2026-09-09 13:19 UTC (permalink / raw)
To: David Hildenbrand, Vlastimil Babka, Naoya Horiguchi,
Breno Leitao, Mike Rapoport, Michal Hocko, Ingo Molnar, Zi Yan,
Dave Hansen, Thomas Gleixner, Borislav Petkov, Johannes Weiner,
H. Peter Anvin, Lorenzo Stoakes, Miaohe Lin, Andrew Morton,
Ard Biesheuvel, kas, Ilias Apalodimas, Liam R. Howlett, x86,
Suren Baghdasaryan, Brendan Jackman
Cc: harry, linux-efi, riel, linux-kernel, linux-mm, kexec, rmikey,
kernel-team
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] Integer truncation in efi_poison_alloc leads to a massive heap buffer overflow during memset on 32-bit or mixed-mode systems.
--
commit 626632820fe1fdcddc73aaeb109705cbddc9828c
Author: Breno Leitao <leitao@debian.org>
Subject: mm/memory-failure: libstub: install the poisoned-memory EFI table
This patch introduces functionality in the EFI stub to create and install a
poisoned-memory EFI configuration table. It walks the UEFI memory map to find
the RAM range and allocates a bitmap to track poisoned memory granules.
> diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
> index 48f93f7758e9e..5cbe675491333 100644
> --- a/drivers/firmware/efi/libstub/efi-stub-helper.c
> +++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
[ ... ]
> +static struct linux_efi_poisoned_memory *efi_poison_alloc(u64 phys_base,
> + 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);
[Severity: Critical]
Could sizeof(*pm) + bitmap_size overflow 32 bits on 32-bit or mixed-mode
systems? If the EFI memory map describes a very large physical address space
(such as a 32PB physical address provided by an untrusted hypervisor in a
TDX/SEV environment), bitmap_size could approach 4GB.
Because the size argument to allocate_pool is a UINTN, a value like
0x100000008 (sizeof(*pm) + 0xFFFFFFF0) would truncate to 8 bytes on 32-bit
EFI systems, resulting in an undersized allocation.
> + if (status != EFI_SUCCESS)
> + return NULL;
> +
> + pm->version = 1;
> + pm->unit_size = EFI_POISON_UNIT_SIZE;
> + pm->phys_base = phys_base;
> + pm->size = bitmap_size;
> + memset(pm->bitmap, 0, bitmap_size);
[Severity: Critical]
If the allocate_pool calculation overflowed and truncated as described above,
does this memset trigger an out-of-bounds write regression?
Since bitmap_size (e.g., 0xFFFFFFF0) fits within a 32-bit size_t, memset
will use the large untruncated size, potentially overwriting up to 4GB of EFI
boot services memory following the undersized allocation.
> +
> + return pm;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260909-hwpoison-kho-v4-0-359313564495@debian.org?part=2
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v4 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
2026-09-09 13:05 ` [PATCH v4 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table Breno Leitao
@ 2026-09-09 13:21 ` sashiko-bot
2026-09-09 14:05 ` Breno Leitao
0 siblings, 1 reply; 15+ messages in thread
From: sashiko-bot @ 2026-09-09 13:21 UTC (permalink / raw)
To: Ilias Apalodimas, Michal Hocko, Naoya Horiguchi, Andrew Morton,
kas, Vlastimil Babka, x86, H. Peter Anvin, David Hildenbrand,
Breno Leitao, Liam R. Howlett, Brendan Jackman, Johannes Weiner,
Borislav Petkov, Suren Baghdasaryan, Ard Biesheuvel,
Lorenzo Stoakes, Mike Rapoport, Miaohe Lin, Ingo Molnar,
Dave Hansen, Zi Yan, Thomas Gleixner
Cc: kernel-team, rmikey, harry, linux-efi, riel, linux-kernel,
linux-mm, kexec
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Missing overflow check when computing the end address of the EFI poisoned memory table can lead to an unbounded memblock reservation and memory corruption.
--
commit 7216d7e029930900ac5741a5f5bbfe7a2107b408
Author: Breno Leitao <leitao@debian.org>
mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
This commit hooks action_result() to efi_hwpoison_record_pfn() to record hard
offlined frames in the LINUX_EFI_POISONED_MEMORY table, ensuring they are
passed down to the next kernel via kexec. It also adds early initialization
code to vet the table inherited from previous boots.
> diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
> new file mode 100644
> index 0000000000000..c18edf111c710
> --- /dev/null
> +++ b/drivers/firmware/efi/poison.c
[ ... ]
> +static bool __init
> +efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)
> +{
> + u64 nbits;
> +
> + /* Whole words, and a bit count that can be taken without wrapping. */
> + if (!pm->size || !IS_ALIGNED(pm->size, sizeof(unsigned long)) ||
> + check_mul_overflow(pm->size, (u64)BITS_PER_BYTE, &nbits))
> + return false;
[Severity: High]
Does this validation fully protect against an excessively large pm->size?
While check_mul_overflow() validates the size for bit conversion, it appears
it does not verify that the table's footprint fits within the physical address
space without overflowing when combined with efi.poisoned_memory.
> +
> + if (pm->unit_size < PAGE_SIZE || !is_power_of_2(pm->unit_size))
> + return false;
> +
> + return IS_ALIGNED(pm->phys_base, pm->unit_size);
> +}
[ ... ]
> +void __init efi_poisoned_memory_reserve(void)
> +{
> + struct linux_efi_poisoned_memory *pm;
> + phys_addr_t start, end;
> +
> + if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
> + return;
> +
> + pm = early_memremap(efi.poisoned_memory, sizeof(*pm));
> + if (!pm) {
> + pr_warn("Could not map poisoned-memory table\n");
> + efi.poisoned_memory = EFI_INVALID_TABLE_ADDR;
> + return;
> + }
> +
> + if (!efi_poison_table_valid(pm)) {
> + efi.poisoned_memory = EFI_INVALID_TABLE_ADDR;
> + early_memunmap(pm, sizeof(*pm));
> + return;
> + }
> +
> + start = PAGE_ALIGN_DOWN(efi.poisoned_memory);
> + end = PAGE_ALIGN(efi.poisoned_memory + sizeof(*pm) + pm->size);
[Severity: High]
Can this computation wrap around if pm->size is maliciously large?
Because pm->size is only constrained to be less than U64_MAX / 8 in
efi_poison_geometry_valid(), an unvalidated addition here could overflow,
resulting in end < start. If that happens, an enormous end - start would
be passed to memblock_add() and memblock_reserve().
This could result in memblock capping the size to PHYS_ADDR_MAX - start,
blindly marking the rest of the physical address space (including MMIO) as
RAM. This forces direct-map cacheable page table entries for MMIO regions,
leading to system crashes or memory layout corruption.
> + early_memunmap(pm, sizeof(*pm));
> +
> + memblock_add(start, end - start);
> + memblock_reserve(start, end - start);
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260909-hwpoison-kho-v4-0-359313564495@debian.org?part=3
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v4 5/5] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator
2026-09-09 13:05 ` [PATCH v4 5/5] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator Breno Leitao
@ 2026-09-09 13:24 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-09-09 13:24 UTC (permalink / raw)
To: Liam R. Howlett, Michal Hocko, Naoya Horiguchi, kas,
David Hildenbrand, Ilias Apalodimas, Thomas Gleixner,
Lorenzo Stoakes, Ingo Molnar, Zi Yan, Breno Leitao,
Andrew Morton, Dave Hansen, Mike Rapoport, Miaohe Lin,
Suren Baghdasaryan, Johannes Weiner, Brendan Jackman, x86,
Ard Biesheuvel, Vlastimil Babka, Borislav Petkov, H. Peter Anvin
Cc: riel, linux-kernel, linux-mm, linux-efi, rmikey, harry,
kernel-team, kexec
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Healthy pages within a memory block containing a poisoned page are completely leaked, preventing memory offlining.
- [Medium] Concurrent non-atomic updates to per-node memory failure statistics (mf_stats) cause lost updates and corrupted counters.
- [Medium] Memory block poison accounting is skipped during memory hotplug, breaking block-level poison tracking and causing global counter leaks.
- [Medium] Boot-poisoned pages lack the MAGIC_HWPOISON marker, breaking unpoison_memory() logic and causing a memory leak on unpoison.
- [Medium] TOCTOU race when setting the HWPoison flag during memory hotplug, potentially causing double accounting of memory failure stats.
--
commit 804f678a0cc93ac88d9e22aff45a011384c3ac87
Author: Breno Leitao <leitao@debian.org>
mm/memory-failure: keep inherited poisoned frames out of the buddy allocator
This commit checks if pages being given to the allocator are poisoned and marks
them accordingly. It hooks into __free_pages_core() so poisoned frames never
enter the buddy allocator. A new function hwpoison_boot_page() is used to set
the appropriate state for these pages.
> diff --git a/mm/memory-failure.c b/mm/memory-failure.c
> index d9b8be696aac3..f6afdb2a89a94 100644
> --- a/mm/memory-failure.c
> +++ b/mm/memory-failure.c
[ ... ]
> +static void update_per_node_mf_stats(unsigned long pfn, enum mf_result result);
> +
> +/* Not num_poisoned_pages_inc(): its per block half divides by zero this early. */
> +void __meminit hwpoison_boot_page(struct page *page)
> +{
> + if (PageHWPoison(page))
> + return;
> +
> + SetPageHWPoison(page);
[Severity: Medium]
Could a concurrent hardware MCE race with hwpoison_boot_page() here? During
memory hotplug, this path checks PageHWPoison(page) and then non-atomically
calls SetPageHWPoison(page). This might lead to both paths setting the flag
and double accounting in both global and per-node memory failure statistics.
> + set_page_count(page, 1);
> + /* The page has been completely isolated == MF_RECOVERED */
[Severity: Medium]
Does this code need to call SetPageHWPoisonTakenOff(page) here? Without the
MAGIC_HWPOISON marker, if unpoison_memory() is called on a boot-poisoned
page, it might not properly recognize the page as having been taken off the
buddy allocator, potentially causing a permanent memory leak on unpoison.
> + update_per_node_mf_stats(page_to_pfn(page), MF_RECOVERED);
[Severity: Medium]
Are the non-atomic increments inside update_per_node_mf_stats() safe when
called concurrently during boot? Since __free_pages_core() can be executed
concurrently by multiple kthreads, the unprotected non-atomic increments like
++mf_stats->recovered might lead to corrupted per-node memory failure
statistics.
> + atomic_long_inc(&num_poisoned_pages);
[Severity: Medium]
Does bypassing num_poisoned_pages_inc() here break memory hot-remove
accounting? By calling atomic_long_inc(&num_poisoned_pages) instead of
num_poisoned_pages_inc(), the hotplug memory block's nr_hwpoison counter is
left at 0. If the block is later hot-removed, the global num_poisoned_pages
counter would not be decremented properly.
> +}
> +
> /**
> * MF_ATTR_RO - Create sysfs entry for each memory failure statistics.
[ ... ]
> diff --git a/mm/page_alloc.c b/mm/page_alloc.c
> index 404896b53003e..9e2ce833fd409 100644
> --- a/mm/page_alloc.c
> +++ b/mm/page_alloc.c
[ ... ]
> @@ -1613,6 +1626,18 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,
> atomic_long_add(nr_pages, &page_zone(page)->managed_pages);
> }
>
> + /* First: a block parked by __free_unaccepted() never returns here. */
> + if (range_contains_poisoned_memory(page_to_phys(page),
> + PAGE_SIZE << order)) {
> + poison_block(page, order);
> + /*
> + * TODO: free the frames in the block that are not poisoned.
> + * They stay out of the allocator and still count in
> + * managed_pages, so a unit costs up to a block.
> + */
> + return;
> + }
[Severity: High]
Does this early return completely leak the unpoisoned pages within the memory
block? By returning early and bypassing __free_pages_ok(), the healthy pages
in the block are not returned to the buddy allocator. Since these leaked
pages lack PageBuddy, PageOffline, or PageHWPoison, memory offlining
(test_pages_isolated) might permanently fail with -EBUSY for this block.
> +
> if (page_contains_unaccepted(page, order)) {
> if (order == MAX_PAGE_ORDER && __free_unaccepted(page))
> return;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260909-hwpoison-kho-v4-0-359313564495@debian.org?part=5
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v4 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-09-09 13:19 ` sashiko-bot
@ 2026-09-09 14:00 ` Breno Leitao
0 siblings, 0 replies; 15+ messages in thread
From: Breno Leitao @ 2026-09-09 14:00 UTC (permalink / raw)
To: sashiko-reviews
Cc: David Hildenbrand, Vlastimil Babka, Naoya Horiguchi,
Mike Rapoport, Michal Hocko, Ingo Molnar, Zi Yan, Dave Hansen,
Thomas Gleixner, Borislav Petkov, Johannes Weiner,
H. Peter Anvin, Lorenzo Stoakes, Miaohe Lin, Andrew Morton,
Ard Biesheuvel, kas, Ilias Apalodimas, Liam R. Howlett, x86,
Suren Baghdasaryan, Brendan Jackman, harry, linux-efi, riel,
linux-kernel, linux-mm, kexec, rmikey, kernel-team
On Wed, Sep 09, 2026 at 01:19:11PM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Critical] Integer truncation in efi_poison_alloc leads to a massive heap buffer overflow during memset on 32-bit or mixed-mode systems.
> --
>
> commit 626632820fe1fdcddc73aaeb109705cbddc9828c
> Author: Breno Leitao <leitao@debian.org>
> Subject: mm/memory-failure: libstub: install the poisoned-memory EFI table
>
> This patch introduces functionality in the EFI stub to create and install a
> poisoned-memory EFI configuration table. It walks the UEFI memory map to find
> the RAM range and allocates a bitmap to track poisoned memory granules.
>
> > diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
> > index 48f93f7758e9e..5cbe675491333 100644
> > --- a/drivers/firmware/efi/libstub/efi-stub-helper.c
> > +++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
> [ ... ]
> > +static struct linux_efi_poisoned_memory *efi_poison_alloc(u64 phys_base,
> > + 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);
>
> [Severity: Critical]
> Could sizeof(*pm) + bitmap_size overflow 32 bits on 32-bit or mixed-mode
> systems?
This feature (EFI_POISONED_MEMORY) is only enabled on 32-bits system,
so, 32-bit issues is not a real issue. See very first patch:
+config EFI_POISONED_MEMORY
+ def_bool y
+ depends on EFI_STUB && MEMORY_FAILURE && 64BIT
So, this is a false positive
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v4 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
2026-09-09 13:21 ` sashiko-bot
@ 2026-09-09 14:05 ` Breno Leitao
0 siblings, 0 replies; 15+ messages in thread
From: Breno Leitao @ 2026-09-09 14:05 UTC (permalink / raw)
To: sashiko-reviews
Cc: Ilias Apalodimas, Michal Hocko, Naoya Horiguchi, Andrew Morton,
kas, Vlastimil Babka, x86, H. Peter Anvin, David Hildenbrand,
Liam R. Howlett, Brendan Jackman, Johannes Weiner,
Borislav Petkov, Suren Baghdasaryan, Ard Biesheuvel,
Lorenzo Stoakes, Mike Rapoport, Miaohe Lin, Ingo Molnar,
Dave Hansen, Zi Yan, Thomas Gleixner, kernel-team, rmikey, harry,
linux-efi, riel, linux-kernel, linux-mm, kexec
On Wed, Sep 09, 2026 at 01:21:28PM +0000, sashiko-bot@kernel.org wrote:
> > --- /dev/null
> > +++ b/drivers/firmware/efi/poison.c
> >
> > + /* Whole words, and a bit count that can be taken without wrapping. */
> > + if (!pm->size || !IS_ALIGNED(pm->size, sizeof(unsigned long)) ||
> > + check_mul_overflow(pm->size, (u64)BITS_PER_BYTE, &nbits))
> > + return false;
>
> [Severity: High]
> Does this validation fully protect against an excessively large pm->size?
I was clamping the table before, but we decided to drop it in the last
revision. See the discusion in here:
https://lore.kernel.org/all/apGWUWi5-RbhFHpe@thinkstation/
> > + start = PAGE_ALIGN_DOWN(efi.poisoned_memory);
> > + end = PAGE_ALIGN(efi.poisoned_memory + sizeof(*pm) + pm->size);
>
> [Severity: High]
> Can this computation wrap around if pm->size is maliciously large?
Yes, but that means that someone was able to write to EFI memory to
update size?
I can definitely BUG_ON() on "large" pm->size, but, I don't think this
is what we want here.
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v4 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-09-09 13:05 ` [PATCH v4 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
2026-09-09 13:19 ` sashiko-bot
@ 2026-09-09 14:47 ` Ard Biesheuvel
2026-09-10 13:11 ` Breno Leitao
1 sibling, 1 reply; 15+ messages in thread
From: Ard Biesheuvel @ 2026-09-09 14:47 UTC (permalink / raw)
To: Breno Leitao, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, Kiryl Shutsemau (Meta),
kexec, David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86,
H . Peter Anvin, Brendan Jackman, Johannes Weiner, Zi Yan
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, harry, kernel-team
Hello Breno,
Apologies for chiming in late.
On Wed, 9 Sep 2026, at 15:05, 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.
>
This is true, but that also means a config table could have a 'next' field
pointing to an allocation that was added later.
There is a EFI memreserve table based on this principle: this is a hack
that we added for the arm64 GICv3 LPI table handling, which is a braindead
piece of kit that must use the same physical allocation as the previous
kernel. It is not currently enabled on x86.
Please consider whether or not that is more suitable, and can be repurposed
or shared. (Feel free to make changes to the current format if needed).
I don't have a strong preference either way, but I feel the 2M granularity
may be a bit wasteful, no?
> Size the bitmap from the span the UEFI memory map describes, which
> efi_get_ram_range() walks since the stub has no max_pfn. Bit 0 covers
> the bottom of that span, recorded in phys_base, so a machine whose RAM
> starts high does not pay for the hole below it. Memory the firmware
> hot-adds later sits outside the span and is not carried across a kexec.
>
> One table has to serve every architecture, and they do not agree on what
> becomes RAM: x86 decides by descriptor type, arm64 by attribute. So
> efi_get_ram_range() does not filter at all and spans every descriptor in
> the map. Sizing wide only costs bitmap bytes; sizing narrow silently
> drops the records for every frame outside the span.
>
> 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 | 100 +++++++++++++++++++++++++
> 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, 109 insertions(+)
>
> diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c
> b/drivers/firmware/efi/libstub/efi-stub-helper.c
> index 48f93f7758e9e..5cbe675491333 100644
> --- a/drivers/firmware/efi/libstub/efi-stub-helper.c
> +++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
> @@ -774,3 +774,103 @@ 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
> +/*
> + * Find the base and top of the memory, so, we can create the bitmap
> for
> + * the full range.
> + */
> +static efi_status_t efi_get_ram_range(u64 *base, u64 *top)
> +{
> + struct efi_boot_memmap *map __free(efi_pool) = NULL;
> + u64 ram_base = ULLONG_MAX, ram_top = 0;
> + efi_status_t status;
> + 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);
> + ram_base = min(ram_base, d->phys_addr);
> + ram_top = max(ram_top,
> + d->phys_addr + d->num_pages * EFI_PAGE_SIZE);
> + }
> + if (!ram_top || ram_base == ULLONG_MAX)
> + return EFI_NOT_FOUND;
> +
> + *base = round_down(ram_base, EFI_POISON_UNIT_SIZE);
> + *top = round_up(ram_top, EFI_POISON_UNIT_SIZE);
> +
> + return EFI_SUCCESS;
> +}
> +
> +/* The size of the bitmap */
> +static u64 efi_poison_bitmap_size(u64 span)
> +{
> + u64 bytes = DIV_ROUND_UP(DIV_ROUND_UP(span, EFI_POISON_UNIT_SIZE),
> + BITS_PER_BYTE);
> +
> + return round_up(bytes, sizeof(unsigned long));
> +}
> +
> +static struct linux_efi_poisoned_memory *efi_poison_alloc(u64
> phys_base,
> + 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->phys_base = phys_base;
> + pm->size = bitmap_size;
> + memset(pm->bitmap, 0, bitmap_size);
> +
> + return pm;
> +}
> +
> +/* This needs to be done while boot service is still active */
> +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_base, 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_range(&ram_base, &ram_top) != EFI_SUCCESS) {
> + efi_err("Failed to size the poisoned-memory table!\n");
> + return;
> + }
> +
> + bitmap_size = efi_poison_bitmap_size(ram_top - ram_base);
> + pm = efi_poison_alloc(ram_base, 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 235c9738da2d6..22a315e2814a1 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 0bae0f06b6763..3136132b9628a 100644
> --- a/drivers/firmware/efi/libstub/x86-stub.c
> +++ b/drivers/firmware/efi/libstub/x86-stub.c
> @@ -1024,6 +1024,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 [flat|nested] 15+ messages in thread
* Re: [PATCH v4 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-09-09 14:47 ` Ard Biesheuvel
@ 2026-09-10 13:11 ` Breno Leitao
0 siblings, 0 replies; 15+ messages in thread
From: Breno Leitao @ 2026-09-10 13:11 UTC (permalink / raw)
To: Ard Biesheuvel
Cc: Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi, Andrew Morton,
Kiryl Shutsemau (Meta),
kexec, David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86,
H . Peter Anvin, Brendan Jackman, Johannes Weiner, Zi Yan,
linux-efi, linux-kernel, linux-mm, rmikey, riel, harry,
kernel-team
Hello Ard,
Thanks for the feedback!
On Wed, Sep 09, 2026 at 04:47:27PM +0200, Ard Biesheuvel wrote:
> On Wed, 9 Sep 2026, at 15:05, 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.
>
> This is true, but that also means a config table could have a 'next' field
> pointing to an allocation that was added later.
>
> There is a EFI memreserve table based on this principle: this is a hack
> that we added for the arm64 GICv3 LPI table handling, which is a braindead
> piece of kit that must use the same physical allocation as the previous
> kernel. It is not currently enabled on x86.
>
> Please consider whether or not that is more suitable, and can be repurposed
> or shared. (Feel free to make changes to the current format if needed).
That's actually what I tried in the RFC. I passed an almost-empty EFI
table, then built a linked list of poisoned pages on top of it.
It looked like this:
struct linux_efi_poisoned_memory {
int size; // allocated size of the array
atomic_t count; // number of entries used
phys_addr_t next; // pa of next struct instance
struct {
phys_addr_t base;
phys_addr_t size;
} entry[];
};
The allocation walks that list the same way; see
efi_hwpoison_record_pfn() in patch "[PATCH RFC 2/3] efi: record hardware-poisoned frames into the poisoned-memory table"
This is the RFC patchset:
https://lore.kernel.org/all/20260717-hwpoison-kho-v1-1-9c5eda551998@debian.org/#t
> I don't have a strong preference either way, but I feel the 2M granularity
> may be a bit wasteful, no?
Maybe. It's the same trade-off as unaccepted memory: reducing the
granularity means growing the bitmap.
We can reduce the granularity, but I don't think we should reduce the
granularity if we're going to KEEP using a bitmap.
So I'd say we have two options:
1) Keep it similar to unaccepted memory, with 2M granularity.
- Pro : Similar mental model as unnacepted memory
- Cons: 2 MB might be a bit wasteful
2) Move to a linked list like the RFC, keeping it outside of the EFI
table.
- Pro: Reduce the memory granularities to page instead of 2M blocs.
- Cons: Another way of passing memory information between kexec
kernels.
Any any other option or strong preference?
--breno
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-09-10 13:12 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-09 13:05 [PATCH v4 0/5] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-09-09 13:05 ` [PATCH v4 1/5] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
2026-09-09 13:13 ` sashiko-bot
2026-09-09 13:05 ` [PATCH v4 2/5] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
2026-09-09 13:19 ` sashiko-bot
2026-09-09 14:00 ` Breno Leitao
2026-09-09 14:47 ` Ard Biesheuvel
2026-09-10 13:11 ` Breno Leitao
2026-09-09 13:05 ` [PATCH v4 3/5] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table Breno Leitao
2026-09-09 13:21 ` sashiko-bot
2026-09-09 14:05 ` Breno Leitao
2026-09-09 13:05 ` [PATCH v4 4/5] mm/memory-failure: efi: answer whether a range is poisoned Breno Leitao
2026-09-09 13:17 ` sashiko-bot
2026-09-09 13:05 ` [PATCH v4 5/5] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator Breno Leitao
2026-09-09 13:24 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®