[PATCH v6 7/7] mm/memory-failure: efi: record hardware-poisoned frames into the poisoned-memory table

From: Breno Leitao

Date: Fri Oct 16 2026 - 08:19:44 EST


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@xxxxxxxxxx>
---
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