* [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec
@ 2026-09-15 12:53 Breno Leitao
2026-09-15 12:53 ` [PATCH v5 1/9] mm/page_alloc: factor out the accept-and-free tail of __free_pages_core() Breno Leitao
` (8 more replies)
0 siblings, 9 replies; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 12:53 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, linux-cxl, driver-core, 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. The recorded frames are held back and the rest of the block is
freed, so they 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 keeps the segments a kexec places off these frames.
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, and the memmap or page
tables may end up sitting on it.
- On x86 the next kernel picks its own physical address again.
choose_random_location() draws from the memory map and does not know
about the table, so KASLR can land the kernel image on a recorded
frame. arm64 does not re-pick; the outgoing kernel fixes the address.
- Only memory that reaches the allocator through __free_pages_core() is
covered. Memory memblock hands over reserved and something else frees
later is not: CMA areas are freed from cma_activate_area(), and init
memory and the initrd from free_reserved_pages().
- On a confidential computing guest the frames held back can still be
accepted. accept_memory() rounds a request up by one unit, so freeing
the last clean frame of a block accepts the bad unit next to it.
- An inherited frame keeps its direct map entry. On x86 the MCE handler
calls set_mce_nospec() next to memory_failure(), and nothing replays
that half here, so the frame is out of the allocator but still mapped
writeback. set_memory_*() needs the allocator to split a large page,
so this wants a later pass rather than __free_pages_core().
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 nine patches:
1) factor the accept-and-free tail out of __free_pages_core()
2) add the LINUX_EFI_POISONED_MEMORY table
3) size, build and install it from both stub entry paths
4) adopt an inherited table at parse time: vet the header and hand its
pages to memblock so it can be reached later
5) record poisoned frames into it from the memory_failure() path
6) answer whether a range covers a recorded frame
7) count inherited frames into their memory block when it is created
8) add the helper that flags an inherited frame
9) apply the table 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 v5:
- Free the part of the block that is not recorded instead of dropping the
block whole: the leftover frames were neither buddy nor poisoned nor
offline, so they leaked and left the block impossible to offline
(Sashiko)
- Count inherited frames into the memory block when it is created.
memblk_nr_poison_inc() cannot run that early, and unpoisoning a frame
whose block counter was never incremented wrapped it to ULONG_MAX,
which then refuses memory_block_online() for good (Sashiko)
- Keep the caller's end in range_contains_poisoned_memory() when the
start is clamped up to phys_base, rather than shifting the whole window
past what was asked about (Sashiko)
- Reject an inherited table whose footprint wraps the address space,
which fed a negative range to memblock_add() (Sashiko)
- Say in patch 2 why the native bitops are safe: EFI_STUB && 64BIT is
little-endian everywhere (Sashiko)
- Split the accept-and-free tail of __free_pages_core() into its own
preparation patch at the head of the series, so the replay patch can
reuse it without carrying a rework of that function
- Split the replay itself into the page flag helper, the memory block
counter and the __free_pages_core() hook, one per subsystem it touches
- Split adopting an inherited table from recording into it: the parse-time
plumbing and the memory_failure() hook are unrelated pieces of work
- Put the memory block counter ahead of the __free_pages_core() hook, so
no revision in the middle of the series flags a frame the counter does
not know about
- Size the bitmap from the descriptors that become RAM rather than from
every descriptor in the map, which v4 argued for. Both architectures
gate on EFI_MEMORY_WB, so spanning that plus EFI_UNACCEPTED_MEMORY
stays wider than either accepts on its own while leaving out the MMIO
apertures. On a 5G guest the table went from 65536 to 320 bytes
- Link to v4: https://patch.msgid.link/20260909-hwpoison-kho-v4-0-359313564495@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>
To: Oscar Salvador <osalvador@suse.de>
To: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
To: "Rafael J. Wysocki" <rafael@kernel.org>
To: Danilo Krummrich <dakr@kernel.org>
Cc: linux-efi@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: linux-mm@kvack.org
Cc: harry@kernel.org
Cc: linux-cxl@vger.kernel.org
Cc: driver-core@lists.linux.dev
To: hannes@cmpxchg.or
To: shakeel.butt@linux.dev
---
Breno Leitao (9):
mm/page_alloc: factor out the accept-and-free tail of __free_pages_core()
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: adopt the inherited poisoned-memory table
mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
mm/memory-failure: efi: answer whether a range is poisoned
drivers/base/memory: count inherited poisoned frames into the block
mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame
mm/memory-failure: keep inherited poisoned frames out of the buddy allocator
arch/x86/platform/efi/efi.c | 3 +
drivers/base/memory.c | 35 ++++++
drivers/firmware/efi/Kconfig | 8 ++
drivers/firmware/efi/Makefile | 1 +
drivers/firmware/efi/efi.c | 8 ++
drivers/firmware/efi/libstub/efi-stub-helper.c | 103 +++++++++++++++++
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 | 149 +++++++++++++++++++++++++
include/linux/efi.h | 22 ++++
include/linux/mm.h | 21 ++++
mm/memory-failure.c | 29 +++++
mm/page_alloc.c | 53 +++++++--
14 files changed, 431 insertions(+), 10 deletions(-)
---
base-commit: a9d7ced84989ec05be09b4b8428759ef60450a0f
change-id: 20260622-hwpoison-kho-fc9db2ada8ba
Best regards,
--
Breno Leitao <leitao@debian.org>
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH v5 1/9] mm/page_alloc: factor out the accept-and-free tail of __free_pages_core()
2026-09-15 12:53 [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
@ 2026-09-15 12:53 ` Breno Leitao
2026-09-15 12:57 ` sashiko-bot
2026-09-15 12:53 ` [PATCH v5 2/9] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
` (7 subsequent siblings)
8 siblings, 1 reply; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 12:53 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, linux-cxl, driver-core, kernel-team
__free_pages_core() ends by accepting the block if it is still
unaccepted and then handing it to the allocator. The poisoned-memory
replay later in this series needs that same tail for the parts of a
block it does not withhold.
Move it into accept_and_free_block(). This will be used later, when
we need to accept and free a subset of a order-block that is
not-poisioned.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
mm/page_alloc.c | 31 +++++++++++++++++++------------
1 file changed, 19 insertions(+), 12 deletions(-)
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index 404896b53003ef..b07b5f4751cb95 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -1579,6 +1579,24 @@ static void __free_pages_ok(struct page *page, unsigned int order,
free_one_page(zone, page, pfn, order, fpi_flags);
}
+/* Accept the block if it needs it, then hand it to the allocator. */
+static void __meminit accept_and_free_block(struct page *page,
+ unsigned int order)
+{
+ if (page_contains_unaccepted(page, order)) {
+ if (order == MAX_PAGE_ORDER && __free_unaccepted(page))
+ return;
+
+ accept_memory(page_to_phys(page), PAGE_SIZE << order);
+ }
+
+ /*
+ * Bypass PCP and place fresh pages right to the tail, primarily
+ * relevant for memory onlining.
+ */
+ __free_pages_ok(page, order, FPI_TO_TAIL);
+}
+
void __meminit __free_pages_core(struct page *page, unsigned int order,
enum meminit_context context)
{
@@ -1613,18 +1631,7 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,
atomic_long_add(nr_pages, &page_zone(page)->managed_pages);
}
- if (page_contains_unaccepted(page, order)) {
- if (order == MAX_PAGE_ORDER && __free_unaccepted(page))
- return;
-
- accept_memory(page_to_phys(page), PAGE_SIZE << order);
- }
-
- /*
- * Bypass PCP and place fresh pages right to the tail, primarily
- * relevant for memory onlining.
- */
- __free_pages_ok(page, order, FPI_TO_TAIL);
+ accept_and_free_block(page, order);
}
/*
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH v5 2/9] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table
2026-09-15 12:53 [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-09-15 12:53 ` [PATCH v5 1/9] mm/page_alloc: factor out the accept-and-free tail of __free_pages_core() Breno Leitao
@ 2026-09-15 12:53 ` Breno Leitao
2026-09-15 13:05 ` sashiko-bot
2026-09-15 12:53 ` [PATCH v5 3/9] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
` (6 subsequent siblings)
8 siblings, 1 reply; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 12:53 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, linux-cxl, driver-core, 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 divides by a runtime
unit_size, which needs div_u64() on 32-bit. That combination is
reachable, X86_32 with FLATMEM selects ARCH_SUPPORTS_MEMORY_FAILURE and
has an EFI stub, so the dependency is what keeps the division out rather
than a config nobody can build.
The bitmap is an unsigned long array reached with the native bitops, like
the unaccepted memory table it copies. That is endian dependent, but
EFI_STUB && 64BIT is little-endian everywhere: arm64 turns EFI off under
CPU_BIG_ENDIAN, and no other architecture with the stub is big-endian.
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 0c39adb96b912b..2b37b96a36e099 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 29e0729299f5bd..aafcd41bc00630 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 6d987d7f97781f..af1fa443839c4d 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 c35446a0b66fac..efaf63f9a54edd 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] 27+ messages in thread
* [PATCH v5 3/9] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-09-15 12:53 [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-09-15 12:53 ` [PATCH v5 1/9] mm/page_alloc: factor out the accept-and-free tail of __free_pages_core() Breno Leitao
2026-09-15 12:53 ` [PATCH v5 2/9] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
@ 2026-09-15 12:53 ` Breno Leitao
2026-09-15 13:15 ` sashiko-bot
2026-09-16 15:39 ` Usama Arif
2026-09-15 12:53 ` [PATCH v5 4/9] mm/memory-failure: efi: adopt the inherited poisoned-memory table Breno Leitao
` (5 subsequent siblings)
8 siblings, 2 replies; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 12:53 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, linux-cxl, driver-core, 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 what they agree on is the
attribute: setup_e820() takes a descriptor as RAM only if it is writeback
cacheable, and so does is_usable_memory() on arm64.
The bitmap spans from the lowest to the highest descriptor that is
write-back cacheable or unaccepted memory, as discussed with Kiryl. That
leaves out the MMIO apertures, which sit high enough to stretch it far
past the RAM it needs to describe.
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.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
drivers/firmware/efi/libstub/efi-stub-helper.c | 103 +++++++++++++++++++++++++
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, 112 insertions(+)
diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
index 48f93f7758e9e9..9c66e06c972c5a 100644
--- a/drivers/firmware/efi/libstub/efi-stub-helper.c
+++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
@@ -774,3 +774,106 @@ 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);
+ if (!(d->attribute & EFI_MEMORY_WB) &&
+ d->type != EFI_UNACCEPTED_MEMORY)
+ continue;
+ 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 235c9738da2d63..22a315e2814a1a 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 fd91fc15ec810b..44436869c4efe1 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 0bae0f06b6763a..3136132b9628ab 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] 27+ messages in thread
* [PATCH v5 4/9] mm/memory-failure: efi: adopt the inherited poisoned-memory table
2026-09-15 12:53 [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (2 preceding siblings ...)
2026-09-15 12:53 ` [PATCH v5 3/9] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
@ 2026-09-15 12:53 ` Breno Leitao
2026-09-15 13:24 ` sashiko-bot
2026-09-15 12:53 ` [PATCH v5 5/9] mm/memory-failure: efi: record hardware-poisoned frames into the " Breno Leitao
` (4 subsequent siblings)
8 siblings, 1 reply; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 12:53 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, linux-cxl, driver-core, kernel-team
A table installed by an earlier boot rides the EFI system table into this
kernel, but nothing looks at it yet.
Take it into use from efi_config_parse_tables(). The table can come from
any kernel further up the kexec chain, so vet the header first: the
version, whole-word bitmap size, a bit count and a footprint that can be
taken without wrapping, and a power-of-two unit the base is aligned to.
A table that fails any of those is dropped rather than trusted.
The table is EFI ACPI reclaim memory, which x86 turns into E820_TYPE_ACPI
and leaves out of memblock, so it never reaches the direct map and
touching it later faults. Hand its pages to memblock the way commit
8dbe33956d96 ("efi/unaccepted: Make sure unaccepted table is mapped")
does for the unaccepted memory table, so everything afterwards can reach
it with phys_to_virt().
Signed-off-by: Breno Leitao <leitao@debian.org>
---
drivers/firmware/efi/Makefile | 1 +
drivers/firmware/efi/efi.c | 2 +
drivers/firmware/efi/poison.c | 93 +++++++++++++++++++++++++++++++++++++++++++
include/linux/efi.h | 6 +++
4 files changed, 102 insertions(+)
diff --git a/drivers/firmware/efi/Makefile b/drivers/firmware/efi/Makefile
index 8efbcf699e4ff9..05d0a490923e56 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 af1fa443839c4d..55b2ee53fc2688 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 00000000000000..3f12db3dc9b844
--- /dev/null
+++ b/drivers/firmware/efi/poison.c
@@ -0,0 +1,93 @@
+// 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, end;
+
+ /* 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;
+
+ /* And a footprint that can be page aligned without wrapping either. */
+ if (check_add_overflow(efi.poisoned_memory, sizeof(*pm) + pm->size,
+ &end) || end > PHYS_ADDR_MAX - PAGE_SIZE)
+ return false;
+
+ if (pm->unit_size < PAGE_SIZE || !is_power_of_2(pm->unit_size))
+ return false;
+
+ return IS_ALIGNED(pm->phys_base, pm->unit_size);
+}
+
+/* The 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);
+}
diff --git a/include/linux/efi.h b/include/linux/efi.h
index efaf63f9a54edd..dd3263456dd4a3 100644
--- a/include/linux/efi.h
+++ b/include/linux/efi.h
@@ -1286,6 +1286,12 @@ struct linux_efi_poisoned_memory {
#define EFI_POISON_UNIT_SIZE SZ_2M
+#ifdef CONFIG_EFI_POISONED_MEMORY
+void __init efi_poisoned_memory_reserve(void);
+#else
+static inline void efi_poisoned_memory_reserve(void) { }
+#endif
+
void __init efi_arch_mem_reserve(phys_addr_t addr, u64 size);
/*
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH v5 5/9] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
2026-09-15 12:53 [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (3 preceding siblings ...)
2026-09-15 12:53 ` [PATCH v5 4/9] mm/memory-failure: efi: adopt the inherited poisoned-memory table Breno Leitao
@ 2026-09-15 12:53 ` Breno Leitao
2026-09-15 13:36 ` sashiko-bot
2026-09-15 12:53 ` [PATCH v5 6/9] mm/memory-failure: efi: answer whether a range is poisoned Breno Leitao
` (3 subsequent siblings)
8 siblings, 1 reply; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 12:53 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, linux-cxl, driver-core, 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.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
drivers/firmware/efi/poison.c | 27 +++++++++++++++++++++++++++
include/linux/efi.h | 2 ++
mm/memory-failure.c | 3 +++
3 files changed, 32 insertions(+)
diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
index 3f12db3dc9b844..847592862f01d3 100644
--- a/drivers/firmware/efi/poison.c
+++ b/drivers/firmware/efi/poison.c
@@ -91,3 +91,30 @@ void __init efi_poisoned_memory_reserve(void)
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 dd3263456dd4a3..56402fdccd1149 100644
--- a/include/linux/efi.h
+++ b/include/linux/efi.h
@@ -1288,8 +1288,10 @@ struct linux_efi_poisoned_memory {
#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 a2ca8df501caee..d9b8be696aac38 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] 27+ messages in thread
* [PATCH v5 6/9] mm/memory-failure: efi: answer whether a range is poisoned
2026-09-15 12:53 [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (4 preceding siblings ...)
2026-09-15 12:53 ` [PATCH v5 5/9] mm/memory-failure: efi: record hardware-poisoned frames into the " Breno Leitao
@ 2026-09-15 12:53 ` Breno Leitao
2026-09-15 13:45 ` sashiko-bot
2026-09-15 12:53 ` [PATCH v5 7/9] drivers/base/memory: count inherited poisoned frames into the block Breno Leitao
` (2 subsequent siblings)
8 siblings, 1 reply; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 12:53 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, linux-cxl, driver-core, 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 | 29 +++++++++++++++++++++++++++++
include/linux/mm.h | 14 ++++++++++++++
2 files changed, 43 insertions(+)
diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
index 847592862f01d3..4e5cad0d1b9b65 100644
--- a/drivers/firmware/efi/poison.c
+++ b/drivers/firmware/efi/poison.c
@@ -101,6 +101,35 @@ 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;
+ phys_addr_t end;
+
+ if (!pm)
+ return false;
+
+ nbits = pm->size * BITS_PER_BYTE;
+ end = start + size - 1;
+
+ /* Clamp the start into the table, but keep the caller's end. */
+ if (end < 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 = (end - 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 274fa880077c54..b68824fcfbef19 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] 27+ messages in thread
* [PATCH v5 7/9] drivers/base/memory: count inherited poisoned frames into the block
2026-09-15 12:53 [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (5 preceding siblings ...)
2026-09-15 12:53 ` [PATCH v5 6/9] mm/memory-failure: efi: answer whether a range is poisoned Breno Leitao
@ 2026-09-15 12:53 ` Breno Leitao
2026-09-15 13:59 ` sashiko-bot
2026-09-16 6:48 ` David Hildenbrand (Arm)
2026-09-15 12:53 ` [PATCH v5 8/9] mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame Breno Leitao
2026-09-15 12:53 ` [PATCH v5 9/9] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator Breno Leitao
8 siblings, 2 replies; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 12:53 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, linux-cxl, driver-core, kernel-team
A frame a kexec handed over is flagged as it reaches the allocator, long
before its memory block exists, so memblk_nr_poison_inc() had nowhere to
count it. Walk the block once when it is created and take the count from
the page flag instead.
Without it an unpoison later subtracts from a counter that was never
incremented and wraps it, which then refuses memory_block_online() for
good.
Only a block created online needs the walk. A hotplugged one is created
before its pages are, so there is nothing to find, and its frames are
counted by num_poisoned_pages_inc() as they are flagged.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
drivers/base/memory.c | 35 +++++++++++++++++++++++++++++++++++
1 file changed, 35 insertions(+)
diff --git a/drivers/base/memory.c b/drivers/base/memory.c
index 5eead3346f1e32..49a33ddbb2e717 100644
--- a/drivers/base/memory.c
+++ b/drivers/base/memory.c
@@ -220,11 +220,16 @@ int memory_notify(enum memory_block_state state, void *v)
#if defined(CONFIG_MEMORY_FAILURE) && defined(CONFIG_MEMORY_HOTPLUG)
static unsigned long memblk_nr_poison(struct memory_block *mem);
+static void memblk_nr_poison_init(struct memory_block *mem);
#else
static inline unsigned long memblk_nr_poison(struct memory_block *mem)
{
return 0;
}
+
+static inline void memblk_nr_poison_init(struct memory_block *mem)
+{
+}
#endif
/*
@@ -807,6 +812,7 @@ static int add_memory_block(unsigned long block_id, int nid, unsigned long state
mem->state = state;
mem->nid = nid;
INIT_LIST_HEAD(&mem->group_next);
+ memblk_nr_poison_init(mem);
#ifndef CONFIG_NUMA
if (state == MEM_ONLINE)
@@ -1251,4 +1257,33 @@ static unsigned long memblk_nr_poison(struct memory_block *mem)
{
return atomic_long_read(&mem->nr_hwpoison);
}
+
+/*
+ * Frames a kexec handed over are flagged as they reach the allocator, long
+ * before this block exists, so memblk_nr_poison_inc() had nowhere to count
+ * them. Take them from the page flag instead.
+ */
+static void memblk_nr_poison_init(struct memory_block *mem)
+{
+ unsigned long pfn = section_nr_to_pfn(mem->start_section_nr);
+ unsigned long nr_pages = PAGES_PER_SECTION * sections_per_block;
+ unsigned long i, nr_poison = 0;
+
+ /* A hotplugged block is created before its pages are online. */
+ if (mem->state != MEM_ONLINE)
+ return;
+
+ if (!range_contains_poisoned_memory(PFN_PHYS(pfn),
+ nr_pages << PAGE_SHIFT))
+ return;
+
+ for (i = 0; i < nr_pages; i++) {
+ struct page *page = pfn_to_online_page(pfn + i);
+
+ if (page && PageHWPoison(page))
+ nr_poison++;
+ }
+
+ atomic_long_set(&mem->nr_hwpoison, nr_poison);
+}
#endif
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH v5 8/9] mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame
2026-09-15 12:53 [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (6 preceding siblings ...)
2026-09-15 12:53 ` [PATCH v5 7/9] drivers/base/memory: count inherited poisoned frames into the block Breno Leitao
@ 2026-09-15 12:53 ` Breno Leitao
2026-09-15 14:11 ` sashiko-bot
2026-09-15 12:53 ` [PATCH v5 9/9] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator Breno Leitao
8 siblings, 1 reply; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 12:53 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, linux-cxl, driver-core, kernel-team
A frame the previous kernel recorded as poisoned has to be flagged before
it reaches the allocator, which is long before memory_failure() can run.
Add a helper that leaves it 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.
num_poisoned_pages_inc() does not work at boot: its per memory block half
looks the block up by pfn, and memory_dev_init() has not run, so it
divides by zero. Take only the global counter there. A hotplugged block is
already there, so that path takes both counters as usual.
The caller comes later in this series.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
include/linux/mm.h | 7 +++++++
mm/memory-failure.c | 26 ++++++++++++++++++++++++++
2 files changed, 33 insertions(+)
diff --git a/include/linux/mm.h b/include/linux/mm.h
index b68824fcfbef19..8039830998dd4b 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -5225,6 +5225,8 @@ 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,
+ enum meminit_context context);
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 +5234,11 @@ static inline void memory_failure_queue(unsigned long pfn, int flags)
{
}
+static inline void hwpoison_boot_page(struct page *page,
+ enum meminit_context context)
+{
+}
+
static inline void num_poisoned_pages_inc(unsigned long pfn)
{
}
diff --git a/mm/memory-failure.c b/mm/memory-failure.c
index d9b8be696aac38..60e9682434700b 100644
--- a/mm/memory-failure.c
+++ b/mm/memory-failure.c
@@ -137,6 +137,32 @@ 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);
+
+void __meminit hwpoison_boot_page(struct page *page,
+ enum meminit_context context)
+{
+ unsigned long pfn = page_to_pfn(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(pfn, MF_RECOVERED);
+
+ /*
+ * The per memory block half of num_poisoned_pages_inc() has no block to
+ * find at boot, and divides by zero looking for one. A hotplugged block
+ * is already there.
+ */
+ if (context == MEMINIT_HOTPLUG)
+ num_poisoned_pages_inc(pfn);
+ else
+ 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.
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH v5 9/9] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator
2026-09-15 12:53 [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (7 preceding siblings ...)
2026-09-15 12:53 ` [PATCH v5 8/9] mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame Breno Leitao
@ 2026-09-15 12:53 ` Breno Leitao
2026-09-15 14:25 ` sashiko-bot
2026-09-16 8:32 ` Vlastimil Babka (SUSE)
8 siblings, 2 replies; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 12:53 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, Breno Leitao,
harry, linux-cxl, driver-core, 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.
A block runs up to MAX_PAGE_ORDER and a unit is 2MB, so hold back only
the frames the table covers and free the rest one at a time. The
allocator merges them back up, so a unit costs the frames it names, not
the whole block.
The frames that are freed go back through accept_and_free_block(), so a
clean frame in a block that is still unaccepted is accepted first.
A frame is flagged before its memory block exists, so the per block
counter is seeded from the page flag when the block is created rather
than incremented here.
Suggested-by: Kiryl Shutsemau <kas@kernel.org>
Signed-off-by: Breno Leitao <leitao@debian.org>
---
mm/page_alloc.c | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index b07b5f4751cb95..cca67a2702a421 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -1597,6 +1597,25 @@ static void __meminit accept_and_free_block(struct page *page,
__free_pages_ok(page, order, FPI_TO_TAIL);
}
+static void __meminit free_poisoned_block(struct page *page, unsigned int order,
+ enum meminit_context context)
+{
+ unsigned long i, nr_pages = 1UL << order;
+
+ for (i = 0; i < nr_pages; i++) {
+ struct page *p = page + i;
+ phys_addr_t phys = page_to_phys(p);
+
+ if (range_contains_poisoned_memory(phys, PAGE_SIZE)) {
+ hwpoison_boot_page(p, context);
+ continue;
+ }
+
+ /* this part of the block is not poisoned */
+ accept_and_free_block(p, 0);
+ }
+}
+
void __meminit __free_pages_core(struct page *page, unsigned int order,
enum meminit_context context)
{
@@ -1631,6 +1650,13 @@ 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)) {
+ free_poisoned_block(page, order, context);
+ return;
+ }
+
accept_and_free_block(page, order);
}
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 1/9] mm/page_alloc: factor out the accept-and-free tail of __free_pages_core()
2026-09-15 12:53 ` [PATCH v5 1/9] mm/page_alloc: factor out the accept-and-free tail of __free_pages_core() Breno Leitao
@ 2026-09-15 12:57 ` sashiko-bot
0 siblings, 0 replies; 27+ messages in thread
From: sashiko-bot @ 2026-09-15 12:57 UTC (permalink / raw)
To: hannes, Naoya Horiguchi, Breno Leitao, Lorenzo Stoakes,
Ingo Molnar, Miaohe Lin, Suren Baghdasaryan, Oscar Salvador,
Andrew Morton, Brendan Jackman, Dave Hansen, shakeel.butt,
Johannes Weiner, kas, Danilo Krummrich, Michal Hocko,
Ard Biesheuvel, Borislav Petkov, H. Peter Anvin, x86,
Vlastimil Babka, Ilias Apalodimas, Zi Yan, Rafael J. Wysocki,
Greg Kroah-Hartman, Mike Rapoport, Liam R. Howlett,
David Hildenbrand, Thomas Gleixner
Cc: linux-kernel, kernel-team, linux-cxl, rmikey, linux-efi, riel,
kexec, harry, driver-core, linux-mm
> __free_pages_core() ends by accepting the block if it is still
> unaccepted and then handing it to the allocator. The poisoned-memory
> replay later in this series needs that same tail for the parts of a
> block it does not withhold.
>
> Move it into accept_and_free_block(). This will be used later, when
> we need to accept and free a subset of a order-block that is
> not-poisioned.
>
> 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/20260915-hwpoison-kho-v5-0-3bc7a57bd503@debian.org?part=1
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 2/9] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table
2026-09-15 12:53 ` [PATCH v5 2/9] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
@ 2026-09-15 13:05 ` sashiko-bot
0 siblings, 0 replies; 27+ messages in thread
From: sashiko-bot @ 2026-09-15 13:05 UTC (permalink / raw)
To: Danilo Krummrich, David Hildenbrand, Naoya Horiguchi,
Borislav Petkov, Rafael J. Wysocki, Dave Hansen, Mike Rapoport,
Michal Hocko, Ard Biesheuvel, Oscar Salvador, Thomas Gleixner,
Ilias Apalodimas, Brendan Jackman, Johannes Weiner, Breno Leitao,
kas, Liam R. Howlett, Ingo Molnar, Vlastimil Babka,
Lorenzo Stoakes, x86, Zi Yan, Suren Baghdasaryan, H. Peter Anvin,
Greg Kroah-Hartman, Andrew Morton, hannes, shakeel.butt,
Miaohe Lin
Cc: linux-kernel, driver-core, kernel-team, riel, linux-cxl,
linux-mm, rmikey, harry, kexec, linux-efi
> 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.
>
> [ ... ]
>
> 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/20260915-hwpoison-kho-v5-0-3bc7a57bd503@debian.org?part=2
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 3/9] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-09-15 12:53 ` [PATCH v5 3/9] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
@ 2026-09-15 13:15 ` sashiko-bot
2026-09-15 14:00 ` Breno Leitao
2026-09-16 15:39 ` Usama Arif
1 sibling, 1 reply; 27+ messages in thread
From: sashiko-bot @ 2026-09-15 13:15 UTC (permalink / raw)
To: Suren Baghdasaryan, Johannes Weiner, Mike Rapoport,
Brendan Jackman, Ilias Apalodimas, Zi Yan, Miaohe Lin,
H. Peter Anvin, shakeel.butt, kas, Ard Biesheuvel, Andrew Morton,
Rafael J. Wysocki, x86, hannes, Thomas Gleixner, Naoya Horiguchi,
Ingo Molnar, Greg Kroah-Hartman, Danilo Krummrich,
Lorenzo Stoakes, Michal Hocko, Vlastimil Babka, Liam R. Howlett,
Borislav Petkov, Dave Hansen, Oscar Salvador, David Hildenbrand,
Breno Leitao
Cc: kernel-team, driver-core, linux-kernel, linux-cxl, rmikey, riel,
harry, kexec, linux-efi, linux-mm
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] mm: efi: integer overflow in poisoned memory bitmap allocation on 32-bit
--
commit 591eef51f672049b14b50a75d67dac823925a19a
Author: Breno Leitao <leitao@debian.org>
mm/memory-failure: libstub: install the poisoned-memory EFI table
This commit introduces a new poisoned-memory EFI table installed by the stub.
It sizes a bitmap based on the UEFI memory map to describe the RAM span and
allocates it as EFI_ACPI_RECLAIM_MEMORY so the next kernel does not take it
as free RAM.
> diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
> index 48f93f7758e9e..9c66e06c972c5 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: High]
Could this allocation size wrap around on 32-bit EFI architectures if the
memory map is extremely sparse?
In efi_get_ram_range(), ram_top is determined from EFI memory map
descriptors. If a descriptor is placed at a very high physical address,
bitmap_size could be large enough to wrap around 32-bit arithmetic.
On 32-bit EFI, the allocation size passed to allocate_pool (which takes
a 32-bit UINTN) is implicitly truncated. For example, if bitmap_size modulo
2^32 is very large (e.g., 0xFFFFFFF0), sizeof(*pm) + bitmap_size could
truncate to a tiny allocation (e.g., 8 bytes).
> + 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: High]
If the allocation size wrapped around, could this memset cause a heap
buffer overflow?
The 64-bit bitmap_size is passed to memset, which takes a 32-bit size_t on
32-bit architectures. This independently truncates bitmap_size (e.g., to
0xFFFFFFF0). Since the prior allocation could have wrapped to a very small
size, this would write gigabytes of zeroes into a tiny allocation, causing
boot-time memory corruption.
Would it be safer to check if bitmap_size exceeds the maximum allocation
size for the architecture, or use a safe addition helper before allocating?
> +
> + return pm;
> +}
> +
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260915-hwpoison-kho-v5-0-3bc7a57bd503@debian.org?part=3
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 4/9] mm/memory-failure: efi: adopt the inherited poisoned-memory table
2026-09-15 12:53 ` [PATCH v5 4/9] mm/memory-failure: efi: adopt the inherited poisoned-memory table Breno Leitao
@ 2026-09-15 13:24 ` sashiko-bot
0 siblings, 0 replies; 27+ messages in thread
From: sashiko-bot @ 2026-09-15 13:24 UTC (permalink / raw)
To: Breno Leitao, David Hildenbrand, Mike Rapoport, Oscar Salvador,
Ingo Molnar, Dave Hansen, hannes, Ard Biesheuvel, shakeel.butt,
Borislav Petkov, Ilias Apalodimas, Johannes Weiner,
Liam R. Howlett, Vlastimil Babka, Greg Kroah-Hartman,
Naoya Horiguchi, Andrew Morton, Thomas Gleixner, x86,
Michal Hocko, Brendan Jackman, Rafael J. Wysocki, Zi Yan,
H. Peter Anvin, Suren Baghdasaryan, Lorenzo Stoakes, kas,
Miaohe Lin, Danilo Krummrich
Cc: linux-mm, linux-efi, rmikey, harry, riel, driver-core,
kernel-team, linux-cxl, linux-kernel, kexec
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] efi: ABI incompatibility in poisoned-memory table due to unsigned long
- [High] efi: ABI incompatibility in poisoned-memory table due to PAGE_SIZE check
--
commit 213ea31d156bf79dbf85ad9fc8678fe09bddbde2
Author: Breno Leitao <leitao@debian.org>
mm/memory-failure: efi: adopt the inherited poisoned-memory table
This commit takes the inherited EFI poisoned-memory table into use by parsing
it in efi_config_parse_tables(). It validates the table header and hands its
pages to memblock so everything afterwards can reach it with phys_to_virt().
> diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
> new file mode 100644
> index 0000000000000..3f12db3dc9b84
> --- /dev/null
> +++ b/drivers/firmware/efi/poison.c
> @@ -0,0 +1,93 @@
[ ... ]
> +static bool __init
> +efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)
> +{
> + u64 nbits, end;
> +
> + /* 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 logic break binary compatibility across different kernel
bitnesses by relying on unsigned long?
The ABI structure definition for linux_efi_poisoned_memory uses a
variable-width type for the bitmap (unsigned long bitmap[]) rather than a
fixed-width type like u64. If a 32-bit kernel (or 32-bit EFI environment)
creates the table with a size that is a multiple of 4 (but not 8) and
passes it via kexec to a 64-bit kernel, this validation will fail since
sizeof(unsigned long) is 8.
The 64-bit consumer kernel will reject the valid poisoned-memory table,
losing hardware memory poison information. Accessing the unpoisoned memory
subsequently triggers a Machine Check Exception (MCE), crashing the system.
> +
> + /* And a footprint that can be page aligned without wrapping either. */
> + if (check_add_overflow(efi.poisoned_memory, sizeof(*pm) + pm->size,
> + &end) || end > PHYS_ADDR_MAX - PAGE_SIZE)
> + return false;
> +
> + if (pm->unit_size < PAGE_SIZE || !is_power_of_2(pm->unit_size))
> + return false;
[Severity: High]
Can this logic break kexec chains by tying the acceptance of an external ABI
table to the consumer's local compile-time PAGE_SIZE?
If a kernel configured with a smaller PAGE_SIZE (e.g., 4KB) sets
pm->unit_size to 4096 and kexecs into a kernel configured with a larger
PAGE_SIZE (e.g., 64KB), the new kernel evaluates 4096 < 65536 and entirely
rejects the table.
Discarding all hardware memory poison records can result in MCEs and system
crashes when the supposedly good memory is allocated and accessed. Should
the consumer instead accept the table and safely round up poisoned ranges to
its native page granularity?
> +
> + return IS_ALIGNED(pm->phys_base, pm->unit_size);
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260915-hwpoison-kho-v5-0-3bc7a57bd503@debian.org?part=4
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 5/9] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
2026-09-15 12:53 ` [PATCH v5 5/9] mm/memory-failure: efi: record hardware-poisoned frames into the " Breno Leitao
@ 2026-09-15 13:36 ` sashiko-bot
2026-09-15 14:33 ` Breno Leitao
0 siblings, 1 reply; 27+ messages in thread
From: sashiko-bot @ 2026-09-15 13:36 UTC (permalink / raw)
To: David Hildenbrand, Naoya Horiguchi, kas, Zi Yan, Breno Leitao,
hannes, shakeel.butt, Liam R. Howlett, Miaohe Lin,
Brendan Jackman, Andrew Morton, Thomas Gleixner,
Rafael J. Wysocki, x86, Lorenzo Stoakes, Suren Baghdasaryan,
Ilias Apalodimas, H. Peter Anvin, Ard Biesheuvel,
Borislav Petkov, Johannes Weiner, Danilo Krummrich, Michal Hocko,
Vlastimil Babka, Ingo Molnar, Dave Hansen, Oscar Salvador,
Greg Kroah-Hartman, Mike Rapoport
Cc: kernel-team, kexec, linux-cxl, linux-kernel, linux-efi, linux-mm,
rmikey, driver-core, riel, harry
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] efi: memblock_add() with PAGE_ALIGN_DOWN forces adjacent MMIO into system RAM
- [High] efi: ABI breakage in linux_efi_poisoned_memory due to native word size
- [Medium] mm: hard memory failures on soft-offlined pages are omitted from EFI table
--
commit e69689c81050c18119d69206dc9a7cb1e2fa4eeb
Author: Breno Leitao <leitao@debian.org>
mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
This commit hooks memory_failure() hard offline reports into the EFI
poisoned memory table so that they persist across kexec. Soft-offlined
pages are deliberately excluded to prevent predictive offlining from
becoming a permanent loss.
> diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
> index 3f12db3dc9b84..847592862f01d 100644
> --- a/drivers/firmware/efi/poison.c
> +++ b/drivers/firmware/efi/poison.c
> @@ -91,3 +91,30 @@ void __init efi_poisoned_memory_reserve(void)
> memblock_add(start, end - start);
> memblock_reserve(start, end - start);
[Severity: Critical]
Does this forcefully override the firmware's memory map for adjacent 4KB
chunks on larger page architectures?
On architectures like ARM64 where the kernel page size can be 16KB or
64KB, EFI memory descriptors remain 4KB. If the EFI poisoned memory table
occupies a 4KB region within a 64KB page, rounding down to the kernel
page boundary expands the range.
This could force adjacent 4KB EFI regions containing firmware MMIO or
reserved non-RAM regions into system RAM via memblock_add(). If the
kernel later maps these regions with Write-Back caching attributes in the
direct map, could this cause fatal Machine Check Exceptions or SError
panics during speculative CPU accesses?
[ ... ]
> diff --git a/include/linux/efi.h b/include/linux/efi.h
> index dd3263456dd4a..56402fdccd114 100644
> --- a/include/linux/efi.h
> +++ b/include/linux/efi.h
> @@ -1288,8 +1288,10 @@ struct linux_efi_poisoned_memory {
[Severity: High]
Does the linux_efi_poisoned_memory ABI structure break on architectures
where the native word size differs from the firmware?
The struct uses an unsigned long flexible array for the bitmap, which
alters the element size depending on the kernel bitness (4 bytes on 32-bit
and 8 bytes on 64-bit). efi_poison_geometry_valid() rejects tables where
the size is not a multiple of sizeof(unsigned long).
If a 32-bit EFI firmware generates this table, it will use 4-byte alignment
and a 32-bit word array. Because CONFIG_EFI_POISONED_MEMORY is 64-bit only,
the 64-bit kernel will evaluate sizeof(unsigned long) as 8 and reject
perfectly valid 32-bit firmware tables as malformed.
Since bit array layouts also differ across word sizes on big-endian
architectures, should this firmware-to-kernel ABI boundary use fixed-size
types like u32 or u64 instead?
>
> #ifdef CONFIG_EFI_POISONED_MEMORY
> void __init efi_poisoned_memory_reserve(void);
> +void efi_hwpoison_record_pfn(unsigned long pfn);
[ ... ]
> 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
[ ... ]
> @@ -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);
> }
[Severity: Medium]
Does placing efi_hwpoison_record_pfn() inside this conditional block
unintentionally skip recording hard errors on previously soft-offlined
pages?
Soft-offline marks a page with PageHWPoison but intentionally does not
record it in the EFI table to avoid permanent loss for predictive errors.
If a genuine hard, uncorrectable error later occurs on that same page,
memory_failure() will see the existing PageHWPoison flag and pass
MF_MSG_ALREADY_POISONED to action_result().
By coupling the EFI recording to the meminfo stat increments, genuine
hard errors on soft-offlined pages are omitted from the EFI table. Could
this allow the next kernel to map and crash on physically broken RAM?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260915-hwpoison-kho-v5-0-3bc7a57bd503@debian.org?part=5
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 6/9] mm/memory-failure: efi: answer whether a range is poisoned
2026-09-15 12:53 ` [PATCH v5 6/9] mm/memory-failure: efi: answer whether a range is poisoned Breno Leitao
@ 2026-09-15 13:45 ` sashiko-bot
2026-09-16 6:44 ` David Hildenbrand (Arm)
0 siblings, 1 reply; 27+ messages in thread
From: sashiko-bot @ 2026-09-15 13:45 UTC (permalink / raw)
To: Andrew Morton, hannes, Borislav Petkov, Danilo Krummrich,
Brendan Jackman, x86, kas, Suren Baghdasaryan, Zi Yan,
Liam R. Howlett, Vlastimil Babka, Thomas Gleixner,
Oscar Salvador, Rafael J. Wysocki, shakeel.butt, Johannes Weiner,
David Hildenbrand, Ilias Apalodimas, Miaohe Lin, Ard Biesheuvel,
Breno Leitao, Michal Hocko, Naoya Horiguchi, Greg Kroah-Hartman,
Mike Rapoport, H. Peter Anvin, Lorenzo Stoakes, Dave Hansen,
Ingo Molnar
Cc: linux-mm, kexec, linux-cxl, linux-efi, linux-kernel, harry,
rmikey, kernel-team, driver-core, riel
> 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>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260915-hwpoison-kho-v5-0-3bc7a57bd503@debian.org?part=6
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 7/9] drivers/base/memory: count inherited poisoned frames into the block
2026-09-15 12:53 ` [PATCH v5 7/9] drivers/base/memory: count inherited poisoned frames into the block Breno Leitao
@ 2026-09-15 13:59 ` sashiko-bot
2026-09-16 6:48 ` David Hildenbrand (Arm)
1 sibling, 0 replies; 27+ messages in thread
From: sashiko-bot @ 2026-09-15 13:59 UTC (permalink / raw)
To: Ingo Molnar, Greg Kroah-Hartman, Liam R. Howlett, Mike Rapoport,
Danilo Krummrich, H. Peter Anvin, Lorenzo Stoakes, Zi Yan,
Michal Hocko, Borislav Petkov, David Hildenbrand,
Brendan Jackman, Johannes Weiner, Oscar Salvador, kas,
Naoya Horiguchi, hannes, Rafael J. Wysocki, Suren Baghdasaryan,
x86, Breno Leitao, Miaohe Lin, Ard Biesheuvel, Dave Hansen,
shakeel.butt, Vlastimil Babka, Andrew Morton, Ilias Apalodimas,
Thomas Gleixner
Cc: rmikey, linux-efi, kernel-team, linux-cxl, linux-kernel, harry,
linux-mm, driver-core, riel, kexec
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] mm: TOCTOU race in memblk_nr_poison_init() drops early MCE counts
--
commit a8282f47e9b2f9ae014e9948a2edfc336de13cd0
Author: Breno Leitao <leitao@debian.org>
drivers/base/memory: count inherited poisoned frames into the block
This commit initializes the poisoned frame count for memory blocks created
during early boot by scanning for pre-existing poisoned pages. It addresses
an issue where kexec-inherited poisoned frames would later cause a counter
underflow on unpoisoning, preventing the block from being onlined.
> diff --git a/drivers/base/memory.c b/drivers/base/memory.c
> index 5eead3346f1e3..49a33ddbb2e71 100644
> --- a/drivers/base/memory.c
> +++ b/drivers/base/memory.c
[ ... ]
> @@ -807,6 +812,7 @@ static int add_memory_block(unsigned long block_id, int nid, unsigned long state
> mem->state = state;
> mem->nid = nid;
> INIT_LIST_HEAD(&mem->group_next);
> + memblk_nr_poison_init(mem);
>
> #ifndef CONFIG_NUMA
> if (state == MEM_ONLINE)
[Severity: Medium]
Could this ordering create a Time-Of-Check to Time-Of-Use (TOCTOU) race
during early boot?
If a hardware machine check exception (MCE) occurs concurrently after
memblk_nr_poison_init() runs but before __add_memory_block() publishes the
block via xa_store(), does the memory_failure() worker set the page flag
but drop the block counter increment because memory_block_get() still
returns NULL?
If this concurrent increment is dropped, could a subsequent unpoison
operation underflow mem->nr_hwpoison to ULONG_MAX, permanently breaking
memory_block_online() with -EHWPOISON?
[ ... ]
> @@ -1251,4 +1257,33 @@ static unsigned long memblk_nr_poison(struct memory_block *mem)
> {
> return atomic_long_read(&mem->nr_hwpoison);
> }
> +
> +/*
> + * Frames a kexec handed over are flagged as they reach the allocator, long
> + * before this block exists, so memblk_nr_poison_inc() had nowhere to count
> + * them. Take them from the page flag instead.
> + */
> +static void memblk_nr_poison_init(struct memory_block *mem)
> +{
> + unsigned long pfn = section_nr_to_pfn(mem->start_section_nr);
> + unsigned long nr_pages = PAGES_PER_SECTION * sections_per_block;
> + unsigned long i, nr_poison = 0;
> +
> + /* A hotplugged block is created before its pages are online. */
> + if (mem->state != MEM_ONLINE)
> + return;
> +
> + if (!range_contains_poisoned_memory(PFN_PHYS(pfn),
> + nr_pages << PAGE_SHIFT))
> + return;
[Severity: Medium]
Does this check unconditionally return false for a fresh early MCE because
the MCE is not in the inherited EFI table?
If so, would this skip the manual walk, leaving the counter entirely
dependent on the concurrent memory_failure() increment that might be
dropped in the race window described in add_memory_block() above?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260915-hwpoison-kho-v5-0-3bc7a57bd503@debian.org?part=7
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 3/9] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-09-15 13:15 ` sashiko-bot
@ 2026-09-15 14:00 ` Breno Leitao
0 siblings, 0 replies; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 14:00 UTC (permalink / raw)
To: sashiko-bot
Cc: Suren Baghdasaryan, Johannes Weiner, Mike Rapoport,
Brendan Jackman, Ilias Apalodimas, Zi Yan, Miaohe Lin,
H. Peter Anvin, shakeel.butt, kas, Ard Biesheuvel, Andrew Morton,
Rafael J. Wysocki, x86, hannes, Thomas Gleixner, Naoya Horiguchi,
Ingo Molnar, Greg Kroah-Hartman, Danilo Krummrich,
Lorenzo Stoakes, Michal Hocko, Vlastimil Babka, Liam R. Howlett,
Borislav Petkov, Dave Hansen, Oscar Salvador, David Hildenbrand,
kernel-team, driver-core, linux-kernel, linux-cxl, rmikey, riel,
harry, kexec, linux-efi, linux-mm
On Tue, Sep 15, 2026 at 01:15:27PM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [High] mm: efi: integer overflow in poisoned memory bitmap allocation on 32-bit
> --
>
> commit 591eef51f672049b14b50a75d67dac823925a19a
> Author: Breno Leitao <leitao@debian.org>
>
> mm/memory-failure: libstub: install the poisoned-memory EFI table
>
> This commit introduces a new poisoned-memory EFI table installed by the stub.
> It sizes a bitmap based on the UEFI memory map to describe the RAM span and
> allocates it as EFI_ACPI_RECLAIM_MEMORY so the next kernel does not take it
> as free RAM.
>
> > diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
> > index 48f93f7758e9e..9c66e06c972c5 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: High]
> Could this allocation size wrap around on 32-bit EFI architectures if the
> memory map is extremely sparse?
This feature is limited to 64-bits only, so, these wraps on 32-bit
architectures are not real:
config EFI_POISONED_MEMORY
def_bool y
depends on EFI_STUB && MEMORY_FAILURE && 64BIT
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 8/9] mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame
2026-09-15 12:53 ` [PATCH v5 8/9] mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame Breno Leitao
@ 2026-09-15 14:11 ` sashiko-bot
0 siblings, 0 replies; 27+ messages in thread
From: sashiko-bot @ 2026-09-15 14:11 UTC (permalink / raw)
To: Oscar Salvador, Suren Baghdasaryan, Danilo Krummrich,
Greg Kroah-Hartman, Liam R. Howlett, Michal Hocko,
Rafael J. Wysocki, H. Peter Anvin, Ilias Apalodimas, Zi Yan,
Miaohe Lin, Borislav Petkov, Thomas Gleixner, Vlastimil Babka,
Johannes Weiner, Mike Rapoport, Lorenzo Stoakes, Andrew Morton,
x86, David Hildenbrand, Naoya Horiguchi, shakeel.butt,
Brendan Jackman, hannes, Ard Biesheuvel, kas, Ingo Molnar,
Dave Hansen, Breno Leitao
Cc: linux-cxl, linux-mm, rmikey, linux-kernel, kernel-team,
linux-efi, harry, driver-core, kexec, riel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] mm: memory_failure_stats data race in hwpoison_boot_page()
--
commit 95ab593413859ad843df67ede9095a08529eb0cd
Author: Breno Leitao <leitao@debian.org>
mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame
This commit adds a helper, hwpoison_boot_page(), to flag inherited frames
recorded as poisoned by the previous kernel before they reach the allocator.
It leaves the frame in the state expected for poisoned frames, allowing
existing PG_hwpoison logic to cover it appropriately.
> diff --git a/mm/memory-failure.c b/mm/memory-failure.c
> index d9b8be696aac3..60e9682434700 100644
> --- a/mm/memory-failure.c
> +++ b/mm/memory-failure.c
[ ... ]
> @@ -137,6 +137,32 @@ 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);
> +
> +void __meminit hwpoison_boot_page(struct page *page,
> + enum meminit_context context)
> +{
> + unsigned long pfn = page_to_pfn(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(pfn, MF_RECOVERED);
[Severity: Medium]
Is it safe to call update_per_node_mf_stats() here without acquiring mf_mutex?
During memory hotplug (when context == MEMINIT_HOTPLUG), if
hwpoison_boot_page() is called for inherited poisoned frames, it appears this
could race with runtime hardware error processing on other CPUs.
Other paths like memory_failure() update these same per-node statistics while
holding mf_mutex. Because update_per_node_mf_stats() performs non-atomic
increments:
mm/memory-failure.c:update_per_node_mf_stats() {
...
case MF_RECOVERED:
++mf_stats->recovered;
break;
...
++mf_stats->total;
}
Could calling it here circumvent the mutex and corrupt the memory failure
sysfs statistics by losing counter updates?
> +
> + /*
> + * The per memory block half of num_poisoned_pages_inc() has no block to
> + * find at boot, and divides by zero looking for one. A hotplugged block
> + * is already there.
> + */
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260915-hwpoison-kho-v5-0-3bc7a57bd503@debian.org?part=8
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 9/9] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator
2026-09-15 12:53 ` [PATCH v5 9/9] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator Breno Leitao
@ 2026-09-15 14:25 ` sashiko-bot
2026-09-16 8:32 ` Vlastimil Babka (SUSE)
1 sibling, 0 replies; 27+ messages in thread
From: sashiko-bot @ 2026-09-15 14:25 UTC (permalink / raw)
To: Zi Yan, hannes, Danilo Krummrich, Oscar Salvador,
Vlastimil Babka, Thomas Gleixner, shakeel.butt, Ard Biesheuvel,
Michal Hocko, Dave Hansen, H. Peter Anvin, Andrew Morton,
Borislav Petkov, Ingo Molnar, x86, Miaohe Lin, kas,
Mike Rapoport, Naoya Horiguchi, Brendan Jackman, Lorenzo Stoakes,
Greg Kroah-Hartman, Johannes Weiner, Ilias Apalodimas,
David Hildenbrand, Liam R. Howlett, Rafael J. Wysocki,
Breno Leitao, Suren Baghdasaryan
Cc: kexec, linux-efi, linux-kernel, linux-cxl, riel, harry,
driver-core, kernel-team, linux-mm, rmikey
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] mm: panic when accepting clean pages sharing an unaccepted unit with poisoned pages
- [Medium] mm: lost memory failure stats due to lockless update in hwpoison_boot_page()
--
commit 92cea9db628cbe8f9651b225628684a39c80ddd4
Author: Breno Leitao <leitao@debian.org>
Subject: mm/memory-failure: keep inherited poisoned frames out of the buddy allocator
This commit intercepts inherited poisoned pages before they enter the buddy
allocator. It processes memory blocks by checking for poisoned pages, flagging
them, and passing the remaining clean pages through the allocator. This
prevents unaccepted memory blocks with poisoned frames from being mistakenly
re-merged.
> diff --git a/mm/page_alloc.c b/mm/page_alloc.c
> index b07b5f4751cb9..cca67a2702a42 100644
> --- a/mm/page_alloc.c
> +++ b/mm/page_alloc.c
[ ... ]
> @@ -1597,6 +1597,25 @@ static void __meminit accept_and_free_block(struct page *page,
> __free_pages_ok(page, order, FPI_TO_TAIL);
> }
>
> +static void __meminit free_poisoned_block(struct page *page, unsigned int order,
> + enum meminit_context context)
> +{
> + unsigned long i, nr_pages = 1UL << order;
> +
> + for (i = 0; i < nr_pages; i++) {
> + struct page *p = page + i;
> + phys_addr_t phys = page_to_phys(p);
> +
> + if (range_contains_poisoned_memory(phys, PAGE_SIZE)) {
> + hwpoison_boot_page(p, context);
[Severity: Medium]
Is this call to hwpoison_boot_page() safe from data races during memory
hotplug?
When called during hotplug (where context == MEMINIT_HOTPLUG),
hwpoison_boot_page() calls update_per_node_mf_stats() which performs
non-atomic read-modify-write operations on the node stats:
mm/memory-failure.c:update_per_node_mf_stats() {
...
++mf_stats->recovered;
...
}
Since hwpoison_boot_page() does not acquire mf_mutex, could a concurrent
memory_failure() execution on the same NUMA node corrupt the recovered and
total counters?
> + continue;
> + }
> +
> + /* this part of the block is not poisoned */
> + accept_and_free_block(p, 0);
[Severity: High]
Can this sequence cause a kernel panic when accepting clean pages that share
an unaccepted memory unit with poisoned pages?
When a clean page belongs to an unaccepted memory region,
accept_and_free_block() invokes accept_memory(). However, accept_memory()
operates at the unit_size granularity (typically 2MB) and rounds down the
start address to this boundary before calling arch_accept_memory():
drivers/firmware/efi/unaccepted_memory.c:accept_memory() {
...
arch_accept_memory(phys_start, phys_end);
...
}
Because the clean page and the poisoned page share the same unaccepted
unit_size block, won't arch_accept_memory() be inadvertently executed on the
poisoned page as well?
If arch_accept_memory() attempts to accept physically poisoned memory (such
as via TDX TDG.MEM.PAGE.ACCEPT), could this trigger an unrecoverable Machine
Check Exception?
> + }
> +}
> +
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260915-hwpoison-kho-v5-0-3bc7a57bd503@debian.org?part=9
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 5/9] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
2026-09-15 13:36 ` sashiko-bot
@ 2026-09-15 14:33 ` Breno Leitao
0 siblings, 0 replies; 27+ messages in thread
From: Breno Leitao @ 2026-09-15 14:33 UTC (permalink / raw)
To: sashiko-reviews
Cc: David Hildenbrand, Naoya Horiguchi, kas, Zi Yan, hannes,
shakeel.butt, Liam R. Howlett, Miaohe Lin, Brendan Jackman,
Andrew Morton, Thomas Gleixner, Rafael J. Wysocki, x86,
Lorenzo Stoakes, Suren Baghdasaryan, Ilias Apalodimas,
H. Peter Anvin, Ard Biesheuvel, Borislav Petkov, Johannes Weiner,
Danilo Krummrich, Michal Hocko, Vlastimil Babka, Ingo Molnar,
Dave Hansen, Oscar Salvador, Greg Kroah-Hartman, Mike Rapoport,
kernel-team, kexec, linux-cxl, linux-kernel, linux-efi, linux-mm,
rmikey, driver-core, riel, harry
On Tue, Sep 15, 2026 at 01:36:29PM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
> - [Critical] efi: memblock_add() with PAGE_ALIGN_DOWN forces adjacent MMIO into system RAM
> - [High] efi: ABI breakage in linux_efi_poisoned_memory due to native word size
> - [Medium] mm: hard memory failures on soft-offlined pages are omitted from EFI table
> --
>
> commit e69689c81050c18119d69206dc9a7cb1e2fa4eeb
> Author: Breno Leitao <leitao@debian.org>
>
> mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
>
> This commit hooks memory_failure() hard offline reports into the EFI
> poisoned memory table so that they persist across kexec. Soft-offlined
> pages are deliberately excluded to prevent predictive offlining from
> becoming a permanent loss.
>
> > diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
> > index 3f12db3dc9b84..847592862f01d 100644
> > --- a/drivers/firmware/efi/poison.c
> > +++ b/drivers/firmware/efi/poison.c
> > @@ -91,3 +91,30 @@ void __init efi_poisoned_memory_reserve(void)
> > memblock_add(start, end - start);
> > memblock_reserve(start, end - start);
>
> [Severity: Critical]
> Does this forcefully override the firmware's memory map for adjacent 4KB
> chunks on larger page architectures?
>
> On architectures like ARM64 where the kernel page size can be 16KB or
> 64KB, EFI memory descriptors remain 4KB. If the EFI poisoned memory table
> occupies a 4KB region within a 64KB page, rounding down to the kernel
> page boundary expands the range.
>
> This could force adjacent 4KB EFI regions containing firmware MMIO or
> reserved non-RAM regions into system RAM via memblock_add(). If the
> kernel later maps these regions with Write-Back caching attributes in the
> direct map, could this cause fatal Machine Check Exceptions or SError
> panics during speculative CPU accesses?
arm64 already rounds every EFI descriptor outward via
memrange_efi_to_native(), so it adds no new class of over-mapping, and
reserve_regions() adds and reserves this WB ACPI-reclaim table anyway
moments later.
So, it doesn't seem relevant?!
> > diff --git a/include/linux/efi.h b/include/linux/efi.h index
> > dd3263456dd4a..56402fdccd114 100644 --- a/include/linux/efi.h +++
> > b/include/linux/efi.h @@ -1288,8 +1288,10 @@ struct
> > linux_efi_poisoned_memory {
>
> [Severity: High] Does the linux_efi_poisoned_memory ABI structure
> break on architectures where the native word size differs from the
> firmware?
>
> The struct uses an unsigned long flexible array for the bitmap, which
> alters the element size depending on the kernel bitness (4 bytes on
> 32-bit and 8 bytes on 64-bit). efi_poison_geometry_valid() rejects
> tables where the size is not a multiple of sizeof(unsigned long).
>
> If a 32-bit EFI firmware generates this table, it will use 4-byte
> alignment and a 32-bit word array. Because CONFIG_EFI_POISONED_MEMORY
> is 64-bit only, the 64-bit kernel will evaluate sizeof(unsigned long)
> as 8 and reject perfectly valid 32-bit firmware tables as malformed.
>
> Since bit array layouts also differ across word sizes on big-endian
> architectures, should this firmware-to-kernel ABI boundary use
> fixed-size types like u32 or u64 instead?
That is not the case, because this feature depends on 64-bits and
!BIG_ENDIAN.
In fact, CONFIG_EFI is depends on OF && !CPU_BIG_ENDIAN
> > 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
> [ ... ]
> > @@ -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); }
>
> [Severity: Medium] Does placing efi_hwpoison_record_pfn() inside this
> conditional block unintentionally skip recording hard errors on
> previously soft-offlined pages?
>
> Soft-offline marks a page with PageHWPoison but intentionally does not
> record it in the EFI table to avoid permanent loss for predictive
> errors.
>
> If a genuine hard, uncorrectable error later occurs on that same page,
> memory_failure() will see the existing PageHWPoison flag and pass
> MF_MSG_ALREADY_POISONED to action_result().
>
> By coupling the EFI recording to the meminfo stat increments, genuine
> hard errors on soft-offlined pages are omitted from the EFI table.
> Could this allow the next kernel to map and crash on physically broken
> RAM?
Yes, this one is real, thanks.
soft_offline_page() reaches num_poisoned_pages_inc() through
page_handle_poison() and never calls action_result(), so the hook does
not need the MF_MSG_ALREADY_POISONED guard to keep soft offlines out of
the table. That guard is there for the stats, and coupling to it drops a
hard error on a page that was soft-offlined earlier.
The frame is not lost for good -- the next kernel hands it out, the error
comes back and is recorded then -- but that is one machine check per
kexec that this series exists to avoid.
I will move the call out of the stats guard in v6, keeping MF_MSG_PFN_MAP
excluded since that path is not System RAM
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 6/9] mm/memory-failure: efi: answer whether a range is poisoned
2026-09-15 13:45 ` sashiko-bot
@ 2026-09-16 6:44 ` David Hildenbrand (Arm)
0 siblings, 0 replies; 27+ messages in thread
From: David Hildenbrand (Arm) @ 2026-09-16 6:44 UTC (permalink / raw)
To: sashiko-reviews, Andrew Morton, hannes, Borislav Petkov,
Danilo Krummrich, Brendan Jackman, x86, kas, Suren Baghdasaryan,
Zi Yan, Liam R. Howlett, Vlastimil Babka, Thomas Gleixner,
Oscar Salvador, Rafael J. Wysocki, shakeel.butt, Johannes Weiner,
Ilias Apalodimas, Miaohe Lin, Ard Biesheuvel, Breno Leitao,
Michal Hocko, Naoya Horiguchi, Greg Kroah-Hartman, Mike Rapoport,
H. Peter Anvin, Lorenzo Stoakes, Dave Hansen, Ingo Molnar
Cc: linux-mm, kexec, linux-cxl, linux-efi, linux-kernel, harry,
rmikey, kernel-team, driver-core, riel
On 9/15/26 15:45, sashiko-bot@kernel.org wrote:
>> 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>
>
> Sashiko has reviewed this patch and found no issues. It looks great!
>
David has reviewed this mail and it annoyed him.
--
Cheers,
David
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 7/9] drivers/base/memory: count inherited poisoned frames into the block
2026-09-15 12:53 ` [PATCH v5 7/9] drivers/base/memory: count inherited poisoned frames into the block Breno Leitao
2026-09-15 13:59 ` sashiko-bot
@ 2026-09-16 6:48 ` David Hildenbrand (Arm)
2026-09-16 9:35 ` Breno Leitao
1 sibling, 1 reply; 27+ messages in thread
From: David Hildenbrand (Arm) @ 2026-09-16 6:48 UTC (permalink / raw)
To: Breno Leitao, Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin,
Naoya Horiguchi, Andrew Morton, kas, kexec, 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, harry,
linux-cxl, driver-core, kernel-team
On 9/15/26 14:53, Breno Leitao wrote:
> A frame a kexec handed over is flagged as it reaches the allocator, long
> before its memory block exists, so memblk_nr_poison_inc() had nowhere to
> count it. Walk the block once when it is created and take the count from
> the page flag instead.
>
> Without it an unpoison later subtracts from a counter that was never
> incremented and wraps it, which then refuses memory_block_online() for
> good.
>
> Only a block created online needs the walk. A hotplugged one is created
> before its pages are, so there is nothing to find, and its frames are
> counted by num_poisoned_pages_inc() as they are flagged.
>
> Signed-off-by: Breno Leitao <leitao@debian.org>
> ---
> drivers/base/memory.c | 35 +++++++++++++++++++++++++++++++++++
> 1 file changed, 35 insertions(+)
>
> diff --git a/drivers/base/memory.c b/drivers/base/memory.c
> index 5eead3346f1e32..49a33ddbb2e717 100644
> --- a/drivers/base/memory.c
> +++ b/drivers/base/memory.c
> @@ -220,11 +220,16 @@ int memory_notify(enum memory_block_state state, void *v)
>
> #if defined(CONFIG_MEMORY_FAILURE) && defined(CONFIG_MEMORY_HOTPLUG)
> static unsigned long memblk_nr_poison(struct memory_block *mem);
> +static void memblk_nr_poison_init(struct memory_block *mem);
> #else
> static inline unsigned long memblk_nr_poison(struct memory_block *mem)
> {
> return 0;
> }
> +
> +static inline void memblk_nr_poison_init(struct memory_block *mem)
> +{
> +}
> #endif
>
> /*
> @@ -807,6 +812,7 @@ static int add_memory_block(unsigned long block_id, int nid, unsigned long state
> mem->state = state;
> mem->nid = nid;
> INIT_LIST_HEAD(&mem->group_next);
> + memblk_nr_poison_init(mem);
>
> #ifndef CONFIG_NUMA
> if (state == MEM_ONLINE)
> @@ -1251,4 +1257,33 @@ static unsigned long memblk_nr_poison(struct memory_block *mem)
> {
> return atomic_long_read(&mem->nr_hwpoison);
> }
> +
> +/*
> + * Frames a kexec handed over are flagged as they reach the allocator, long
> + * before this block exists, so memblk_nr_poison_inc() had nowhere to count
> + * them. Take them from the page flag instead.
> + */
> +static void memblk_nr_poison_init(struct memory_block *mem)
> +{
> + unsigned long pfn = section_nr_to_pfn(mem->start_section_nr);
> + unsigned long nr_pages = PAGES_PER_SECTION * sections_per_block;
> + unsigned long i, nr_poison = 0;
> +
> + /* A hotplugged block is created before its pages are online. */
> + if (mem->state != MEM_ONLINE)
> + return;
> +
> + if (!range_contains_poisoned_memory(PFN_PHYS(pfn),
> + nr_pages << PAGE_SHIFT))
> + return;
> +
> + for (i = 0; i < nr_pages; i++) {
> + struct page *page = pfn_to_online_page(pfn + i);
> +
> + if (page && PageHWPoison(page))
> + nr_poison++;
> + }
That just slows down boot unnecessarily on 99.9999999999999999999% of all
systems out there.
--
Cheers,
David
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 9/9] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator
2026-09-15 12:53 ` [PATCH v5 9/9] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator Breno Leitao
2026-09-15 14:25 ` sashiko-bot
@ 2026-09-16 8:32 ` Vlastimil Babka (SUSE)
1 sibling, 0 replies; 27+ messages in thread
From: Vlastimil Babka (SUSE) @ 2026-09-16 8:32 UTC (permalink / raw)
To: Breno Leitao, Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin,
Naoya Horiguchi, Andrew Morton, kas, kexec, David Hildenbrand,
Lorenzo Stoakes, Liam R. Howlett, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Brendan Jackman, Johannes Weiner, Zi Yan, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, rmikey, riel, harry,
linux-cxl, driver-core, kernel-team
On 9/15/26 14:53, Breno Leitao wrote:
> 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.
>
> A block runs up to MAX_PAGE_ORDER and a unit is 2MB, so hold back only
> the frames the table covers and free the rest one at a time. The
> allocator merges them back up, so a unit costs the frames it names, not
> the whole block.
>
> The frames that are freed go back through accept_and_free_block(), so a
> clean frame in a block that is still unaccepted is accepted first.
>
> A frame is flagged before its memory block exists, so the per block
> counter is seeded from the page flag when the block is created rather
> than incremented here.
>
> Suggested-by: Kiryl Shutsemau <kas@kernel.org>
> Signed-off-by: Breno Leitao <leitao@debian.org>
Acked-by: Vlastimil Babka (SUSE) <vbabka@kernel.org>
> ---
> mm/page_alloc.c | 26 ++++++++++++++++++++++++++
> 1 file changed, 26 insertions(+)
>
> diff --git a/mm/page_alloc.c b/mm/page_alloc.c
> index b07b5f4751cb95..cca67a2702a421 100644
> --- a/mm/page_alloc.c
> +++ b/mm/page_alloc.c
> @@ -1597,6 +1597,25 @@ static void __meminit accept_and_free_block(struct page *page,
> __free_pages_ok(page, order, FPI_TO_TAIL);
> }
>
> +static void __meminit free_poisoned_block(struct page *page, unsigned int order,
> + enum meminit_context context)
> +{
> + unsigned long i, nr_pages = 1UL << order;
> +
> + for (i = 0; i < nr_pages; i++) {
> + struct page *p = page + i;
> + phys_addr_t phys = page_to_phys(p);
> +
> + if (range_contains_poisoned_memory(phys, PAGE_SIZE)) {
> + hwpoison_boot_page(p, context);
> + continue;
> + }
> +
> + /* this part of the block is not poisoned */
> + accept_and_free_block(p, 0);
> + }
> +}
> +
> void __meminit __free_pages_core(struct page *page, unsigned int order,
> enum meminit_context context)
> {
> @@ -1631,6 +1650,13 @@ 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)) {
> + free_poisoned_block(page, order, context);
> + return;
> + }
> +
> accept_and_free_block(page, order);
> }
>
>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 7/9] drivers/base/memory: count inherited poisoned frames into the block
2026-09-16 6:48 ` David Hildenbrand (Arm)
@ 2026-09-16 9:35 ` Breno Leitao
2026-09-16 14:51 ` David Hildenbrand (Arm)
0 siblings, 1 reply; 27+ messages in thread
From: Breno Leitao @ 2026-09-16 9:35 UTC (permalink / raw)
To: David Hildenbrand (Arm)
Cc: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kas, kexec, 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,
Oscar Salvador, Greg Kroah-Hartman, Rafael J. Wysocki,
Danilo Krummrich, hannes, shakeel.butt, linux-efi, linux-kernel,
linux-mm, rmikey, riel, harry, linux-cxl, driver-core,
kernel-team
Hello David,
First of all, thanks for your time looking at this patchset.
On Wed, Sep 16, 2026 at 08:48:56AM +0200, David Hildenbrand (Arm) wrote:
> On 9/15/26 14:53, Breno Leitao wrote:
> > +static void memblk_nr_poison_init(struct memory_block *mem)
> > +{
> > + unsigned long pfn = section_nr_to_pfn(mem->start_section_nr);
> > + unsigned long nr_pages = PAGES_PER_SECTION * sections_per_block;
> > + unsigned long i, nr_poison = 0;
> > +
> > + /* A hotplugged block is created before its pages are online. */
> > + if (mem->state != MEM_ONLINE)
> > + return;
> > +
> > + if (!range_contains_poisoned_memory(PFN_PHYS(pfn),
> > + nr_pages << PAGE_SHIFT))
> > + return;
> > +
> > + for (i = 0; i < nr_pages; i++) {
> > + struct page *page = pfn_to_online_page(pfn + i);
> > +
> > + if (page && PageHWPoison(page))
> > + nr_poison++;
> > + }
>
> That just slows down boot unnecessarily on 99.9999999999999999999% of all
> systems out there.
hmmm, I am not sure I see it that way.
The loop only runs for a block the bitmap marks. On a machine with nothing
recorded the bitmap is all zeros, the range_contains_poisoned_memory() check
right above it returns false, and the loop never executes.
What every boot does pay is that check, once per block. The stub installs
the table whether or not anything was ever recorded in it, so this is not a
NULL test: it is two 64-bit divisions by the unit size plus a
find_next_bit() over the single word a 128M block covers at one bit per 2M.
The real cost (that "for loop above"), comes when you kexec (not on cold
boot -- given the bitmap is empty), and you are trying to init
a memory block that has poisoned pages into it. Which seems the right
trade-off, no?
That said, can we do better? Yes. The silly win is to let the table say
whether anything was ever recorded in it, something like a
linux_efi_poisoned_memory->empty that the first recorded frame clears,
and return on that before the bitmap is reached at all.
Is this what you are looking for, or something more drastic?
Thanks for your review,
--breno
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 7/9] drivers/base/memory: count inherited poisoned frames into the block
2026-09-16 9:35 ` Breno Leitao
@ 2026-09-16 14:51 ` David Hildenbrand (Arm)
0 siblings, 0 replies; 27+ messages in thread
From: David Hildenbrand (Arm) @ 2026-09-16 14:51 UTC (permalink / raw)
To: Breno Leitao
Cc: Ard Biesheuvel, Ilias Apalodimas, Miaohe Lin, Naoya Horiguchi,
Andrew Morton, kas, kexec, 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,
Oscar Salvador, Greg Kroah-Hartman, Rafael J. Wysocki,
Danilo Krummrich, hannes, shakeel.butt, linux-efi, linux-kernel,
linux-mm, rmikey, riel, harry, linux-cxl, driver-core,
kernel-team
On 9/16/26 11:35, Breno Leitao wrote:
> Hello David,
>
> First of all, thanks for your time looking at this patchset.
>
> On Wed, Sep 16, 2026 at 08:48:56AM +0200, David Hildenbrand (Arm) wrote:
>> On 9/15/26 14:53, Breno Leitao wrote:
>>> +static void memblk_nr_poison_init(struct memory_block *mem)
>>> +{
>>> + unsigned long pfn = section_nr_to_pfn(mem->start_section_nr);
>>> + unsigned long nr_pages = PAGES_PER_SECTION * sections_per_block;
>>> + unsigned long i, nr_poison = 0;
>>> +
>>> + /* A hotplugged block is created before its pages are online. */
>>> + if (mem->state != MEM_ONLINE)
>>> + return;
>>> +
>>> + if (!range_contains_poisoned_memory(PFN_PHYS(pfn),
>>> + nr_pages << PAGE_SHIFT))
>>> + return;
>>> +
>>> + for (i = 0; i < nr_pages; i++) {
>>> + struct page *page = pfn_to_online_page(pfn + i);
>>> +
>>> + if (page && PageHWPoison(page))
>>> + nr_poison++;
>>> + }
>>
>> That just slows down boot unnecessarily on 99.9999999999999999999% of all
>> systems out there.
>
> hmmm, I am not sure I see it that way.
>
> The loop only runs for a block the bitmap marks. On a machine with nothing
> recorded the bitmap is all zeros, the range_contains_poisoned_memory() check
> right above it returns false, and the loop never executes.
>
> What every boot does pay is that check, once per block. The stub installs
> the table whether or not anything was ever recorded in it, so this is not a
> NULL test: it is two 64-bit divisions by the unit size plus a
> find_next_bit() over the single word a 128M block covers at one bit per 2M.
>
> The real cost (that "for loop above"), comes when you kexec (not on cold
> boot -- given the bitmap is empty), and you are trying to init
> a memory block that has poisoned pages into it. Which seems the right
> trade-off, no?
>
> That said, can we do better? Yes. The silly win is to let the table say
> whether anything was ever recorded in it, something like a
> linux_efi_poisoned_memory->empty that the first recorded frame clears,
> and return on that before the bitmap is reached at all.
>
> Is this what you are looking for, or something more drastic?
Ah, that magical "range_contains_poisoned_memory" does a bitmap scan?
I'm sorry, but that is absolutely confusing.
There is no way someone will figure out that range_contains_poisoned_memory()
queries some efi specific bitmap that won't even be able to represent any memory
outside of it's range.
I don't really have time to give a better solution, but starting with the
naming, range_contains_poisoned_memory() is just absolutely misleading.
--
Cheers,
David
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 3/9] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-09-15 12:53 ` [PATCH v5 3/9] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
2026-09-15 13:15 ` sashiko-bot
@ 2026-09-16 15:39 ` Usama Arif
1 sibling, 0 replies; 27+ messages in thread
From: Usama Arif @ 2026-09-16 15:39 UTC (permalink / raw)
To: Breno Leitao
Cc: Usama Arif, 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, Oscar Salvador,
Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, hannes,
shakeel.butt, linux-efi, linux-kernel, linux-mm, rmikey, riel,
harry, linux-cxl, driver-core, kernel-team
On Tue, 15 Sep 2026 05:53:37 -0700 Breno Leitao <leitao@debian.org> 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 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 what they agree on is the
> attribute: setup_e820() takes a descriptor as RAM only if it is writeback
> cacheable, and so does is_usable_memory() on arm64.
>
> The bitmap spans from the lowest to the highest descriptor that is
> write-back cacheable or unaccepted memory, as discussed with Kiryl. That
> leaves out the MMIO apertures, which sit high enough to stretch it far
> past the RAM it needs to describe.
>
> 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.
>
> Signed-off-by: Breno Leitao <leitao@debian.org>
> ---
> drivers/firmware/efi/libstub/efi-stub-helper.c | 103 +++++++++++++++++++++++++
> 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, 112 insertions(+)
>
> diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
> index 48f93f7758e9e9..9c66e06c972c5a 100644
> --- a/drivers/firmware/efi/libstub/efi-stub-helper.c
> +++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
> @@ -774,3 +774,106 @@ 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);
> + if (!(d->attribute & EFI_MEMORY_WB) &&
> + d->type != EFI_UNACCEPTED_MEMORY)
> + continue;
> + ram_base = min(ram_base, d->phys_addr);
Can this use the architecture's full RAM predicate? On x86,
setup_e820() maps EFI_LOADER_CODE, EFI_LOADER_DATA, both boot-services
types, and EFI_CONVENTIONAL_MEMORY as E820_TYPE_RAM without requiring
EFI_MEMORY_WB.
If a non-WB descriptor is at either end of RAM, this code omits it
from the bitmap span. efi_hwpoison_record_pfn() then rejects a
poisoned PFN there, so the next kernel can allocate the bad frame.
One possible way to preserve the x86 behavior before applying the WB rule is:
if (IS_ENABLED(CONFIG_X86) &&
(d->type == EFI_LOADER_CODE ||
d->type == EFI_LOADER_DATA ||
d->type == EFI_BOOT_SERVICES_CODE ||
d->type == EFI_BOOT_SERVICES_DATA ||
d->type == EFI_CONVENTIONAL_MEMORY))
goto include;
if (!(d->attribute & EFI_MEMORY_WB) &&
d->type != EFI_UNACCEPTED_MEMORY)
continue;
include:
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 235c9738da2d63..22a315e2814a1a 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 fd91fc15ec810b..44436869c4efe1 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 0bae0f06b6763a..3136132b9628ab 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] 27+ messages in thread
end of thread, other threads:[~2026-09-16 15:40 UTC | newest]
Thread overview: 27+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-15 12:53 [PATCH v5 0/9] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-09-15 12:53 ` [PATCH v5 1/9] mm/page_alloc: factor out the accept-and-free tail of __free_pages_core() Breno Leitao
2026-09-15 12:57 ` sashiko-bot
2026-09-15 12:53 ` [PATCH v5 2/9] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
2026-09-15 13:05 ` sashiko-bot
2026-09-15 12:53 ` [PATCH v5 3/9] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
2026-09-15 13:15 ` sashiko-bot
2026-09-15 14:00 ` Breno Leitao
2026-09-16 15:39 ` Usama Arif
2026-09-15 12:53 ` [PATCH v5 4/9] mm/memory-failure: efi: adopt the inherited poisoned-memory table Breno Leitao
2026-09-15 13:24 ` sashiko-bot
2026-09-15 12:53 ` [PATCH v5 5/9] mm/memory-failure: efi: record hardware-poisoned frames into the " Breno Leitao
2026-09-15 13:36 ` sashiko-bot
2026-09-15 14:33 ` Breno Leitao
2026-09-15 12:53 ` [PATCH v5 6/9] mm/memory-failure: efi: answer whether a range is poisoned Breno Leitao
2026-09-15 13:45 ` sashiko-bot
2026-09-16 6:44 ` David Hildenbrand (Arm)
2026-09-15 12:53 ` [PATCH v5 7/9] drivers/base/memory: count inherited poisoned frames into the block Breno Leitao
2026-09-15 13:59 ` sashiko-bot
2026-09-16 6:48 ` David Hildenbrand (Arm)
2026-09-16 9:35 ` Breno Leitao
2026-09-16 14:51 ` David Hildenbrand (Arm)
2026-09-15 12:53 ` [PATCH v5 8/9] mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame Breno Leitao
2026-09-15 14:11 ` sashiko-bot
2026-09-15 12:53 ` [PATCH v5 9/9] mm/memory-failure: keep inherited poisoned frames out of the buddy allocator Breno Leitao
2026-09-15 14:25 ` sashiko-bot
2026-09-16 8:32 ` Vlastimil Babka (SUSE)
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®