[PATCH v4 0/5] mm/memory-failure: keep hardware-poisoned pages out of the next kexec
Breno Leitao
leitao at debian.org
Wed Sep 9 06:05:19 PDT 2026
Problem:
========
When a page is hard-offlined due to an uncorrectable memory error (multi
bit ECC), memory_failure() sets PG_hwpoison, unmaps it, removes it from
the buddy allocator. This information is not carried to the next kernel
that is kexeced. The new kernel kexecs and trip over that bad memory
bank _again_.
Why now:
========
Several industry trends make this increasingly important:
1) DRAM is getting more expensive
2) soldered / on-package memory (LPDDR, HBM) is becoming more common, so a
failing part can no longer simply be swapped;
3) memory is kept in service far longer (at Meta, DRAM lifetime is being
drastically extended).
4) It is more and more common to kexec instead of full reboot
5) Increase of memory per system with CXL
What was done already:
======================
In order make linux deal better with the problem above, I've done
already fixed a bunch of stuff in this area, such as:
1) Panic on unrecoverable errors, instead of "printk and carry over":
https://lore.kernel.org/all/20260630-ecc_panic-v10-0-c6ed5b62eea2@debian.org/
2) Respect poisoned memory at kexec time
https://lore.kernel.org/all/20260812-kexec_posioned-v6-0-e477887086f0@debian.org/
Now, the natural follow up is to carry the poisoned memory information
to the next kexec kernel, avoiding tripping over a known "bad page".
Proposed Solution:
==================
Carry the poisoned frames to the next kernel in a new EFI configuration
table, LINUX_EFI_POISONED_MEMORY.
The table is a bitmap with one bit per 2MB of physical memory, modeled
on LINUX_EFI_UNACCEPTED_MEMORY. The stub sizes it from the EFI memory
map and installs it empty while boot services are up -- a running kernel
cannot install a configuration table, it can only flip bits -- and a
table inherited from an earlier boot is reused as-is. That is one
fixed-size allocation, 64KB per TiB of the span the memory map describes,
with no list to grow at runtime and no chain to trust at parse time.
The mechanism is architecture independent, so x86 and arm64 use the same
code.
Each hard offline sets the bit for its unit. Soft-offlined pages are not
recorded: they are still functional, and were offlined predictively.
The next kernel takes the table into use from efi_config_parse_tables(),
which vets the inherited header and hands the table's pages to memblock.
It is EFI ACPI reclaim memory, which x86 leaves out of memblock and so
out of the direct map; the unaccepted memory table is handled the same
way for the same reason.
The frames themselves are poisoned in __free_pages_core(), as each block
reaches the buddy allocator. That is the one point every producer passes
through -- memblock_free_pages() early, deferred_free_pages() once the
deferred struct pages are up, and generic_online_page() at any later
hotplug -- and it is already where the unaccepted memory table is
consulted. A block covering a recorded frame is dropped whole rather than
freed, so the frames never enter the allocator instead of being taken
back out of it. They end up in the state a frame poisoned by this kernel
would be in, so everything that already understands PG_hwpoison covers
them -- including the kexec segment placement check from the series
linked above, which is what keeps the next kernel image off these frames.
Blocks run up to MAX_PAGE_ORDER and a unit is 2MB, so dropping the block
writes off more than the unit itself, and MemFree falls by more than
HardwareCorrupted accounts for.
Granularity is the trade-off: one bad 4KB frame costs a whole 2MB unit in
every later kernel of the chain. In exchange, a row or column fault --
roughly a quarter of the DRAM faults reported in [1], and potentially
thousands of 4KB pages scattered over gigabytes -- collapses into a bit
or two.
A bit is never cleared, which is a known limitation: it stands for a
whole unit, so an unpoison of one frame cannot tell whether the unit as a
whole is good again.
Known limitations:
==================
In order to keep this patchset digestible, I am making some trade-offs,
thus, this feature has the following limitations:
- Memory hot-added after boot is not covered: the bitmap spans the RAM
the EFI memory map describes, and a frame above it is not recorded.
Same gap the unaccepted-memory table has.
- Memory preserved across a KHO handover is not covered.
kho_preserved_memory_reserve() marks it MEMBLOCK_RSRV_NOINIT, so
memmap_init_reserved_pages() leaves those struct pages uninitialised,
and a frame there is neither free nor PageReserved when the bitmap is
applied. Its record is dropped, and the frame goes back to the
allocator once the owner releases it.
- Without a memblock reservation, early boot can allocate over a
recorded frame before the flag is applied. The frame does not reach
the allocator afterwards, but the memmap or page tables may end up
sitting on it.
- The per memory block hardware-poison counter does not include
inherited frames. memblk_nr_poison_inc() finds the block by pfn and
memory_dev_init() has not built it yet, so the count is dropped and
the block is later created reading zero. This is not a big deal,
since I do not expect the kexeced kernel to try to unpoison the
page that came from previous kernel. The only downside is the fact
that kernel B will not have a proper counting of how many pages
are poisoned (they will be invisible -- aka 0).
All of the limitations above can be fixed in follow up work. I am trying
to keep this patchset the foundation, with that work done on top.
The series is five patches:
1) add the LINUX_EFI_POISONED_MEMORY table
2) size, build and install it from both stub entry paths
3) record poisoned frames into the table from the memory_failure() path,
and hand the table to memblock so it can be reached later
4) answer whether a range covers a recorded frame
5) apply it from __free_pages_core(), as each block reaches the buddy
allocator
This was initially discussed at
https://lore.kernel.org/all/ajut_LDQGYCShApx@gmail.com/
A special thanks to Kiryl Shutsemau, for feedbacks and suggestions.
[1] https://arxiv.org/abs/2408.15302
To: Ard Biesheuvel <ardb at kernel.org>
To: Ilias Apalodimas <ilias.apalodimas at linaro.org>
To: Miaohe Lin <linmiaohe at huawei.com>
To: Naoya Horiguchi <nao.horiguchi at gmail.com>
To: Andrew Morton <akpm at linux-foundation.org>
Cc: linux-efi at vger.kernel.org
Cc: linux-kernel at vger.kernel.org
Cc: linux-mm at kvack.org
Cc: rmikey at meta.com
To: kas at kernel.org
Cc: riel at surriel.com
To: kexec at lists.infradead.org
Signed-off-by: Breno Leitao <leitao at 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 at kernel.org>
To: Ilias Apalodimas <ilias.apalodimas at linaro.org>
To: Miaohe Lin <linmiaohe at huawei.com>
To: Naoya Horiguchi <nao.horiguchi at gmail.com>
To: Andrew Morton <akpm at linux-foundation.org>
To: David Hildenbrand <david at kernel.org>
To: Lorenzo Stoakes <ljs at kernel.org>
To: "Liam R. Howlett" <liam at infradead.org>
To: Vlastimil Babka <vbabka at kernel.org>
To: Mike Rapoport <rppt at kernel.org>
To: Suren Baghdasaryan <surenb at google.com>
To: Michal Hocko <mhocko at suse.com>
To: Thomas Gleixner <tglx at kernel.org>
To: Ingo Molnar <mingo at redhat.com>
To: Borislav Petkov <bp at alien8.de>
To: Dave Hansen <dave.hansen at linux.intel.com>
To: x86 at kernel.org
To: "H. Peter Anvin" <hpa at zytor.com>
To: Brendan Jackman <brendan.jackman at linux.dev>
To: Johannes Weiner <hannes at cmpxchg.org>
To: Zi Yan <ziy at nvidia.com>
Cc: linux-efi at vger.kernel.org
Cc: linux-kernel at vger.kernel.org
Cc: linux-mm at kvack.org
Cc: harry at kernel.org
---
Breno Leitao (5):
mm/memory-failure: efi: add the LINUX_EFI_POISONED_MEMORY configuration table
mm/memory-failure: libstub: install the poisoned-memory EFI table
mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table
mm/memory-failure: efi: answer whether a range is poisoned
mm/memory-failure: keep inherited poisoned frames out of the buddy allocator
arch/x86/platform/efi/efi.c | 3 +
drivers/firmware/efi/Kconfig | 8 ++
drivers/firmware/efi/Makefile | 1 +
drivers/firmware/efi/efi.c | 8 ++
drivers/firmware/efi/libstub/efi-stub-helper.c | 100 ++++++++++++++++++
drivers/firmware/efi/libstub/efi-stub.c | 1 +
drivers/firmware/efi/libstub/efistub.h | 6 ++
drivers/firmware/efi/libstub/x86-stub.c | 2 +
drivers/firmware/efi/poison.c | 141 +++++++++++++++++++++++++
include/linux/efi.h | 22 ++++
include/linux/mm.h | 19 ++++
mm/memory-failure.c | 18 ++++
mm/page_alloc.c | 25 +++++
13 files changed, 354 insertions(+)
---
base-commit: a9d7ced84989ec05be09b4b8428759ef60450a0f
change-id: 20260622-hwpoison-kho-fc9db2ada8ba
Best regards,
--
Breno Leitao <leitao at debian.org>
More information about the kexec
mailing list