* [PATCH v6 0/7] mm/memory-failure: keep hardware-poisoned pages out of the next kexec
@ 2026-10-16 12:15 Breno Leitao
2026-10-16 12:15 ` [PATCH v6 1/7] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
` (6 more replies)
0 siblings, 7 replies; 11+ messages in thread
From: Breno Leitao @ 2026-10-16 12:15 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Miaohe Lin, Naoya Horiguchi, Oscar Salvador, Greg Kroah-Hartman,
Rafael J. Wysocki, Danilo Krummrich, Catalin Marinas, Will Deacon,
Mark Rutland, kas, kexec, Brendan Jackman, Johannes Weiner,
Zi Yan, shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, linux-cxl, driver-core,
linux-arm-kernel, rmikey, riel, Breno Leitao, harry, kernel-team
Problem:
========
When a page is hard-offlined due to an uncorrectable memory error (multi
bit ECC), memory_failure() sets PG_hwpoison, unmaps it, removes it from
the buddy allocator. This information is not carried to the next kernel
that is kexec'd. The kexec'd kernel then trips 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:
======================
To deal with the problem above, I have already fixed a few things in
this area:
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 that an earlier stub run in the same firmware session installed
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.
Errors injected by software (MADV_HWPOISON, hwpoison-inject) are not
recorded either: they are not hardware faults.
The next kernel takes the table into use from efi_config_parse_tables().
It vets the inherited header and reserves the table's own pages in
memblock.
Then, once the architecture has reserved the kernel image and the
initrd, it reads the bitmap once and reserves every recorded unit in
memblock with a new MEMBLOCK_RSRV_HWPOISON flag. On x86 and arm64 this
runs before the page tables and the memmap are allocated, so memblock
cannot hand a recorded unit to an early allocation, and it never frees
one to the buddy allocator.
memmap_init_reserved_pages() then flags the frames of those units
PG_hwpoison, initializing their struct pages through init_deferred_page()
where that initialization is deferred. 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. From there on the memmap is the only
source of poisoned frames: nothing reads the bitmap again.
The memory block poison counter is seeded from the memmap when a block is
created at boot, behind num_poisoned_pages.
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.
- The bitmap spans the descriptors that are write-back cacheable or
unaccepted memory. On x86 the stub also reports loader, boot services
and conventional memory without EFI_MEMORY_WB as RAM, so such a
descriptor at either end of RAM falls outside the bitmap and its
frames are not recorded.
- A recorded unit that overlaps a reservation made before the units are
reserved, the kernel image or a kexec segment for one, is flagged only
where that reservation leaves it free, and the overlap is logged. A
kexec segment was placed around the bad frames, so the frames it
leaves free include them. A unit that is covered entirely is skipped
with a warning.
- The reserved array cannot be resized when the table is parsed, so
memblock_reserve_hwpoison() keeps 32 slots free for the reservations
that follow and skips the units that do not fit, with a warning.
Adjacent units share a slot.
- Memory preserved across a KHO handover is not covered.
kho_preserved_memory_reserve() reserves it after the units are
reserved and marks it MEMBLOCK_RSRV_NOINIT, so
memmap_init_reserved_pages() skips it and a recorded frame there is
not flagged. The frame goes back to the allocator once the owner
releases 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.
- 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.
- On a host with SME active the frames are not recorded: the stub writes
the table in the clear, so reading it back there needs a decrypted
mapping. Parsing an inherited table is not affected. Mapping the table
decrypted is follow up work.
- The CMA patchset is being worked by Shaikh Kamaluddin
https://lore.kernel.org/all/20261010162017.62506-1-shaikhkamal2012@gmail.com/#t
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 seven patches:
1. Add the table: Defines the EFI table: GUID, Kconfig symbol, and
a bitmap with one bit per 2MiB.
2. Install it from the stub: The EFI stub allocates the table at boot,
sized from the memory map, installed empty.
3. Flag an inherited frame: Adds a helper flagging an inherited frame
PG_hwpoison, counting it in the global counter and the managed pages,
and disabling unpoison.
4. Memblock flag: Adds a memblock flag keeping poisoned ranges from
allocators and buddy, and marking frames PG_hwpoison.
5. Memory block counter: Seeds each memory block's poison counter from
the memmap at boot, only when poison exists.
6. Reserve the inherited units: Reads the inherited table once at boot
and reserves every recorded 2MiB unit in memblock.
7. Record poisoned frames: Records each hard-offlined frame's unit in
the table, so the next kexec'd kernel reserves it.
This was initially discussed at
https://lore.kernel.org/all/ajut_LDQGYCShApx@gmail.com/
A special thanks to Kiryl and David, 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 v6:
- Rework the consumer along the lines David Hildenbrand described: read the
bitmap once, at parse time, and reserve each recorded unit in memblock
with a new MEMBLOCK_RSRV_HWPOISON flag. memmap_init_reserved_pages()
flags the frames PG_hwpoison, so the memmap is the only source of
poisoned frames from then on. memblock never allocates from a unit or
frees it to buddy, which also keeps early allocations off it (David)
* This was discussed in the MM alignement meeting/thread as well.
- Drop the __free_pages_core() hook, the accept-and-free refactor under it
and the range query. Nothing queries the bitmap after parse time (David)
- Seed the memory block poison counter from the memmap, behind
num_poisoned_pages, instead of walking the bitmap (David)
- Record a hard error on a frame that was soft-offlined earlier: the hook
moves ahead of the MF_MSG_ALREADY_POISONED statistics guard (Sashiko)
- Do not count inherited frames in the per-node memory failure statistics:
this kernel handled no failure
- Reject a table whose units would run past the address space
- Do not record on a host with SME active: the table is written in the
clear and needs a decrypted mapping
- Do not record errors injected by software (MF_SW_SIMULATED): they are
not hardware faults, and a recorded bit is never cleared
- Map the table with memremap() for recording instead of using the direct
map, which mem= and a crash kernel's usable range trim on arm64
- Reserve the table's pages without adding them to memblock.memory: they
are no longer read through the direct map, and with mem= the extra
region made a CONFIG_DEBUG_VM kernel warn
- Treat an inherited frame as a hardware failure, so unpoison_memory()
cannot free it, and count it as managed so that memory offlining adds up
- Link to v5: https://patch.msgid.link/20260915-hwpoison-kho-v5-0-3bc7a57bd503@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: shakeel.butt@linux.dev
---
Breno Leitao (7):
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: add hwpoison_boot_page() to flag an inherited frame
mm/memblock: add a reserved-region flag for poisoned frames
drivers/base/memory: count the poison a block already holds when creating it
mm/memory-failure: efi: reserve the inherited poisoned-memory units
mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
arch/arm64/mm/init.c | 3 +
arch/x86/kernel/setup.c | 3 +
arch/x86/platform/efi/efi.c | 3 +
drivers/base/memory.c | 30 +++
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 | 243 +++++++++++++++++++++++++
include/linux/efi.h | 24 +++
include/linux/memblock.h | 10 +
mm/internal.h | 5 +
mm/memblock.c | 80 +++++++-
mm/memory-failure.c | 61 ++++---
17 files changed, 567 insertions(+), 24 deletions(-)
---
base-commit: a9d7ced84989ec05be09b4b8428759ef60450a0f
change-id: 20260622-hwpoison-kho-fc9db2ada8ba
Best regards,
--
Breno Leitao <leitao@debian.org>
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v6 1/7] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table
2026-10-16 12:15 [PATCH v6 0/7] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
@ 2026-10-16 12:15 ` Breno Leitao
2026-10-16 12:15 ` [PATCH v6 2/7] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
` (5 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Breno Leitao @ 2026-10-16 12:15 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Miaohe Lin, Naoya Horiguchi, Oscar Salvador, Greg Kroah-Hartman,
Rafael J. Wysocki, Danilo Krummrich, Catalin Marinas, Will Deacon,
Mark Rutland, kas, kexec, Brendan Jackman, Johannes Weiner,
Zi Yan, shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, linux-cxl, driver-core,
linux-arm-kernel, rmikey, riel, Breno Leitao, harry, kernel-team
Hardware-poisoned page frames are tracked only in the running kernel's
data structures, so a kexec loses them and the next kernel doesn't have
this information, thus, tripping into them again.
Add an EFI configuration table to carry that information across kexec.
It is a bitmap with one bit per EFI_POISON_UNIT_SIZE (2MiB) of physical
memory starting at phys_base, modeled on the LINUX_EFI_UNACCEPTED_MEMORY
table, and it rides the EFI system table to every kernel in the chain.
Basing the bitmap keeps a machine whose RAM starts high from paying for
the hole below it.
List the table in the x86 efi_tables[] too, so an SME host maps it
unencrypted like every other EFI table.
The Kconfig symbol has no prompt. There is nothing for a user to decide,
so it is on wherever it can be, and it only costs 64K per TiB of RAM on
a kernel that already has the EFI stub and MEMORY_FAILURE. It is
restricted to 64BIT because the unit arithmetic 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.
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 related [flat|nested] 11+ messages in thread
* [PATCH v6 2/7] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-10-16 12:15 [PATCH v6 0/7] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-10-16 12:15 ` [PATCH v6 1/7] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
@ 2026-10-16 12:15 ` Breno Leitao
2026-10-16 12:24 ` sashiko-bot
2026-10-16 12:15 ` [PATCH v6 3/7] mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame Breno Leitao
` (4 subsequent siblings)
6 siblings, 1 reply; 11+ messages in thread
From: Breno Leitao @ 2026-10-16 12:15 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Miaohe Lin, Naoya Horiguchi, Oscar Salvador, Greg Kroah-Hartman,
Rafael J. Wysocki, Danilo Krummrich, Catalin Marinas, Will Deacon,
Mark Rutland, kas, kexec, Brendan Jackman, Johannes Weiner,
Zi Yan, shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, linux-cxl, driver-core,
linux-arm-kernel, rmikey, riel, Breno Leitao, harry, kernel-team
A EFI config table can only be installed while boot services are still
up, so the stub has to create it; the running kernel can only flip bits
in a table that already exists.
Size the bitmap from the span the UEFI memory map describes, which
efi_get_ram_range() walks since the stub has no max_pfn. Bit 0 covers
the bottom of that span, recorded in phys_base, so a machine whose RAM
starts high does not pay for the hole below it. Memory the firmware
hot-adds later sits outside the span and is not carried across a kexec.
One table has to serve every architecture, so 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. arm64 makes the same cut in is_usable_memory(); x86 sorts
descriptors by type instead, so a RAM descriptor without the attribute
would sit outside the span and not be carried across a kexec.
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.
Reuse a table that an earlier stub run in the same firmware session already
installed.
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..21569fe82cb514 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;
+
+ /* An earlier stub run in this firmware session may have installed it. */
+ 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 related [flat|nested] 11+ messages in thread
* [PATCH v6 3/7] mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame
2026-10-16 12:15 [PATCH v6 0/7] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-10-16 12:15 ` [PATCH v6 1/7] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
2026-10-16 12:15 ` [PATCH v6 2/7] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
@ 2026-10-16 12:15 ` Breno Leitao
2026-10-16 12:15 ` [PATCH v6 4/7] mm/memblock: add a reserved-region flag for poisoned frames Breno Leitao
` (3 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Breno Leitao @ 2026-10-16 12:15 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Miaohe Lin, Naoya Horiguchi, Oscar Salvador, Greg Kroah-Hartman,
Rafael J. Wysocki, Danilo Krummrich, Catalin Marinas, Will Deacon,
Mark Rutland, kas, kexec, Brendan Jackman, Johannes Weiner,
Zi Yan, shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, linux-cxl, driver-core,
linux-arm-kernel, rmikey, riel, Breno Leitao, harry, kernel-team
A kernel that kexecs inherits the frames its predecessor found
hardware-poisoned. It has to carry them as PG_hwpoison in its own memmap,
so that everything that already understands the flag covers them,
including the kexec segment placement check.
Add a helper that sets the flag and counts the frame in
num_poisoned_pages. It runs while the memmap is initialised, before
memory_dev_init() has run. memblk_nr_poison_inc() divides by
sections_per_block, which memory_dev_init() sets, so the helper
increments num_poisoned_pages itself instead of calling
num_poisoned_pages_inc().
An inherited frame is a hardware failure like any other, so the helper
sets hw_memory_failure. Without it, unpoison_memory() accepts the frame,
clears the flag and drops the last reference, which frees a frame that
is still bad.
The helper also counts the frame as managed, as a frame that fails at
run time stays counted. Memory offlining subtracts every frame of the
block that is not PageOffline, so a frame that was never counted leaves
the managed page count too low after an offline.
The per-node memory failure statistics are not updated. This kernel
handled no failure, it only inherited one.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
mm/internal.h | 5 +++++
mm/memory-failure.c | 8 ++++++++
2 files changed, 13 insertions(+)
diff --git a/mm/internal.h b/mm/internal.h
index da14c56fb24e11..4bd3bbce86e0ad 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1169,6 +1169,7 @@ void SetPageHWPoisonTakenOff(struct page *page);
void ClearPageHWPoisonTakenOff(struct page *page);
bool take_page_off_buddy(struct page *page);
bool put_page_back_buddy(struct page *page);
+void __init hwpoison_boot_page(struct page *page);
struct task_struct *task_early_kill(struct task_struct *tsk, int force_early);
void add_to_kill_ksm(struct task_struct *tsk, const struct page *p,
struct vm_area_struct *vma, struct list_head *to_kill,
@@ -1181,6 +1182,10 @@ static inline int unmap_poisoned_folio(struct folio *folio, unsigned long pfn, b
{
return -EBUSY;
}
+
+static inline void hwpoison_boot_page(struct page *page)
+{
+}
#endif
extern unsigned long __must_check vm_mmap_pgoff(struct file *, unsigned long,
diff --git a/mm/memory-failure.c b/mm/memory-failure.c
index a2ca8df501caee..d0d6b184ee469f 100644
--- a/mm/memory-failure.c
+++ b/mm/memory-failure.c
@@ -96,6 +96,14 @@ void num_poisoned_pages_sub(unsigned long pfn, long i)
memblk_nr_poison_sub(pfn, i);
}
+void __init hwpoison_boot_page(struct page *page)
+{
+ SetPageHWPoison(page);
+ atomic_long_inc(&num_poisoned_pages);
+ hw_memory_failure = true;
+ adjust_managed_page_count(page, 1);
+}
+
/*
* Return the first or the last hardware-poisoned online page in [start,
* start + size), or PHYS_ADDR_MAX if the range is clean.
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v6 4/7] mm/memblock: add a reserved-region flag for poisoned frames
2026-10-16 12:15 [PATCH v6 0/7] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (2 preceding siblings ...)
2026-10-16 12:15 ` [PATCH v6 3/7] mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame Breno Leitao
@ 2026-10-16 12:15 ` Breno Leitao
2026-10-16 12:26 ` sashiko-bot
2026-10-16 12:15 ` [PATCH v6 5/7] drivers/base/memory: count the poison a block already holds when creating it Breno Leitao
` (2 subsequent siblings)
6 siblings, 1 reply; 11+ messages in thread
From: Breno Leitao @ 2026-10-16 12:15 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Miaohe Lin, Naoya Horiguchi, Oscar Salvador, Greg Kroah-Hartman,
Rafael J. Wysocki, Danilo Krummrich, Catalin Marinas, Will Deacon,
Mark Rutland, kas, kexec, Brendan Jackman, Johannes Weiner,
Zi Yan, shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, linux-cxl, driver-core,
linux-arm-kernel, rmikey, riel, Breno Leitao, harry, kernel-team
memblock allocates from and frees to buddy whatever is not reserved, and
knows nothing about frames an earlier kernel found hardware-poisoned.
Such a frame can back the memmap, a page table or a percpu area before
any hwpoison code runs, and otherwise reaches the buddy allocator like
any other free frame.
Add MEMBLOCK_RSRV_HWPOISON for a reserved range holding those frames.
memblock neither allocates from a flagged range nor frees it to buddy.
memmap_init_reserved_pages() initializes its struct pages like any other
reserved range, including above first_deferred_pfn, but flags the RAM
frames PG_hwpoison instead of PG_reserved. That is the state of a frame
poisoned by this kernel. Holes in the range stay reserved.
PG_reserved is left clear on purpose. page_is_unmovable() treats a
reserved page as unmovable but skips a counted PG_hwpoison one, so a
memory block holding an inherited frame can still be offlined.
memblock_reserve_hwpoison() is the entry point. It is best effort. Until
the reserved array can be resized it keeps spare slots for the
reservations that must not fail. It flags only what no earlier
reservation covers, a kexec segment for one: that was placed around the
bad frames, and memblock_add_range() would warn about the overlap. A page
shared with such a reservation stays with it, so no page is flagged
twice.
It returns 0 only when every free part of the range is reserved. If any
part cannot be reserved it returns an error, even when another part was.
Name the flag in the debugfs flag table. MEMBLOCK_RSRV_HUGETLB gets a
name too: it has none, and would be left as a NULL slot below the new
flag, which memblock_debug_show() passes on to seq_puts().
Signed-off-by: Breno Leitao <leitao@debian.org>
---
include/linux/memblock.h | 10 ++++++
mm/memblock.c | 80 +++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 89 insertions(+), 1 deletion(-)
diff --git a/include/linux/memblock.h b/include/linux/memblock.h
index aa845f488327f2..86755be249666d 100644
--- a/include/linux/memblock.h
+++ b/include/linux/memblock.h
@@ -52,6 +52,8 @@ extern unsigned long long max_possible_pfn;
* kernel that we know is good to use. It is the only memory that
* allocations may happen from in this phase.
* @MEMBLOCK_RSRV_HUGETLB: memory is reserved for hugetlb pages
+ * @MEMBLOCK_RSRV_HWPOISON: reserved memory region holding frames an earlier
+ * kernel found to be hardware-poisoned.
*/
enum memblock_flags {
MEMBLOCK_NONE = 0x0, /* No special request */
@@ -63,6 +65,7 @@ enum memblock_flags {
MEMBLOCK_RSRV_KERN = 0x20, /* memory reserved for kernel use */
MEMBLOCK_KHO_SCRATCH = 0x40, /* scratch memory for kexec handover */
MEMBLOCK_RSRV_HUGETLB = 0x80, /* memory reserved for hugetlb pages */
+ MEMBLOCK_RSRV_HWPOISON = 0x100, /* poisoned by an earlier kernel */
};
/**
@@ -142,6 +145,8 @@ static __always_inline int memblock_reserve_kern(phys_addr_t base, phys_addr_t s
return __memblock_reserve(base, size, NUMA_NO_NODE, MEMBLOCK_RSRV_KERN);
}
+int memblock_reserve_hwpoison(phys_addr_t base, phys_addr_t size);
+
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
int memblock_physmem_add(phys_addr_t base, phys_addr_t size);
#endif
@@ -295,6 +300,11 @@ static inline bool memblock_is_reserved_noinit(struct memblock_region *m)
return m->flags & MEMBLOCK_RSRV_NOINIT;
}
+static inline bool memblock_is_reserved_hwpoison(struct memblock_region *m)
+{
+ return m->flags & MEMBLOCK_RSRV_HWPOISON;
+}
+
static inline bool memblock_is_driver_managed(struct memblock_region *m)
{
return m->flags & MEMBLOCK_DRIVER_MANAGED;
diff --git a/mm/memblock.c b/mm/memblock.c
index 31961ed628b0ad..d20d99972a4db0 100644
--- a/mm/memblock.c
+++ b/mm/memblock.c
@@ -36,6 +36,9 @@
# define INIT_MEMBLOCK_RESERVED_REGIONS INIT_MEMBLOCK_REGIONS
#endif
+/* Reserved slots memblock_reserve_hwpoison() leaves free until a resize. */
+#define MEMBLOCK_HWPOISON_SPARE 32
+
#ifndef INIT_MEMBLOCK_MEMORY_REGIONS
#define INIT_MEMBLOCK_MEMORY_REGIONS INIT_MEMBLOCK_REGIONS
#endif
@@ -1015,6 +1018,55 @@ int __init_memblock __memblock_reserve(phys_addr_t base, phys_addr_t size,
return memblock_add_range(&memblock.reserved, base, size, nid, flags);
}
+int __init memblock_reserve_hwpoison(phys_addr_t base, phys_addr_t size)
+{
+ phys_addr_t end = base + size;
+ bool reserved = false;
+ int err = 0;
+
+ while (base < end) {
+ phys_addr_t next = end;
+ struct memblock_region *r;
+ bool covered = false;
+
+ for_each_reserved_mem_region(r) {
+ if (r->base + r->size <= base)
+ continue;
+ if (r->base >= end)
+ break;
+ if (PAGE_ALIGN_DOWN(r->base) <= base) {
+ base = min(end, PAGE_ALIGN(r->base + r->size));
+ covered = true;
+ } else {
+ next = PAGE_ALIGN_DOWN(r->base);
+ }
+ break;
+ }
+ if (covered)
+ continue;
+
+ if (!memblock_can_resize &&
+ memblock.reserved.cnt + MEMBLOCK_HWPOISON_SPARE >=
+ memblock.reserved.max) {
+ err = -ENOSPC;
+ break;
+ }
+
+ err = __memblock_reserve(base, next - base, NUMA_NO_NODE,
+ MEMBLOCK_RSRV_HWPOISON);
+ if (err)
+ break;
+
+ reserved = true;
+ base = next;
+ }
+
+ if (err)
+ return err;
+
+ return reserved ? 0 : -EBUSY;
+}
+
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
int __init_memblock memblock_physmem_add(phys_addr_t base, phys_addr_t size)
{
@@ -2394,6 +2446,27 @@ static void __init memmap_init_reserved_range(phys_addr_t start,
}
}
+/*
+ * Frames an earlier kernel found poisoned end up as a frame poisoned by this
+ * kernel would: PG_hwpoison, not PG_reserved. Holes in the range stay reserved.
+ */
+static void __init memmap_init_hwpoison_range(phys_addr_t start,
+ phys_addr_t end, int nid)
+{
+ unsigned long pfn;
+
+ for_each_valid_pfn(pfn, PFN_DOWN(start), PFN_UP(end)) {
+ struct page *page = pfn_to_page(pfn);
+
+ init_deferred_page(pfn, nid);
+
+ if (memblock_is_map_memory(PFN_PHYS(pfn)))
+ hwpoison_boot_page(page);
+ else
+ __SetPageReserved(page);
+ }
+}
+
static void __init memmap_init_reserved_pages(void)
{
struct memblock_region *region;
@@ -2438,7 +2511,10 @@ static void __init memmap_init_reserved_pages(void)
if (!numa_valid_node(nid))
nid = early_pfn_to_nid(PFN_DOWN(start));
- memmap_init_reserved_range(start, end, nid);
+ if (memblock_is_reserved_hwpoison(region))
+ memmap_init_hwpoison_range(start, end, nid);
+ else
+ memmap_init_reserved_range(start, end, nid);
}
}
}
@@ -2874,6 +2950,8 @@ static const char * const flagname[] = {
[ilog2(MEMBLOCK_RSRV_NOINIT)] = "RSV_NIT",
[ilog2(MEMBLOCK_RSRV_KERN)] = "RSV_KERN",
[ilog2(MEMBLOCK_KHO_SCRATCH)] = "KHO_SCRATCH",
+ [ilog2(MEMBLOCK_RSRV_HUGETLB)] = "RSV_HUGETLB",
+ [ilog2(MEMBLOCK_RSRV_HWPOISON)] = "RSV_HWPOISON",
};
static int memblock_debug_show(struct seq_file *m, void *private)
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v6 5/7] drivers/base/memory: count the poison a block already holds when creating it
2026-10-16 12:15 [PATCH v6 0/7] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (3 preceding siblings ...)
2026-10-16 12:15 ` [PATCH v6 4/7] mm/memblock: add a reserved-region flag for poisoned frames Breno Leitao
@ 2026-10-16 12:15 ` Breno Leitao
2026-10-16 12:15 ` [PATCH v6 6/7] mm/memory-failure: efi: reserve the inherited poisoned-memory units Breno Leitao
2026-10-16 12:15 ` [PATCH v6 7/7] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table Breno Leitao
6 siblings, 0 replies; 11+ messages in thread
From: Breno Leitao @ 2026-10-16 12:15 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Miaohe Lin, Naoya Horiguchi, Oscar Salvador, Greg Kroah-Hartman,
Rafael J. Wysocki, Danilo Krummrich, Catalin Marinas, Will Deacon,
Mark Rutland, kas, kexec, Brendan Jackman, Johannes Weiner,
Zi Yan, shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, linux-cxl, driver-core,
linux-arm-kernel, rmikey, riel, Breno Leitao, harry, kernel-team
A frame flagged poisoned before memory_dev_init() runs cannot be counted
on its memory block. memblk_nr_poison_inc() divides by sections_per_block,
which memory_dev_init() sets, and the block does not exist yet. The block
is then created reading zero, however many of its frames are poisoned.
Until now nothing could poison a frame that early. It becomes possible
once a kernel flags the frames it inherits from the one that kexeced into
it, as it initializes its memmap.
memory_block_online() refuses a block that holds a poisoned frame, and a
counter stuck at zero lets one through.
Count what a block holds when it is created at boot. The walk sits behind
num_poisoned_pages, so a machine with no poison does not pay for it.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
drivers/base/memory.c | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/drivers/base/memory.c b/drivers/base/memory.c
index 5eead3346f1e32..77481f375c2206 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,28 @@ static unsigned long memblk_nr_poison(struct memory_block *mem)
{
return atomic_long_read(&mem->nr_hwpoison);
}
+
+/*
+ * Frames poisoned before the block existed were never counted on it. Count
+ * them now, or the first unpoison takes the counter below zero.
+ */
+static void memblk_nr_poison_init(struct memory_block *mem)
+{
+ unsigned long pfn = section_nr_to_pfn(mem->start_section_nr);
+ unsigned long end = pfn + PAGES_PER_SECTION * sections_per_block;
+ unsigned long nr = 0;
+
+ /* A hotplugged block is created before its pages are online. */
+ if (mem->state != MEM_ONLINE || !atomic_long_read(&num_poisoned_pages))
+ return;
+
+ for (; pfn < end; pfn++) {
+ struct page *page = pfn_to_online_page(pfn);
+
+ if (page && PageHWPoison(page))
+ nr++;
+ }
+
+ atomic_long_set(&mem->nr_hwpoison, nr);
+}
#endif
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v6 6/7] mm/memory-failure: efi: reserve the inherited poisoned-memory units
2026-10-16 12:15 [PATCH v6 0/7] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (4 preceding siblings ...)
2026-10-16 12:15 ` [PATCH v6 5/7] drivers/base/memory: count the poison a block already holds when creating it Breno Leitao
@ 2026-10-16 12:15 ` Breno Leitao
2026-10-16 12:24 ` sashiko-bot
2026-10-16 12:15 ` [PATCH v6 7/7] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table Breno Leitao
6 siblings, 1 reply; 11+ messages in thread
From: Breno Leitao @ 2026-10-16 12:15 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Miaohe Lin, Naoya Horiguchi, Oscar Salvador, Greg Kroah-Hartman,
Rafael J. Wysocki, Danilo Krummrich, Catalin Marinas, Will Deacon,
Mark Rutland, kas, kexec, Brendan Jackman, Johannes Weiner,
Zi Yan, shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, linux-cxl, driver-core,
linux-arm-kernel, rmikey, riel, Breno Leitao, harry, 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, a whole-word bitmap size, a bit count and a footprint that can
be taken without wrapping, a power-of-two unit the base is aligned to,
and units that stay inside the address space. A table that fails any of
those is dropped rather than trusted.
Reserve the table's own pages so that nothing allocates from them, but
do not add them to memblock.memory. They are EFI ACPI reclaim memory,
which x86 leaves out of it.
With mem= trimming RAM, adding them would leave an island of memory far
above the rest. A CONFIG_DEBUG_VM kernel then warns while it initialises
the reserved ranges that share the island's memory section.
Then read the bitmap once and reserve every unit whose bit is set with
memblock_reserve_hwpoison(). That has to wait until the kernel image and
the initrd are reserved, or their frames would be flagged poisoned. The
architecture calls efi_poisoned_memory_reserve_units() for it: right after
efi_init() on x86, which reserves them earlier, and at the end of
arm64_memblock_init() on arm64, which runs after efi_init(). Both are
before the page tables and the memmap are allocated, so memblock never
hands those units out, and memmap_init_reserved_pages() flags their
frames. Nothing reads the bitmap after this.
The reserved array cannot be resized yet, so a unit that does not fit is
skipped. One that overlaps an earlier reservation, such as the kernel
image or a kexec segment, is flagged only where that reservation leaves
it free, and skipped if nothing is left.
Log the number of units skipped and the number that overlap, as a
recorded frame inside the overlap stays in use. An overlap is expected
when a kexec placed its segments around the bad frames, so it is logged
at info level.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
arch/arm64/mm/init.c | 3 +
arch/x86/kernel/setup.c | 3 +
drivers/firmware/efi/Makefile | 1 +
drivers/firmware/efi/efi.c | 2 +
drivers/firmware/efi/poison.c | 174 ++++++++++++++++++++++++++++++++++++++++++
include/linux/efi.h | 8 ++
6 files changed, 191 insertions(+)
diff --git a/arch/arm64/mm/init.c b/arch/arm64/mm/init.c
index fbf215ecc7d061..6f74020a92be1d 100644
--- a/arch/arm64/mm/init.c
+++ b/arch/arm64/mm/init.c
@@ -298,6 +298,9 @@ void __init arm64_memblock_init(void)
}
early_init_fdt_scan_reserved_mem();
+
+ /* Poisoned units go last: the reservations above keep their frames. */
+ efi_poisoned_memory_reserve_units();
}
void __init bootmem_init(void)
diff --git a/arch/x86/kernel/setup.c b/arch/x86/kernel/setup.c
index cda6adb9f69c42..2a84b5c7db7063 100644
--- a/arch/x86/kernel/setup.c
+++ b/arch/x86/kernel/setup.c
@@ -1007,6 +1007,9 @@ void __init setup_arch(char **cmdline_p)
if (efi_enabled(EFI_BOOT))
efi_init();
+ /* Poisoned units go after the image, the initrd and setup_data. */
+ efi_poisoned_memory_reserve_units();
+
reserve_ibft_region();
x86_init.resources.dmi_setup();
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..fa84df55aeb469
--- /dev/null
+++ b/drivers/firmware/efi/poison.c
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Kernel side of the LINUX_EFI_POISONED_MEMORY table: one bit per
+ * EFI_POISON_UNIT_SIZE, set as frames go bad, and turned into memblock
+ * reservations 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)
+{
+ const u64 max_end = PHYS_ADDR_MAX - PAGE_SIZE;
+ u64 nbits, span, top, end, len;
+
+ /* 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. */
+ len = sizeof(*pm) + pm->size;
+ if (check_add_overflow(efi.poisoned_memory, len, &end) ||
+ end > max_end)
+ return false;
+
+ if (pm->unit_size < PAGE_SIZE || !is_power_of_2(pm->unit_size))
+ return false;
+
+ /* Every unit has to lie inside the address space. */
+ if (check_mul_overflow(nbits, (u64)pm->unit_size, &span) ||
+ check_add_overflow(pm->phys_base, span, &top))
+ return false;
+
+ return IS_ALIGNED(pm->phys_base, pm->unit_size);
+}
+
+/*
+ * The table came from an earlier kernel, check version and geometry,
+ * otherwise bail out.
+ */
+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;
+}
+
+/*
+ * Reserve every unit the table records, so that memblock allocates nothing
+ * from it and memmap_init_reserved_pages() flags its frames. The bitmap is
+ * read through early_memremap(): the table is not in the direct map yet.
+ */
+static void __init
+efi_poison_reserve_units(phys_addr_t table,
+ const struct linux_efi_poisoned_memory *hdr)
+{
+ const phys_addr_t bitmap = table +
+ offsetof(struct linux_efi_poisoned_memory, bitmap);
+ const u32 unit_size = hdr->unit_size;
+ unsigned int nr_units = 0, nr_overlap = 0, nr_skipped = 0;
+ u64 off;
+
+ for (off = 0; off < hdr->size; off += PAGE_SIZE) {
+ u64 chunk = min_t(u64, PAGE_SIZE, hdr->size - off);
+ u64 nbits = chunk * BITS_PER_BYTE;
+ unsigned long bit, *map;
+
+ map = early_memremap(bitmap + off, chunk);
+ if (!map) {
+ pr_warn("Could not map poisoned-memory bitmap\n");
+ break;
+ }
+
+ for_each_set_bit(bit, map, nbits) {
+ u64 unit = off * BITS_PER_BYTE + bit;
+ phys_addr_t base = hdr->phys_base + unit * unit_size;
+ bool overlap;
+
+ overlap = memblock_is_region_reserved(base, unit_size);
+ if (memblock_reserve_hwpoison(base, unit_size)) {
+ nr_skipped++;
+ } else {
+ nr_units++;
+ if (overlap)
+ nr_overlap++;
+ }
+ }
+ early_memunmap(map, chunk);
+ }
+
+ if (nr_units)
+ pr_info("reserved %u poisoned unit(s) of %uK inherited across kexec\n",
+ nr_units, unit_size >> 10);
+ if (nr_overlap)
+ pr_info("%u poisoned unit(s) overlap an earlier reservation, only their free frames are flagged\n",
+ nr_overlap);
+ if (nr_skipped)
+ pr_warn("could not reserve %u poisoned unit(s)\n", nr_skipped);
+}
+
+/* The vetted header of the inherited table. */
+static struct linux_efi_poisoned_memory efi_poison_hdr __initdata;
+
+/*
+ * Vet the inherited table and reserve its pages. Called from
+ * efi_config_parse_tables(); the units it records wait for the architecture,
+ * see efi_poisoned_memory_reserve_units().
+ */
+void __init efi_poisoned_memory_reserve(void)
+{
+ struct linux_efi_poisoned_memory *pm;
+ phys_addr_t table = efi.poisoned_memory;
+ phys_addr_t start, end;
+ u64 len;
+
+ if (table == EFI_INVALID_TABLE_ADDR)
+ return;
+
+ pm = early_memremap(table, 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;
+ }
+
+ efi_poison_hdr = *pm;
+ early_memunmap(pm, sizeof(*pm));
+
+ len = sizeof(efi_poison_hdr) + efi_poison_hdr.size;
+ start = PAGE_ALIGN_DOWN(table);
+ end = PAGE_ALIGN(table + len);
+ memblock_reserve(start, end - start);
+}
+
+/*
+ * Reserve the units the table records. The architecture calls this once the
+ * kernel image, the initrd and its other early reservations are in memblock,
+ * so that a unit sharing frames with them leaves those frames alone.
+ */
+void __init efi_poisoned_memory_reserve_units(void)
+{
+ if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
+ return;
+
+ efi_poison_reserve_units(efi.poisoned_memory, &efi_poison_hdr);
+}
diff --git a/include/linux/efi.h b/include/linux/efi.h
index efaf63f9a54edd..396e0d61112205 100644
--- a/include/linux/efi.h
+++ b/include/linux/efi.h
@@ -1286,6 +1286,14 @@ struct linux_efi_poisoned_memory {
#define EFI_POISON_UNIT_SIZE SZ_2M
+#ifdef CONFIG_EFI_POISONED_MEMORY
+void __init efi_poisoned_memory_reserve(void);
+void __init efi_poisoned_memory_reserve_units(void);
+#else
+static inline void efi_poisoned_memory_reserve(void) { }
+static inline void efi_poisoned_memory_reserve_units(void) { }
+#endif
+
void __init efi_arch_mem_reserve(phys_addr_t addr, u64 size);
/*
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v6 7/7] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
2026-10-16 12:15 [PATCH v6 0/7] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
` (5 preceding siblings ...)
2026-10-16 12:15 ` [PATCH v6 6/7] mm/memory-failure: efi: reserve the inherited poisoned-memory units Breno Leitao
@ 2026-10-16 12:15 ` Breno Leitao
6 siblings, 0 replies; 11+ messages in thread
From: Breno Leitao @ 2026-10-16 12:15 UTC (permalink / raw)
To: Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, Dave Hansen, x86, H. Peter Anvin, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Miaohe Lin, Naoya Horiguchi, Oscar Salvador, Greg Kroah-Hartman,
Rafael J. Wysocki, Danilo Krummrich, Catalin Marinas, Will Deacon,
Mark Rutland, kas, kexec, Brendan Jackman, Johannes Weiner,
Zi Yan, shakeel.butt
Cc: linux-efi, linux-kernel, linux-mm, linux-cxl, driver-core,
linux-arm-kernel, rmikey, riel, Breno Leitao, harry, kernel-team
When memory_failure() reports the outcome of a hard offline, set the
frame's bit in the LINUX_EFI_POISONED_MEMORY bitmap, so that the next
kernel reserves its unit.
Hook action_result() ahead of the statistics guard. A hard error on a
frame that was soft-offlined earlier comes in as MF_MSG_ALREADY_POISONED,
which skips the statistics, but it is a real failure and has to be
recorded. MF_MSG_PFN_MAP is left out, as it is not System RAM.
A bit is only ever set, never cleared (for now). Several frames share
a bit, and an unpoison cannot tell whether the unit as a whole is good
again. That is a known limitation.
The table is mapped with memremap() from an early_initcall rather than
through the direct map. On arm64, mem= and the usable memory range of a
crash kernel trim memblock, which drops the table's pages from the direct
map, and the first recorded error then faults.
Recording is skipped on a host with SME active. The stub writes the table
in the clear, so reading it back there needs a decrypted mapping. That is
left for a follow up.
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 | 69 +++++++++++++++++++++++++++++++++++++++++++
include/linux/efi.h | 2 ++
mm/memory-failure.c | 53 ++++++++++++++++++---------------
3 files changed, 101 insertions(+), 23 deletions(-)
diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
index fa84df55aeb469..4bb56865551224 100644
--- a/drivers/firmware/efi/poison.c
+++ b/drivers/firmware/efi/poison.c
@@ -11,12 +11,14 @@
#define pr_fmt(fmt) "efi: " fmt
#include <linux/bitmap.h>
+#include <linux/cc_platform.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>
+#include <linux/uaccess.h>
static bool __init
efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)
@@ -172,3 +174,70 @@ void __init efi_poisoned_memory_reserve_units(void)
efi_poison_reserve_units(efi.poisoned_memory, &efi_poison_hdr);
}
+
+static struct linux_efi_poisoned_memory *efi_poison_table __ro_after_init;
+
+static bool __init efi_poison_mapped(const void *map, size_t len)
+{
+ const void *end = map + len;
+ const void *p;
+ u64 val;
+
+ for (p = map; p < end; p = PTR_ALIGN_DOWN(p, PAGE_SIZE) + PAGE_SIZE)
+ if (copy_from_kernel_nofault(&val, p, sizeof(val)))
+ return false;
+
+ return true;
+}
+
+static int __init efi_poisoned_memory_map(void)
+{
+ struct linux_efi_poisoned_memory *hdr, *table = NULL;
+ size_t len = 0;
+
+ if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
+ return 0;
+
+ /*
+ * The stub writes the table in the clear, so an SME host needs a
+ * decrypted mapping. Skip such hosts for now.
+ */
+ if (cc_platform_has(CC_ATTR_HOST_MEM_ENCRYPT)) {
+ pr_warn("SME is active, poisoned frames are not recorded\n");
+ return 0;
+ }
+
+ hdr = memremap(efi.poisoned_memory, sizeof(*hdr), MEMREMAP_WB);
+ if (hdr) {
+ if (efi_poison_mapped(hdr, sizeof(*hdr)))
+ len = sizeof(*hdr) + hdr->size;
+ memunmap(hdr);
+ if (len)
+ table = memremap(efi.poisoned_memory, len, MEMREMAP_WB);
+ }
+ if (table && !efi_poison_mapped(table, len)) {
+ memunmap(table);
+ table = NULL;
+ }
+ if (!table)
+ pr_warn("Could not map poisoned-memory table, poisoned frames are not recorded\n");
+ efi_poison_table = table;
+
+ return 0;
+}
+early_initcall(efi_poisoned_memory_map);
+
+void efi_hwpoison_record_pfn(unsigned long pfn)
+{
+ struct linux_efi_poisoned_memory *pm = efi_poison_table;
+ phys_addr_t addr = PFN_PHYS(pfn);
+ u64 unit;
+
+ if (!pm || addr < pm->phys_base)
+ return;
+
+ /* unit is the bit we are going to set in the bitmap */
+ 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 396e0d61112205..259e64467ed1ba 100644
--- a/include/linux/efi.h
+++ b/include/linux/efi.h
@@ -1289,9 +1289,11 @@ struct linux_efi_poisoned_memory {
#ifdef CONFIG_EFI_POISONED_MEMORY
void __init efi_poisoned_memory_reserve(void);
void __init efi_poisoned_memory_reserve_units(void);
+void efi_hwpoison_record_pfn(unsigned long pfn);
#else
static inline void efi_poisoned_memory_reserve(void) { }
static inline void efi_poisoned_memory_reserve_units(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 d0d6b184ee469f..c95b0be04a0cf8 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>
@@ -1327,10 +1328,16 @@ static bool panic_on_unrecoverable_mf(enum mf_action_page_type type,
* setting PG_dirty outside page lock. See also comment above set_page_dirty().
*/
static int action_result(unsigned long pfn, enum mf_action_page_type type,
- enum mf_result result)
+ enum mf_result result, int flags)
{
trace_memory_failure_event(pfn, type, result);
+ /*
+ * An error injected by software is not a hardware fault, leave it out.
+ */
+ if (type != MF_MSG_PFN_MAP && !(flags & MF_SW_SIMULATED))
+ efi_hwpoison_record_pfn(pfn);
+
if (type != MF_MSG_ALREADY_POISONED && type != MF_MSG_PFN_MAP) {
num_poisoned_pages_inc(pfn);
update_per_node_mf_stats(pfn, result);
@@ -1346,7 +1353,7 @@ static int action_result(unsigned long pfn, enum mf_action_page_type type,
}
static int page_action(struct page_state *ps, struct page *p,
- unsigned long pfn)
+ unsigned long pfn, int flags)
{
int result;
@@ -1358,7 +1365,7 @@ static int page_action(struct page_state *ps, struct page *p,
* Could adjust zone counters here to correct for the missing page.
*/
- return action_result(pfn, ps->type, result);
+ return action_result(pfn, ps->type, result, flags);
}
static inline bool PageHWPoisonTakenOff(struct page *page)
@@ -1731,7 +1738,7 @@ static bool hwpoison_user_mappings(struct folio *folio, struct page *p,
}
static int identify_page_state(unsigned long pfn, struct page *p,
- unsigned long page_flags)
+ unsigned long page_flags, int flags)
{
struct page_state *ps;
@@ -1750,7 +1757,7 @@ static int identify_page_state(unsigned long pfn, struct page *p,
for (ps = error_states;; ps++)
if ((page_flags & ps->mask) == ps->res)
break;
- return page_action(ps, p, pfn);
+ return page_action(ps, p, pfn, flags);
}
/*
@@ -2144,16 +2151,16 @@ static int try_memory_failure_hugetlb(unsigned long pfn, int flags)
flags |= MF_NO_RETRY;
goto retry;
}
- return action_result(pfn, MF_MSG_GET_HWPOISON, MF_IGNORED);
+ return action_result(pfn, MF_MSG_GET_HWPOISON, MF_IGNORED, flags);
case MF_HUGETLB_FOLIO_PRE_POISONED:
case MF_HUGETLB_PAGE_PRE_POISONED:
rv = -EHWPOISON;
if (flags & MF_ACTION_REQUIRED)
rv = kill_accessing_process(current, pfn, flags);
if (res == MF_HUGETLB_PAGE_PRE_POISONED)
- action_result(pfn, MF_MSG_ALREADY_POISONED, MF_FAILED);
+ action_result(pfn, MF_MSG_ALREADY_POISONED, MF_FAILED, flags);
else
- action_result(pfn, MF_MSG_HUGE, MF_FAILED);
+ action_result(pfn, MF_MSG_HUGE, MF_FAILED, flags);
return rv;
default:
WARN_ON((res != MF_HUGETLB_FREED) && (res != MF_HUGETLB_IN_USED));
@@ -2185,17 +2192,17 @@ static int try_memory_failure_hugetlb(unsigned long pfn, int flags)
} else {
res = MF_FAILED;
}
- return action_result(pfn, MF_MSG_FREE_HUGE, res);
+ return action_result(pfn, MF_MSG_FREE_HUGE, res, flags);
}
page_flags = folio->flags.f;
if (!hwpoison_user_mappings(folio, p, pfn, flags)) {
folio_unlock(folio);
- return action_result(pfn, MF_MSG_UNMAP_FAILED, MF_FAILED);
+ return action_result(pfn, MF_MSG_UNMAP_FAILED, MF_FAILED, flags);
}
- return identify_page_state(pfn, p, page_flags);
+ return identify_page_state(pfn, p, page_flags, flags);
}
#else
@@ -2247,7 +2254,7 @@ static int memory_failure_dev_pagemap(unsigned long pfn, int flags,
/* drop pgmap ref acquired in caller */
put_dev_pagemap(pgmap);
if (rc != -EOPNOTSUPP)
- action_result(pfn, MF_MSG_DAX, rc ? MF_FAILED : MF_RECOVERED);
+ action_result(pfn, MF_MSG_DAX, rc ? MF_FAILED : MF_RECOVERED, flags);
return rc;
}
@@ -2390,7 +2397,7 @@ static int memory_failure_pfn(unsigned long pfn, int flags)
}
if (!mf_handled)
- return action_result(pfn, MF_MSG_PFN_MAP, MF_IGNORED);
+ return action_result(pfn, MF_MSG_PFN_MAP, MF_IGNORED, flags);
}
/*
@@ -2403,7 +2410,7 @@ static int memory_failure_pfn(unsigned long pfn, int flags)
kill_procs(&tokill, true, pfn, flags);
- return action_result(pfn, MF_MSG_PFN_MAP, MF_RECOVERED);
+ return action_result(pfn, MF_MSG_PFN_MAP, MF_RECOVERED, flags);
}
/**
@@ -2490,7 +2497,7 @@ int memory_failure(unsigned long pfn, int flags)
res = kill_accessing_process(current, pfn, flags);
if (flags & MF_COUNT_INCREASED)
put_page(p);
- action_result(pfn, MF_MSG_ALREADY_POISONED, MF_FAILED);
+ action_result(pfn, MF_MSG_ALREADY_POISONED, MF_FAILED, flags);
goto unlock_mutex;
}
@@ -2521,9 +2528,9 @@ int memory_failure(unsigned long pfn, int flags)
}
res = MF_FAILED;
}
- res = action_result(pfn, MF_MSG_BUDDY, res);
+ res = action_result(pfn, MF_MSG_BUDDY, res, flags);
} else {
- res = action_result(pfn, MF_MSG_KERNEL_HIGH_ORDER, MF_IGNORED);
+ res = action_result(pfn, MF_MSG_KERNEL_HIGH_ORDER, MF_IGNORED, flags);
}
goto unlock_mutex;
case 1:
@@ -2535,11 +2542,11 @@ int memory_failure(unsigned long pfn, int flags)
* slab, page tables, large-kmalloc).
* No recovery possible.
*/
- res = action_result(pfn, MF_MSG_KERNEL, MF_IGNORED);
+ res = action_result(pfn, MF_MSG_KERNEL, MF_IGNORED, flags);
goto unlock_mutex;
default:
/* Transient lifecycle race with the page allocator. */
- res = action_result(pfn, MF_MSG_GET_HWPOISON, MF_IGNORED);
+ res = action_result(pfn, MF_MSG_GET_HWPOISON, MF_IGNORED, flags);
goto unlock_mutex;
}
@@ -2587,7 +2594,7 @@ int memory_failure(unsigned long pfn, int flags)
res = -EHWPOISON;
kill_procs_now(p, pfn, flags, folio);
put_page(p);
- action_result(pfn, MF_MSG_UNSPLIT_THP, MF_FAILED);
+ action_result(pfn, MF_MSG_UNSPLIT_THP, MF_FAILED, flags);
goto unlock_mutex;
}
VM_BUG_ON_PAGE(!page_count(p), p);
@@ -2641,7 +2648,7 @@ int memory_failure(unsigned long pfn, int flags)
* Abort on fail: __filemap_remove_folio() assumes unmapped page.
*/
if (!hwpoison_user_mappings(folio, p, pfn, flags)) {
- res = action_result(pfn, MF_MSG_UNMAP_FAILED, MF_FAILED);
+ res = action_result(pfn, MF_MSG_UNMAP_FAILED, MF_FAILED, flags);
goto unlock_page;
}
@@ -2650,12 +2657,12 @@ int memory_failure(unsigned long pfn, int flags)
*/
if (folio_test_lru(folio) && !folio_test_swapcache(folio) &&
folio->mapping == NULL) {
- res = action_result(pfn, MF_MSG_TRUNCATED_LRU, MF_IGNORED);
+ res = action_result(pfn, MF_MSG_TRUNCATED_LRU, MF_IGNORED, flags);
goto unlock_page;
}
identify_page_state:
- res = identify_page_state(pfn, p, page_flags);
+ res = identify_page_state(pfn, p, page_flags, flags);
mutex_unlock(&mf_mutex);
return res;
unlock_page:
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v6 2/7] mm/memory-failure: libstub: install the poisoned-memory EFI table
2026-10-16 12:15 ` [PATCH v6 2/7] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
@ 2026-10-16 12:24 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-16 12:24 UTC (permalink / raw)
To: Breno Leitao, Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner,
Ingo Molnar, Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Miaohe Lin, Naoya Horiguchi,
Oscar Salvador, Greg Kroah-Hartman, Rafael J. Wysocki,
Danilo Krummrich, Catalin Marinas, Will Deacon, Mark Rutland, kas,
kexec, Brendan Jackman, Johannes Weiner, Zi Yan, shakeel.butt
Cc: linux-cxl, rmikey, riel, harry, kernel-team
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] mm: 32-bit integer truncation in efi_poison_alloc causes heap buffer overflow
--
commit 09b9f9b59673eeb21856d3365898ea89a642bbc0
Author: Breno Leitao <leitao@debian.org>
mm/memory-failure: libstub: install the poisoned-memory EFI table
This commit adds support for installing a poisoned-memory EFI configuration
table during boot services. It sizes a bitmap based on the RAM span to
track memory failures.
> diff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
> --- a/drivers/firmware/efi/libstub/efi-stub-helper.c
> +++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
[ ... ]
> +static efi_status_t efi_get_ram_range(u64 *base, u64 *top)
> +{
[ ... ]
> + 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);
[Severity: High]
Could ram_top wrap around to 0 here if it is very close to ULLONG_MAX?
If ram_top wraps to 0, the later calculation of span (ram_top - ram_base)
in install_poisoned_memory_table() would result in a very large 64-bit value.
> +
> + return EFI_SUCCESS;
> +}
[ ... ]
> +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]
Does this risk a heap buffer overflow due to 32-bit integer truncation?
If the earlier calculation produces a bitmap_size exceeding 32 bits, could
sizeof(*pm) + bitmap_size truncate to a tiny allocation on 32-bit
architectures?
The subsequent memset() might then use the truncated or original 64-bit size
to write far past the bounds of the allocated pool:
memset(pm->bitmap, 0, bitmap_size);
> + 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;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261016-hwpoison-kho-v6-0-f304255147da@debian.org?part=2
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v6 6/7] mm/memory-failure: efi: reserve the inherited poisoned-memory units
2026-10-16 12:15 ` [PATCH v6 6/7] mm/memory-failure: efi: reserve the inherited poisoned-memory units Breno Leitao
@ 2026-10-16 12:24 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-16 12:24 UTC (permalink / raw)
To: Breno Leitao, Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner,
Ingo Molnar, Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Miaohe Lin, Naoya Horiguchi,
Oscar Salvador, Greg Kroah-Hartman, Rafael J. Wysocki,
Danilo Krummrich, Catalin Marinas, Will Deacon, Mark Rutland, kas,
kexec, Brendan Jackman, Johannes Weiner, Zi Yan, shakeel.butt
Cc: linux-cxl, rmikey, riel, harry, kernel-team
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] efi: linux_efi_poisoned_memory ABI depends on architecture word size
- [High] efi: missing PHYS_ADDR_MAX check causes physical address truncation
--
commit 458406c8bb39d60c5c6472cbc3795bf8b4bdc8e3
Author: Breno Leitao <leitao@debian.org>
mm/memory-failure: efi: reserve the inherited poisoned-memory units
This commit parses a new EFI table to inherit hardware-poisoned memory frames
from previous kernels during kexec. It prevents the memory allocator from using
these corrupt physical pages.
> diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
> new file mode 100644
> --- /dev/null
> +++ b/drivers/firmware/efi/poison.c
> @@ -0,0 +1,174 @@
[ ... ]
> +static bool __init
> +efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)
> +{
> + const u64 max_end = PHYS_ADDR_MAX - PAGE_SIZE;
> + u64 nbits, span, top, end, len;
> +
> + /* 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 architecture-dependent validation cause valid tables from mismatched
architectures to be rejected during kexec?
If a 32-bit kernel populates the linux_efi_poisoned_memory table with a size
that is a multiple of 4, a 64-bit kexec kernel will evaluate
IS_ALIGNED(pm->size, sizeof(unsigned long)) where sizeof(unsigned long) is 8.
This check would fail, and the table would be erroneously discarded.
Additionally, if the table structure uses unsigned long for the bitmap array,
reading a 32-bit written array as 64-bit words can shift bit indices on
big-endian systems. Could fixed-width types like u64 be used instead to
ensure a stable cross-kernel ABI?
> +
> + /* And a footprint that can be page aligned without wrapping either. */
> + len = sizeof(*pm) + pm->size;
> + if (check_add_overflow(efi.poisoned_memory, len, &end) ||
> + end > max_end)
> + return false;
> +
> + if (pm->unit_size < PAGE_SIZE || !is_power_of_2(pm->unit_size))
> + return false;
> +
> + /* Every unit has to lie inside the address space. */
> + if (check_mul_overflow(nbits, (u64)pm->unit_size, &span) ||
> + check_add_overflow(pm->phys_base, span, &top))
> + return false;
[Severity: High]
Should this validation also check if top exceeds PHYS_ADDR_MAX?
While this calculates the top address in 64-bit space, without an explicit
bounds check, a table specifying a phys_base above 4GB could pass validation
on a 32-bit architecture without PAE.
> +
> + return IS_ALIGNED(pm->phys_base, pm->unit_size);
> +}
[ ... ]
> +static void __init
> +efi_poison_reserve_units(phys_addr_t table,
> + const struct linux_efi_poisoned_memory *hdr)
> +{
[ ... ]
> + for_each_set_bit(bit, map, nbits) {
> + u64 unit = off * BITS_PER_BYTE + bit;
> + phys_addr_t base = hdr->phys_base + unit * unit_size;
[Severity: High]
Can this lead to physical address truncation on 32-bit systems?
Because hdr->phys_base + unit * unit_size evaluates as a 64-bit integer,
assigning it to phys_addr_t (which is 32 bits on 32-bit systems without PAE)
will silently truncate the upper 32 bits. This could cause valid low memory
to be erroneously marked as poisoned and reserved, potentially leading to
boot failures.
> + bool overlap;
> +
> + overlap = memblock_is_region_reserved(base, unit_size);
> + if (memblock_reserve_hwpoison(base, unit_size)) {
> + nr_skipped++;
> + } else {
> + nr_units++;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261016-hwpoison-kho-v6-0-f304255147da@debian.org?part=6
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v6 4/7] mm/memblock: add a reserved-region flag for poisoned frames
2026-10-16 12:15 ` [PATCH v6 4/7] mm/memblock: add a reserved-region flag for poisoned frames Breno Leitao
@ 2026-10-16 12:26 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-16 12:26 UTC (permalink / raw)
To: Breno Leitao, Ard Biesheuvel, Ilias Apalodimas, Thomas Gleixner,
Ingo Molnar, Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Miaohe Lin, Naoya Horiguchi,
Oscar Salvador, Greg Kroah-Hartman, Rafael J. Wysocki,
Danilo Krummrich, Catalin Marinas, Will Deacon, Mark Rutland, kas,
kexec, Brendan Jackman, Johannes Weiner, Zi Yan, shakeel.butt
Cc: linux-cxl, rmikey, riel, harry, kernel-team
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] mm: page-level sharing detection fails due to unaligned base check
--
commit 91f3abf246b482e4b3339361ad3572545e8e83f6
Author: Breno Leitao <leitao@debian.org>
mm/memblock: add a reserved-region flag for poisoned frames
This commit adds the MEMBLOCK_RSRV_HWPOISON flag to memblock to reserve
hardware-poisoned frames found by an earlier kernel. It introduces
memblock_reserve_hwpoison() to handle these reservations while attempting
to avoid overlap with existing reserved regions. Additionally, it ensures
these frames are flagged with PG_hwpoison instead of PG_reserved during
initialization.
> diff --git a/mm/memblock.c b/mm/memblock.c
> index 31961ed628b0a..d20d99972a4db 100644
> --- a/mm/memblock.c
> +++ b/mm/memblock.c
[ ... ]
> @@ -1015,6 +1018,55 @@ int __init_memblock __memblock_reserve(phys_addr_t base, phys_addr_t size,
> return memblock_add_range(&memblock.reserved, base, size, nid, flags);
> }
>
> +int __init memblock_reserve_hwpoison(phys_addr_t base, phys_addr_t size)
> +{
> + phys_addr_t end = base + size;
> + bool reserved = false;
> + int err = 0;
> +
> + while (base < end) {
> + phys_addr_t next = end;
> + struct memblock_region *r;
> + bool covered = false;
> +
> + for_each_reserved_mem_region(r) {
> + if (r->base + r->size <= base)
> + continue;
> + if (r->base >= end)
> + break;
> + if (PAGE_ALIGN_DOWN(r->base) <= base) {
[Severity: High]
Does this byte-level check cause the loop to skip regions that share
the same page but end before the new reservation's base?
The commit message states that a page shared with an earlier reservation
stays with it, so no page is flagged twice. However, if an existing
reservation spans 0x1000-0x1100 and the new HWPOISON reservation requests
0x1800-0x1900 (both in the same 4KB page), 0x1100 <= 0x1800 is true.
This causes the loop to skip the region and bypass the subsequent
PAGE_ALIGN_DOWN overlap check, leading to the new region being reserved
alongside the old one.
If the prior reservation was a KERNEL reservation, the page erroneously
receives both PG_reserved and PG_hwpoison.
[ ... ]
> +/*
> + * Frames an earlier kernel found poisoned end up as a frame poisoned by this
> + * kernel would: PG_hwpoison, not PG_reserved. Holes in the range stay reserved.
> + */
> +static void __init memmap_init_hwpoison_range(phys_addr_t start,
> + phys_addr_t end, int nid)
> +{
> + unsigned long pfn;
> +
> + for_each_valid_pfn(pfn, PFN_DOWN(start), PFN_UP(end)) {
> + struct page *page = pfn_to_page(pfn);
> +
> + init_deferred_page(pfn, nid);
> +
> + if (memblock_is_map_memory(PFN_PHYS(pfn)))
> + hwpoison_boot_page(page);
[Severity: High]
Can this path execute multiple times for the same PFN when a page contains
multiple unmerged HWPOISON fragments?
Because of the unaligned base check bypass above,
memmap_init_reserved_pages() will process the same page twice. Since
hwpoison_boot_page() lacks a TestSetPageHWPoison() guard, it unconditionally
increments num_poisoned_pages and calls adjust_managed_page_count(page, 1)
on every invocation.
This artificially inflates both the global poisoned page count and the
zone's managed page count, potentially leading to page allocator
underflows and corrupted VM statistics.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261016-hwpoison-kho-v6-0-f304255147da@debian.org?part=4
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-10-16 12:26 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-16 12:15 [PATCH v6 0/7] mm/memory-failure: keep hardware-poisoned pages out of the next kexec Breno Leitao
2026-10-16 12:15 ` [PATCH v6 1/7] mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table Breno Leitao
2026-10-16 12:15 ` [PATCH v6 2/7] mm/memory-failure: libstub: install the poisoned-memory EFI table Breno Leitao
2026-10-16 12:24 ` sashiko-bot
2026-10-16 12:15 ` [PATCH v6 3/7] mm/memory-failure: add hwpoison_boot_page() to flag an inherited frame Breno Leitao
2026-10-16 12:15 ` [PATCH v6 4/7] mm/memblock: add a reserved-region flag for poisoned frames Breno Leitao
2026-10-16 12:26 ` sashiko-bot
2026-10-16 12:15 ` [PATCH v6 5/7] drivers/base/memory: count the poison a block already holds when creating it Breno Leitao
2026-10-16 12:15 ` [PATCH v6 6/7] mm/memory-failure: efi: reserve the inherited poisoned-memory units Breno Leitao
2026-10-16 12:24 ` sashiko-bot
2026-10-16 12:15 ` [PATCH v6 7/7] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table Breno Leitao
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox