AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/09/09 14:09 flow patch-triage
1m Args:
null

Results:
{
  "EnableConfigs": [
    "EFI_POISONED_MEMORY",
    "MEMORY_FAILURE",
    "EFI_STUB",
    "EFI"
  ],
  "FocusSymbols": [
    "efi_hwpoison_record_pfn",
    "range_contains_poisoned_memory",
    "hwpoison_boot_page",
    "memory_failure"
  ],
  "KMSANReasoning": "The patch series introduces EFI poisoned memory tracking (CONFIG_EFI_POISONED_MEMORY) to preserve hardware-poisoned page information across kexec reboots. \n\n1. Allocation \u0026 Initialization: The table structure `struct linux_efi_poisoned_memory` and its trailing bitmap are allocated in the EFI stub and explicitly fully initialized (all header fields assigned, and `memset(pm-\u003ebitmap, 0, bitmap_size)` clears the bitmap).\n2. Memory Accesses: Runtime operations (`range_contains_poisoned_memory`, `efi_hwpoison_record_pfn`, `poison_block`, and `hwpoison_boot_page`) only read and update fully initialized memory structures and struct page flags.\n3. No Uninitialized Leaks or Reads: There are no data transfers to user space, no conditional branches on uninitialized variables, and no exposed padding.\n4. KASAN vs KMSAN: Potential risks such as invalid pointer math, out-of-bounds bitmap accesses, or memory reservation errors are properly handled by KASAN and standard kernel debugging tools. KMSAN is not required.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch adds support for tracking hardware-poisoned memory across kexec via an EFI configuration table (CONFIG_EFI_POISONED_MEMORY). It introduces hooks in core memory-failure handling (efi_hwpoison_record_pfn in action_result) and memory initialization/hotplug (__free_pages_core, hwpoison_boot_page). These paths are reachable at runtime via memory failure mechanisms such as madvise(MADV_HWPOISON) and memory hotplugging in virtualized environments with EFI.",
  "WorthFuzzing": true
}

1/1 2026/09/09 14:09 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit a79cbb105c59817af32a22a00171f7bb91467654\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Wed Sep 9 14:09:05 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/arch/x86/platform/efi/efi.c b/arch/x86/platform/efi/efi.c\nindex 0c39adb96b912..2b37b96a36e09 100644\n--- a/arch/x86/platform/efi/efi.c\n+++ b/arch/x86/platform/efi/efi.c\n@@ -93,6 +93,9 @@ static const unsigned long * const efi_tables[] = {\n #ifdef CONFIG_UNACCEPTED_MEMORY\n \t\u0026efi.unaccepted,\n #endif\n+#ifdef CONFIG_EFI_POISONED_MEMORY\n+\t\u0026efi.poisoned_memory,\n+#endif\n };\n \n u64 efi_setup;\t\t/* efi setup_data physical address */\ndiff --git a/drivers/firmware/efi/Kconfig b/drivers/firmware/efi/Kconfig\nindex 29e0729299f5b..aafcd41bc0063 100644\n--- a/drivers/firmware/efi/Kconfig\n+++ b/drivers/firmware/efi/Kconfig\n@@ -263,6 +263,14 @@ config EFI_COCO_SECRET\n \t  virt/coco/efi_secret module to access the secrets, which in turn\n \t  allows userspace programs to access the injected secrets.\n \n+config EFI_POISONED_MEMORY\n+\tdef_bool y\n+\tdepends on EFI_STUB \u0026\u0026 MEMORY_FAILURE \u0026\u0026 64BIT\n+\thelp\n+\t  Record page frames that are hardware-poisoned while this kernel runs\n+\t  into an EFI configuration table, and honor that table early on the\n+\t  next kernel so a kexec does not hand known-bad RAM back out.\n+\n config OVMF_DEBUG_LOG\n \tbool \"Expose OVMF firmware debug log via sysfs\"\n \tdepends on EFI\ndiff --git a/drivers/firmware/efi/Makefile b/drivers/firmware/efi/Makefile\nindex 8efbcf699e4ff..05d0a490923e5 100644\n--- a/drivers/firmware/efi/Makefile\n+++ b/drivers/firmware/efi/Makefile\n@@ -43,4 +43,5 @@ obj-$(CONFIG_EFI_EARLYCON)\t\t+= earlycon.o\n obj-$(CONFIG_UEFI_CPER_ARM)\t\t+= cper-arm.o\n obj-$(CONFIG_UEFI_CPER_X86)\t\t+= cper-x86.o\n obj-$(CONFIG_UNACCEPTED_MEMORY)\t\t+= unaccepted_memory.o\n+obj-$(CONFIG_EFI_POISONED_MEMORY)\t+= poison.o\n obj-$(CONFIG_TEE_STMM_EFI)\t\t+= stmm/tee_stmm_efi.o\ndiff --git a/drivers/firmware/efi/efi.c b/drivers/firmware/efi/efi.c\nindex 6d987d7f97781..55b2ee53fc268 100644\n--- a/drivers/firmware/efi/efi.c\n+++ b/drivers/firmware/efi/efi.c\n@@ -55,6 +55,9 @@ struct efi __read_mostly efi = {\n #ifdef CONFIG_UNACCEPTED_MEMORY\n \t.unaccepted\t\t= EFI_INVALID_TABLE_ADDR,\n #endif\n+#ifdef CONFIG_EFI_POISONED_MEMORY\n+\t.poisoned_memory\t= EFI_INVALID_TABLE_ADDR,\n+#endif\n };\n EXPORT_SYMBOL(efi);\n \n@@ -677,6 +680,9 @@ static const efi_config_table_type_t common_tables[] __initconst = {\n #ifdef CONFIG_UNACCEPTED_MEMORY\n \t{LINUX_EFI_UNACCEPTED_MEM_TABLE_GUID,\t\u0026efi.unaccepted,\t\"Unaccepted\"\t},\n #endif\n+#ifdef CONFIG_EFI_POISONED_MEMORY\n+\t{LINUX_EFI_POISONED_MEMORY_TABLE_GUID,\t\u0026efi.poisoned_memory,\t\"POISON\"\t},\n+#endif\n #ifdef CONFIG_EFI_GENERIC_STUB\n \t{LINUX_EFI_PRIMARY_DISPLAY_TABLE_GUID,\t\u0026primary_display_table\t\t\t},\n #endif\n@@ -877,6 +883,8 @@ int __init efi_config_parse_tables(const efi_config_table_t *config_tables,\n \t\t}\n \t}\n \n+\tefi_poisoned_memory_reserve();\n+\n \treturn 0;\n }\n \ndiff --git a/drivers/firmware/efi/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c\nindex 48f93f7758e9e..5cbe675491333 100644\n--- a/drivers/firmware/efi/libstub/efi-stub-helper.c\n+++ b/drivers/firmware/efi/libstub/efi-stub-helper.c\n@@ -774,3 +774,103 @@ void efi_remap_image(unsigned long image_base, unsigned alloc_size,\n \t\t\tefi_warn(\"Failed to remap data region non-executable\\n\");\n \t}\n }\n+\n+#ifdef CONFIG_EFI_POISONED_MEMORY\n+/*\n+ * Find the base and top of the memory, so, we can create the bitmap for\n+ * the full range.\n+ */\n+static efi_status_t efi_get_ram_range(u64 *base, u64 *top)\n+{\n+\tstruct efi_boot_memmap *map __free(efi_pool) = NULL;\n+\tu64 ram_base = ULLONG_MAX, ram_top = 0;\n+\tefi_status_t status;\n+\tint i, nr_desc;\n+\n+\tstatus = efi_get_memory_map(\u0026map, false);\n+\tif (status != EFI_SUCCESS)\n+\t\treturn status;\n+\n+\tnr_desc = map-\u003emap_size / map-\u003edesc_size;\n+\tfor (i = 0; i \u003c nr_desc; i++) {\n+\t\tefi_memory_desc_t *d;\n+\n+\t\td = efi_memdesc_ptr((unsigned long)map-\u003emap, map-\u003edesc_size, i);\n+\t\tram_base = min(ram_base, d-\u003ephys_addr);\n+\t\tram_top = max(ram_top,\n+\t\t\t      d-\u003ephys_addr + d-\u003enum_pages * EFI_PAGE_SIZE);\n+\t}\n+\tif (!ram_top || ram_base == ULLONG_MAX)\n+\t\treturn EFI_NOT_FOUND;\n+\n+\t*base = round_down(ram_base, EFI_POISON_UNIT_SIZE);\n+\t*top = round_up(ram_top, EFI_POISON_UNIT_SIZE);\n+\n+\treturn EFI_SUCCESS;\n+}\n+\n+/* The size of the bitmap */\n+static u64 efi_poison_bitmap_size(u64 span)\n+{\n+\tu64 bytes = DIV_ROUND_UP(DIV_ROUND_UP(span, EFI_POISON_UNIT_SIZE),\n+\t\t\t\t BITS_PER_BYTE);\n+\n+\treturn round_up(bytes, sizeof(unsigned long));\n+}\n+\n+static struct linux_efi_poisoned_memory *efi_poison_alloc(u64 phys_base,\n+\t\t\t\t\t\t\t  u64 bitmap_size)\n+{\n+\tstruct linux_efi_poisoned_memory *pm;\n+\tefi_status_t status;\n+\n+\tstatus = efi_bs_call(allocate_pool, EFI_ACPI_RECLAIM_MEMORY,\n+\t\t\t     sizeof(*pm) + bitmap_size, (void **)\u0026pm);\n+\tif (status != EFI_SUCCESS)\n+\t\treturn NULL;\n+\n+\tpm-\u003eversion = 1;\n+\tpm-\u003eunit_size = EFI_POISON_UNIT_SIZE;\n+\tpm-\u003ephys_base = phys_base;\n+\tpm-\u003esize = bitmap_size;\n+\tmemset(pm-\u003ebitmap, 0, bitmap_size);\n+\n+\treturn pm;\n+}\n+\n+/* This needs to be done while boot service is still active */\n+void install_poisoned_memory_table(void)\n+{\n+\tefi_guid_t poisoned_memory_table_guid = LINUX_EFI_POISONED_MEMORY_TABLE_GUID;\n+\tstruct linux_efi_poisoned_memory *pm;\n+\tu64 ram_base, ram_top, bitmap_size;\n+\tefi_status_t status;\n+\n+\t/* A table installed by an earlier boot rides the system table across kexec. */\n+\tpm = get_efi_config_table(poisoned_memory_table_guid);\n+\tif (pm) {\n+\t\tif (pm-\u003eversion != 1)\n+\t\t\tefi_err(\"Unknown version of poisoned-memory table\\n\");\n+\t\treturn;\n+\t}\n+\n+\tif (efi_get_ram_range(\u0026ram_base, \u0026ram_top) != EFI_SUCCESS) {\n+\t\tefi_err(\"Failed to size the poisoned-memory table!\\n\");\n+\t\treturn;\n+\t}\n+\n+\tbitmap_size = efi_poison_bitmap_size(ram_top - ram_base);\n+\tpm = efi_poison_alloc(ram_base, bitmap_size);\n+\tif (!pm) {\n+\t\tefi_err(\"Failed to allocate poisoned-memory table!\\n\");\n+\t\treturn;\n+\t}\n+\n+\tstatus = efi_bs_call(install_configuration_table,\n+\t\t\t     \u0026poisoned_memory_table_guid, pm);\n+\tif (status != EFI_SUCCESS) {\n+\t\tefi_bs_call(free_pool, pm);\n+\t\tefi_err(\"Failed to install poisoned-memory config table!\\n\");\n+\t}\n+}\n+#endif\ndiff --git a/drivers/firmware/efi/libstub/efi-stub.c b/drivers/firmware/efi/libstub/efi-stub.c\nindex 235c9738da2d6..22a315e2814a1 100644\n--- a/drivers/firmware/efi/libstub/efi-stub.c\n+++ b/drivers/firmware/efi/libstub/efi-stub.c\n@@ -179,6 +179,7 @@ efi_status_t efi_stub_common(efi_handle_t handle,\n \t\t\t EFI_RT_SUPPORTED_SET_VIRTUAL_ADDRESS_MAP);\n \n \tinstall_memreserve_table();\n+\tinstall_poisoned_memory_table();\n \n \tstatus = efi_boot_kernel(handle, image, image_addr, cmdline_ptr);\n \ndiff --git a/drivers/firmware/efi/libstub/efistub.h b/drivers/firmware/efi/libstub/efistub.h\nindex fd91fc15ec810..44436869c4efe 100644\n--- a/drivers/firmware/efi/libstub/efistub.h\n+++ b/drivers/firmware/efi/libstub/efistub.h\n@@ -1169,6 +1169,12 @@ efi_enable_reset_attack_mitigation(void) { }\n \n void efi_retrieve_eventlog(void);\n \n+#ifdef CONFIG_EFI_POISONED_MEMORY\n+void install_poisoned_memory_table(void);\n+#else\n+static inline void install_poisoned_memory_table(void) { }\n+#endif\n+\n struct sysfb_display_info *alloc_primary_display(void);\n struct sysfb_display_info *__alloc_primary_display(void);\n void free_primary_display(struct sysfb_display_info *dpy);\ndiff --git a/drivers/firmware/efi/libstub/x86-stub.c b/drivers/firmware/efi/libstub/x86-stub.c\nindex 0bae0f06b6763..3136132b9628a 100644\n--- a/drivers/firmware/efi/libstub/x86-stub.c\n+++ b/drivers/firmware/efi/libstub/x86-stub.c\n@@ -1024,6 +1024,8 @@ void __noreturn efi_stub_entry(efi_handle_t handle,\n \n \tsetup_unaccepted_memory();\n \n+\tinstall_poisoned_memory_table();\n+\n \tstatus = exit_boot(boot_params, handle);\n \tif (status != EFI_SUCCESS) {\n \t\tefi_err(\"exit_boot() failed!\\n\");\ndiff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c\nnew file mode 100644\nindex 0000000000000..e16d43f4438ee\n--- /dev/null\n+++ b/drivers/firmware/efi/poison.c\n@@ -0,0 +1,141 @@\n+// SPDX-License-Identifier: GPL-2.0-only\n+/*\n+ * Runtime side of the LINUX_EFI_POISONED_MEMORY table: one bit per\n+ * EFI_POISON_UNIT_SIZE, set here as frames go bad, honored by the next kernel.\n+ *\n+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.\n+ * Copyright (c) 2026 Breno Leitao \u003cleitao@debian.org\u003e\n+ */\n+\n+#define pr_fmt(fmt) \"efi: \" fmt\n+\n+#include \u003clinux/bitmap.h\u003e\n+#include \u003clinux/efi.h\u003e\n+#include \u003clinux/io.h\u003e\n+#include \u003clinux/log2.h\u003e\n+#include \u003clinux/memblock.h\u003e\n+#include \u003clinux/mm.h\u003e\n+#include \u003clinux/overflow.h\u003e\n+\n+static bool __init\n+efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)\n+{\n+\tu64 nbits;\n+\n+\t/* Whole words, and a bit count that can be taken without wrapping. */\n+\tif (!pm-\u003esize || !IS_ALIGNED(pm-\u003esize, sizeof(unsigned long)) ||\n+\t    check_mul_overflow(pm-\u003esize, (u64)BITS_PER_BYTE, \u0026nbits))\n+\t\treturn false;\n+\n+\tif (pm-\u003eunit_size \u003c PAGE_SIZE || !is_power_of_2(pm-\u003eunit_size))\n+\t\treturn false;\n+\n+\treturn IS_ALIGNED(pm-\u003ephys_base, pm-\u003eunit_size);\n+}\n+\n+/* The table may come from an earlier kernel, so vet it before using it. */\n+static bool __init\n+efi_poison_table_valid(const struct linux_efi_poisoned_memory *pm)\n+{\n+\tif (pm-\u003eversion != 1) {\n+\t\tpr_warn(\"Ignoring poisoned-memory table with version %u\\n\",\n+\t\t\tpm-\u003eversion);\n+\t\treturn false;\n+\t}\n+\n+\tif (!efi_poison_geometry_valid(pm)) {\n+\t\tpr_warn(\"Ignoring malformed poisoned-memory table\\n\");\n+\t\treturn false;\n+\t}\n+\n+\treturn true;\n+}\n+\n+/*\n+ * Vet the inherited table and hand its pages to memblock, the way the\n+ * unaccepted memory table is handled. It is EFI ACPI reclaim memory, which\n+ * becomes E820_TYPE_ACPI and would otherwise stay out of the direct map, and\n+ * touching it then faults. Called from efi_config_parse_tables(), so\n+ * everything later can reach it with efi_poisoned_memory().\n+ */\n+void __init efi_poisoned_memory_reserve(void)\n+{\n+\tstruct linux_efi_poisoned_memory *pm;\n+\tphys_addr_t start, end;\n+\n+\tif (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)\n+\t\treturn;\n+\n+\tpm = early_memremap(efi.poisoned_memory, sizeof(*pm));\n+\tif (!pm) {\n+\t\tpr_warn(\"Could not map poisoned-memory table\\n\");\n+\t\tefi.poisoned_memory = EFI_INVALID_TABLE_ADDR;\n+\t\treturn;\n+\t}\n+\n+\tif (!efi_poison_table_valid(pm)) {\n+\t\tefi.poisoned_memory = EFI_INVALID_TABLE_ADDR;\n+\t\tearly_memunmap(pm, sizeof(*pm));\n+\t\treturn;\n+\t}\n+\n+\tstart = PAGE_ALIGN_DOWN(efi.poisoned_memory);\n+\tend = PAGE_ALIGN(efi.poisoned_memory + sizeof(*pm) + pm-\u003esize);\n+\tearly_memunmap(pm, sizeof(*pm));\n+\n+\tmemblock_add(start, end - start);\n+\tmemblock_reserve(start, end - start);\n+}\n+\n+/* The table, vetted at parse time, or NULL if this boot has none. */\n+static struct linux_efi_poisoned_memory *efi_poisoned_memory(void)\n+{\n+\tif (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)\n+\t\treturn NULL;\n+\n+\treturn phys_to_virt(efi.poisoned_memory);\n+}\n+\n+/* Does the range cover a unit an earlier kernel recorded as bad? */\n+bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size)\n+{\n+\tstruct linux_efi_poisoned_memory *pm = efi_poisoned_memory();\n+\tu64 first, last, nbits;\n+\n+\tif (!pm)\n+\t\treturn false;\n+\n+\tnbits = pm-\u003esize * BITS_PER_BYTE;\n+\n+\tif (start + size \u003c= pm-\u003ephys_base)\n+\t\treturn false;\n+\tif (start \u003c pm-\u003ephys_base)\n+\t\tstart = pm-\u003ephys_base;\n+\n+\tfirst = (start - pm-\u003ephys_base) / pm-\u003eunit_size;\n+\tif (first \u003e= nbits)\n+\t\treturn false;\n+\n+\tlast = (start + size - 1 - pm-\u003ephys_base) / pm-\u003eunit_size;\n+\tlast = min(last, nbits - 1);\n+\n+\treturn find_next_bit(pm-\u003ebitmap, last + 1, first) \u003c= last;\n+}\n+\n+/*\n+ * A bit is never cleared: it stands for a whole EFI_POISON_UNIT_SIZE, so an\n+ * unpoison cannot tell whether the unit as a whole is good again.\n+ */\n+void efi_hwpoison_record_pfn(unsigned long pfn)\n+{\n+\tstruct linux_efi_poisoned_memory *pm = efi_poisoned_memory();\n+\tphys_addr_t addr = PFN_PHYS(pfn);\n+\tu64 unit;\n+\n+\tif (!pm || addr \u003c pm-\u003ephys_base)\n+\t\treturn;\n+\n+\tunit = (addr - pm-\u003ephys_base) / pm-\u003eunit_size;\n+\tif (unit \u003c pm-\u003esize * BITS_PER_BYTE)\n+\t\tset_bit(unit, pm-\u003ebitmap);\n+}\ndiff --git a/include/linux/efi.h b/include/linux/efi.h\nindex c35446a0b66fa..56402fdccd114 100644\n--- a/include/linux/efi.h\n+++ b/include/linux/efi.h\n@@ -23,6 +23,7 @@\n #include \u003clinux/pstore.h\u003e\n #include \u003clinux/range.h\u003e\n #include \u003clinux/reboot.h\u003e\n+#include \u003clinux/sizes.h\u003e\n #include \u003clinux/uuid.h\u003e\n \n #include \u003casm/page.h\u003e\n@@ -422,6 +423,7 @@ void efi_native_runtime_setup(void);\n #define LINUX_EFI_COCO_SECRET_AREA_GUID\t\tEFI_GUID(0xadf956ad, 0xe98c, 0x484c,  0xae, 0x11, 0xb5, 0x1c, 0x7d, 0x33, 0x64, 0x47)\n #define LINUX_EFI_BOOT_MEMMAP_GUID\t\tEFI_GUID(0x800f683f, 0xd08b, 0x423a,  0xa2, 0x93, 0x96, 0x5c, 0x3c, 0x6f, 0xe2, 0xb4)\n #define LINUX_EFI_UNACCEPTED_MEM_TABLE_GUID\tEFI_GUID(0xd5d1de3c, 0x105c, 0x44f9,  0x9e, 0xa9, 0xbc, 0xef, 0x98, 0x12, 0x00, 0x31)\n+#define LINUX_EFI_POISONED_MEMORY_TABLE_GUID\tEFI_GUID(0xaf828a15, 0x0ef4, 0x439a,  0xb8, 0x6a, 0xd6, 0xd6, 0x9e, 0xaf, 0xba, 0xfa)\n \n #define RISCV_EFI_BOOT_PROTOCOL_GUID\t\tEFI_GUID(0xccd15fec, 0x6f73, 0x4eec,  0x83, 0x95, 0x3e, 0x69, 0xe4, 0xb9, 0x40, 0xbf)\n \n@@ -650,6 +652,7 @@ extern struct efi {\n \tunsigned long\t\t\tmokvar_table;\t\t/* MOK variable config table */\n \tunsigned long\t\t\tcoco_secret;\t\t/* Confidential computing secret table */\n \tunsigned long\t\t\tunaccepted;\t\t/* Unaccepted memory table */\n+\tunsigned long\t\t\tpoisoned_memory;\t/* Hardware-poisoned memory table */\n \n \tefi_get_time_t\t\t\t*get_time;\n \tefi_set_time_t\t\t\t*set_time;\n@@ -1272,6 +1275,25 @@ struct linux_efi_memreserve {\n #define EFI_MEMRESERVE_COUNT(size) (((size) - sizeof(struct linux_efi_memreserve)) \\\n \t/ sizeof_field(struct linux_efi_memreserve, entry[0]))\n \n+/* Bit N covers the unit at @phys_base + N * @unit_size. */\n+struct linux_efi_poisoned_memory {\n+\tu32\t\tversion;\n+\tu32\t\tunit_size;\t/* bytes of phys space per bitmap bit */\n+\tu64\t\tphys_base;\t/* address the first bit covers */\n+\tu64\t\tsize;\t\t/* bitmap size in bytes */\n+\tunsigned long\tbitmap[];\n+};\n+\n+#define EFI_POISON_UNIT_SIZE\tSZ_2M\n+\n+#ifdef CONFIG_EFI_POISONED_MEMORY\n+void __init efi_poisoned_memory_reserve(void);\n+void efi_hwpoison_record_pfn(unsigned long pfn);\n+#else\n+static inline void efi_poisoned_memory_reserve(void) { }\n+static inline void efi_hwpoison_record_pfn(unsigned long pfn) { }\n+#endif\n+\n void __init efi_arch_mem_reserve(phys_addr_t addr, u64 size);\n \n /*\ndiff --git a/include/linux/mm.h b/include/linux/mm.h\nindex 274fa880077c5..9d9f2e8fdc136 100644\n--- a/include/linux/mm.h\n+++ b/include/linux/mm.h\n@@ -5225,6 +5225,7 @@ extern const struct attribute_group memory_failure_attr_group;\n extern void memory_failure_queue(unsigned long pfn, int flags);\n void num_poisoned_pages_inc(unsigned long pfn);\n void num_poisoned_pages_sub(unsigned long pfn, long i);\n+void __meminit hwpoison_boot_page(struct page *page);\n phys_addr_t range_first_hwpoison(phys_addr_t start, unsigned long size);\n phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size);\n #else\n@@ -5232,6 +5233,10 @@ static inline void memory_failure_queue(unsigned long pfn, int flags)\n {\n }\n \n+static inline void hwpoison_boot_page(struct page *page)\n+{\n+}\n+\n static inline void num_poisoned_pages_inc(unsigned long pfn)\n {\n }\n@@ -5387,6 +5392,20 @@ static inline bool pfn_is_unaccepted_memory(unsigned long pfn)\n \treturn range_contains_unaccepted_memory(pfn \u003c\u003c PAGE_SHIFT, PAGE_SIZE);\n }\n \n+#ifdef CONFIG_EFI_POISONED_MEMORY\n+\n+bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size);\n+\n+#else\n+\n+static inline bool range_contains_poisoned_memory(phys_addr_t start,\n+\t\t\t\t\t\t  unsigned long size)\n+{\n+\treturn false;\n+}\n+\n+#endif\n+\n void vma_pgtable_walk_begin(struct vm_area_struct *vma);\n void vma_pgtable_walk_end(struct vm_area_struct *vma);\n \ndiff --git a/mm/memory-failure.c b/mm/memory-failure.c\nindex a2ca8df501cae..f6afdb2a89a94 100644\n--- a/mm/memory-failure.c\n+++ b/mm/memory-failure.c\n@@ -43,6 +43,7 @@\n #include \u003clinux/sched/signal.h\u003e\n #include \u003clinux/sched/task.h\u003e\n #include \u003clinux/dax.h\u003e\n+#include \u003clinux/efi.h\u003e\n #include \u003clinux/ksm.h\u003e\n #include \u003clinux/rmap.h\u003e\n #include \u003clinux/export.h\u003e\n@@ -136,6 +137,21 @@ phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size)\n \treturn range_hwpoison(start, size, false);\n }\n \n+static void update_per_node_mf_stats(unsigned long pfn, enum mf_result result);\n+\n+/* Not num_poisoned_pages_inc(): its per block half divides by zero this early. */\n+void __meminit hwpoison_boot_page(struct page *page)\n+{\n+\tif (PageHWPoison(page))\n+\t\treturn;\n+\n+\tSetPageHWPoison(page);\n+\tset_page_count(page, 1);\n+\t/* The page has been completely isolated == MF_RECOVERED */\n+\tupdate_per_node_mf_stats(page_to_pfn(page), MF_RECOVERED);\n+\tatomic_long_inc(\u0026num_poisoned_pages);\n+}\n+\n /**\n  * MF_ATTR_RO - Create sysfs entry for each memory failure statistics.\n  * @_name: name of the file in the per NUMA sysfs directory.\n@@ -1326,6 +1342,8 @@ static int action_result(unsigned long pfn, enum mf_action_page_type type,\n \tif (type != MF_MSG_ALREADY_POISONED \u0026\u0026 type != MF_MSG_PFN_MAP) {\n \t\tnum_poisoned_pages_inc(pfn);\n \t\tupdate_per_node_mf_stats(pfn, result);\n+\t\t/* Only hard offlines are carried over to the next kernel. */\n+\t\tefi_hwpoison_record_pfn(pfn);\n \t}\n \n \tpr_err(\"%#lx: recovery action for %s: %s\\n\",\ndiff --git a/mm/page_alloc.c b/mm/page_alloc.c\nindex 404896b53003e..9e2ce833fd409 100644\n--- a/mm/page_alloc.c\n+++ b/mm/page_alloc.c\n@@ -1579,6 +1579,19 @@ static void __free_pages_ok(struct page *page, unsigned int order,\n \t\tfree_one_page(zone, page, pfn, order, fpi_flags);\n }\n \n+/* Flag the frames an earlier kernel recorded as bad. */\n+static void __meminit poison_block(struct page *page, unsigned int order)\n+{\n+\tunsigned long i, nr_pages = 1UL \u003c\u003c order;\n+\n+\tfor (i = 0; i \u003c nr_pages; i++) {\n+\t\tstruct page *p = page + i;\n+\n+\t\tif (range_contains_poisoned_memory(page_to_phys(p), PAGE_SIZE))\n+\t\t\thwpoison_boot_page(p);\n+\t}\n+}\n+\n void __meminit __free_pages_core(struct page *page, unsigned int order,\n \t\tenum meminit_context context)\n {\n@@ -1613,6 +1626,18 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,\n \t\tatomic_long_add(nr_pages, \u0026page_zone(page)-\u003emanaged_pages);\n \t}\n \n+\t/* First: a block parked by __free_unaccepted() never returns here. */\n+\tif (range_contains_poisoned_memory(page_to_phys(page),\n+\t\t\t\t\t   PAGE_SIZE \u003c\u003c order)) {\n+\t\tpoison_block(page, order);\n+\t\t/*\n+\t\t * TODO: free the frames in the block that are not poisoned.\n+\t\t * They stay out of the allocator and still count in\n+\t\t * managed_pages, so a unit costs up to a block.\n+\t\t */\n+\t\treturn;\n+\t}\n+\n \tif (page_contains_unaccepted(page, order)) {\n \t\tif (order == MAX_PAGE_ORDER \u0026\u0026 __free_unaccepted(page))\n \t\t\treturn;\n"
}

2/1 2026/09/09 14:09 agent patch-evaluator
1m Model:
core

Args:
null

Results:
{
  "EnableConfigs": [
    "EFI_POISONED_MEMORY",
    "MEMORY_FAILURE",
    "EFI_STUB",
    "EFI"
  ],
  "FocusSymbols": [
    "efi_hwpoison_record_pfn",
    "range_contains_poisoned_memory",
    "hwpoison_boot_page",
    "memory_failure"
  ],
  "Reasoning": "The patch adds support for tracking hardware-poisoned memory across kexec via an EFI configuration table (CONFIG_EFI_POISONED_MEMORY). It introduces hooks in core memory-failure handling (efi_hwpoison_record_pfn in action_result) and memory initialization/hotplug (__free_pages_core, hwpoison_boot_page). These paths are reachable at runtime via memory failure mechanisms such as madvise(MADV_HWPOISON) and memory hotplugging in virtualized environments with EFI.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer and security engineer.
Your job is to review a provided patch series and evaluate whether it warrants fuzzing with syzkaller.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on internal assumptions. You must actively use your code access
tools to inspect the actual source code, callers, and surrounding context.

================================================================================
1. CORE TRIAGE PHILOSOPHY
================================================================================
The goal of patch fuzzing is to discover crashes, regressions, exposed latent bugs,
and newly triggered assertions introduced by the patch series.

- REACHABILITY IS THE PRIMARY GATE:
  Fuzzing can only discover bugs in code that can actually execute in standard virtualized
  environments (GCE or QEMU, utilizing software-emulated devices like USB gadgets, netdev, tun/tap).
  If the modified code is structurally unreachable (see Section 2), it MUST NOT be fuzzed,
  regardless of whether it adds assertions or complex logic.

- DO NOT BLINDLY TRUST "NO FUNCTIONAL CHANGE" (NFCI) OR "REFACTORING" CLAIMS:
  Patch authors routinely label changes as "cleanups", "refactorings", or state
  "No functional change intended". Do NOT take these claims at face value.
  Code refactorings that rearrange logic, introduce helper functions, or alter state management
  in core subsystems frequently introduce subtle semantic shifts or uncover latent kernel bugs.
  If reachable executable code is modified or refactored, it MUST be fuzzed.

- NEW OR MODIFIED ASSERTIONS IN REACHABLE CODE MUST BE FUZZED:
  When a patch introduces or modifies runtime checks or assertions (e.g., WARN_ON*, VM_WARN_ON*,
  BUG_ON*, lockdep_assert*) in reachable code paths, it enforces new or stricter invariants.
  Even if the author believes the invariant always holds, fuzzing is essential to verify whether
  an unusual sequence of operations can violate it.

================================================================================
2. WHEN TO RETURN WorthFuzzing=false (NEGATIVE CRITERIA)
================================================================================
Return WorthFuzzing=false ONLY IF all modified code falls strictly into one or more of these categories:

- Non-kernel and non-executable changes:
  * Modifications to Documentation/, comments, or spelling fixes.
  * User-space directories, self-tests, samples, or scripts (e.g., tools/, samples/, scripts/, usr/)
    that do not affect the compiled kernel image (vmlinux) or kernel modules.
  * Purely decorative logging (e.g., message strings in pr_err, printk, dev_info) or tracepoints
    that do not alter control flow or data structures.
  * Build system or Kconfig changes that do not alter compiled C logic.
- Structurally unreachable hardware:
  * Vendor-specific PCIe switches, SmartNICs, or GPU drivers (e.g., mlxsw, pds_core, qed,
    ionic, amdgpu) requiring physical ASIC/PCIe cards not emulated in standard QEMU.
- Unreachable execution paths:
  * Driver teardown callbacks (.remove, .shutdown, pci_unregister_driver) executed only during
    physical PCI hot-unplug or manual sysfs driver unbinding.
  * Code paths exclusive to architectures other than the target architecture.

================================================================================
3. WHEN TO RETURN WorthFuzzing=true (POSITIVE CRITERIA)
================================================================================
Return WorthFuzzing=true whenever the patch touches reachable executable code, including:
- Core Subsystems:
  * Any logic modifications in memory management (mm/), synchronization/locking (kernel/locking/),
    BPF, scheduler, core networking, VFS, or syscall handling.
- Refactorings and Code Cleanups:
  * Any restructuring of reachable data structures, helper abstractions, or algorithm flows.
- Runtime Assertions and Defensive Checks:
  * Any introduction or alteration of assertions (WARN_ON*, VM_WARN_ON*, BUG_ON*, etc.) in reachable paths.
- Reachable Drivers and Protocols:
  * Drivers accessible via virtual buses (virtio, USB gadget, loopback, netlink, binder, sockets, etc.).

================================================================================
4. EXTRACTING FocusSymbols (PREVENTING DILUTION)
================================================================================
When WorthFuzzing=true, you must extract specific kernel functions into FocusSymbols to guide the fuzzer:

- AVOID UBIQUITOUS LIFECYCLE HOT-PATHS:
  Do NOT list generic, ubiquitous functions called by almost every program in the corpus
  (including, but not limited to: general memory allocators and deallocators, page fault
  and trap handlers, or core synchronization primitives; this is not an exhaustive list).
  Listing ubiquitous functions causes the fuzzer to classify thousands of unrelated tests as "focused",
  which severely dilutes fuzzing effort away from the actual changes.

- TARGET SPECIFIC FEATURE LOGIC AND ENTRYPOINTS:
  List functions that specifically implement the logic being added or altered, or direct API entrypoints
  for the subsystem feature under review.

- HANDLING STATIC INLINE FUNCTIONS IN HEADERS (.h):
  Compiler-inlined static functions (such as static inlines in mm/*.h or include/linux/*.h) lack
  distinct symbol addresses in vmlinux and cannot be targeted directly by symbol coverage filters.
  If the changes are primarily in static inline helpers, identify non-static, feature-specific caller
  functions in .c files that exercise them (avoiding ubiquitous lifecycle wrappers).

================================================================================
5. IDENTIFYING EnableConfigs
================================================================================
Identify any specific CONFIG_ options required to properly compile and reach the modified code:
- Inspect Kconfig files and #ifdef guards; do not make assumptions.
- Check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed.
- Strip any 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit a79cbb105c59817af32a22a00171f7bb91467654
Author: syz-cluster <triage@syzkaller.com>
Date:   Wed Sep 9 14:09:05 2026 +0000

    syz-cluster: applied patch under review

diff --git a/arch/x86/platform/efi/efi.c b/arch/x86/platform/efi/efi.c
index 0c39adb96b912..2b37b96a36e09 100644
--- a/arch/x86/platform/efi/efi.c
+++ b/arch/x86/platform/efi/efi.c
@@ -93,6 +93,9 @@ static const unsigned long * const efi_tables[] = {
 #ifdef CONFIG_UNACCEPTED_MEMORY
 	&efi.unaccepted,
 #endif
+#ifdef CONFIG_EFI_POISONED_MEMORY
+	&efi.poisoned_memory,
+#endif
 };
 
 u64 efi_setup;		/* efi setup_data physical address */
diff --git a/drivers/firmware/efi/Kconfig b/drivers/firmware/efi/Kconfig
index 29e0729299f5b..aafcd41bc0063 100644
--- a/drivers/firmware/efi/Kconfig
+++ b/drivers/firmware/efi/Kconfig
@@ -263,6 +263,14 @@ config EFI_COCO_SECRET
 	  virt/coco/efi_secret module to access the secrets, which in turn
 	  allows userspace programs to access the injected secrets.
 
+config EFI_POISONED_MEMORY
+	def_bool y
+	depends on EFI_STUB && MEMORY_FAILURE && 64BIT
+	help
+	  Record page frames that are hardware-poisoned while this kernel runs
+	  into an EFI configuration table, and honor that table early on the
+	  next kernel so a kexec does not hand known-bad RAM back out.
+
 config OVMF_DEBUG_LOG
 	bool "Expose OVMF firmware debug log via sysfs"
 	depends on EFI
diff --git a/drivers/firmware/efi/Makefile b/drivers/firmware/efi/Makefile
index 8efbcf699e4ff..05d0a490923e5 100644
--- a/drivers/firmware/efi/Makefile
+++ b/drivers/firmware/efi/Makefile
@@ -43,4 +43,5 @@ obj-$(CONFIG_EFI_EARLYCON)		+= earlycon.o
 obj-$(CONFIG_UEFI_CPER_ARM)		+= cper-arm.o
 obj-$(CONFIG_UEFI_CPER_X86)		+= cper-x86.o
 obj-$(CONFIG_UNACCEPTED_MEMORY)		+= unaccepted_memory.o
+obj-$(CONFIG_EFI_POISONED_MEMORY)	+= poison.o
 obj-$(CONFIG_TEE_STMM_EFI)		+= stmm/tee_stmm_efi.o
diff --git a/drivers/firmware/efi/efi.c b/drivers/firmware/efi/efi.c
index 6d987d7f97781..55b2ee53fc268 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
@@ -877,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/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
index 48f93f7758e9e..5cbe675491333 100644
--- a/drivers/firmware/efi/libstub/efi-stub-helper.c
+++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
@@ -774,3 +774,103 @@ void efi_remap_image(unsigned long image_base, unsigned alloc_size,
 			efi_warn("Failed to remap data region non-executable\n");
 	}
 }
+
+#ifdef CONFIG_EFI_POISONED_MEMORY
+/*
+ * Find the base and top of the memory, so, we can create the bitmap for
+ * the full range.
+ */
+static efi_status_t efi_get_ram_range(u64 *base, u64 *top)
+{
+	struct efi_boot_memmap *map __free(efi_pool) = NULL;
+	u64 ram_base = ULLONG_MAX, ram_top = 0;
+	efi_status_t status;
+	int i, nr_desc;
+
+	status = efi_get_memory_map(&map, false);
+	if (status != EFI_SUCCESS)
+		return status;
+
+	nr_desc = map->map_size / map->desc_size;
+	for (i = 0; i < nr_desc; i++) {
+		efi_memory_desc_t *d;
+
+		d = efi_memdesc_ptr((unsigned long)map->map, map->desc_size, i);
+		ram_base = min(ram_base, d->phys_addr);
+		ram_top = max(ram_top,
+			      d->phys_addr + d->num_pages * EFI_PAGE_SIZE);
+	}
+	if (!ram_top || ram_base == ULLONG_MAX)
+		return EFI_NOT_FOUND;
+
+	*base = round_down(ram_base, EFI_POISON_UNIT_SIZE);
+	*top = round_up(ram_top, EFI_POISON_UNIT_SIZE);
+
+	return EFI_SUCCESS;
+}
+
+/* The size of the bitmap */
+static u64 efi_poison_bitmap_size(u64 span)
+{
+	u64 bytes = DIV_ROUND_UP(DIV_ROUND_UP(span, EFI_POISON_UNIT_SIZE),
+				 BITS_PER_BYTE);
+
+	return round_up(bytes, sizeof(unsigned long));
+}
+
+static struct linux_efi_poisoned_memory *efi_poison_alloc(u64 phys_base,
+							  u64 bitmap_size)
+{
+	struct linux_efi_poisoned_memory *pm;
+	efi_status_t status;
+
+	status = efi_bs_call(allocate_pool, EFI_ACPI_RECLAIM_MEMORY,
+			     sizeof(*pm) + bitmap_size, (void **)&pm);
+	if (status != EFI_SUCCESS)
+		return NULL;
+
+	pm->version = 1;
+	pm->unit_size = EFI_POISON_UNIT_SIZE;
+	pm->phys_base = phys_base;
+	pm->size = bitmap_size;
+	memset(pm->bitmap, 0, bitmap_size);
+
+	return pm;
+}
+
+/* This needs to be done while boot service is still active */
+void install_poisoned_memory_table(void)
+{
+	efi_guid_t poisoned_memory_table_guid = LINUX_EFI_POISONED_MEMORY_TABLE_GUID;
+	struct linux_efi_poisoned_memory *pm;
+	u64 ram_base, ram_top, bitmap_size;
+	efi_status_t status;
+
+	/* A table installed by an earlier boot rides the system table across kexec. */
+	pm = get_efi_config_table(poisoned_memory_table_guid);
+	if (pm) {
+		if (pm->version != 1)
+			efi_err("Unknown version of poisoned-memory table\n");
+		return;
+	}
+
+	if (efi_get_ram_range(&ram_base, &ram_top) != EFI_SUCCESS) {
+		efi_err("Failed to size the poisoned-memory table!\n");
+		return;
+	}
+
+	bitmap_size = efi_poison_bitmap_size(ram_top - ram_base);
+	pm = efi_poison_alloc(ram_base, bitmap_size);
+	if (!pm) {
+		efi_err("Failed to allocate poisoned-memory table!\n");
+		return;
+	}
+
+	status = efi_bs_call(install_configuration_table,
+			     &poisoned_memory_table_guid, pm);
+	if (status != EFI_SUCCESS) {
+		efi_bs_call(free_pool, pm);
+		efi_err("Failed to install poisoned-memory config table!\n");
+	}
+}
+#endif
diff --git a/drivers/firmware/efi/libstub/efi-stub.c b/drivers/firmware/efi/libstub/efi-stub.c
index 235c9738da2d6..22a315e2814a1 100644
--- a/drivers/firmware/efi/libstub/efi-stub.c
+++ b/drivers/firmware/efi/libstub/efi-stub.c
@@ -179,6 +179,7 @@ efi_status_t efi_stub_common(efi_handle_t handle,
 			 EFI_RT_SUPPORTED_SET_VIRTUAL_ADDRESS_MAP);
 
 	install_memreserve_table();
+	install_poisoned_memory_table();
 
 	status = efi_boot_kernel(handle, image, image_addr, cmdline_ptr);
 
diff --git a/drivers/firmware/efi/libstub/efistub.h b/drivers/firmware/efi/libstub/efistub.h
index fd91fc15ec810..44436869c4efe 100644
--- a/drivers/firmware/efi/libstub/efistub.h
+++ b/drivers/firmware/efi/libstub/efistub.h
@@ -1169,6 +1169,12 @@ efi_enable_reset_attack_mitigation(void) { }
 
 void efi_retrieve_eventlog(void);
 
+#ifdef CONFIG_EFI_POISONED_MEMORY
+void install_poisoned_memory_table(void);
+#else
+static inline void install_poisoned_memory_table(void) { }
+#endif
+
 struct sysfb_display_info *alloc_primary_display(void);
 struct sysfb_display_info *__alloc_primary_display(void);
 void free_primary_display(struct sysfb_display_info *dpy);
diff --git a/drivers/firmware/efi/libstub/x86-stub.c b/drivers/firmware/efi/libstub/x86-stub.c
index 0bae0f06b6763..3136132b9628a 100644
--- a/drivers/firmware/efi/libstub/x86-stub.c
+++ b/drivers/firmware/efi/libstub/x86-stub.c
@@ -1024,6 +1024,8 @@ void __noreturn efi_stub_entry(efi_handle_t handle,
 
 	setup_unaccepted_memory();
 
+	install_poisoned_memory_table();
+
 	status = exit_boot(boot_params, handle);
 	if (status != EFI_SUCCESS) {
 		efi_err("exit_boot() failed!\n");
diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
new file mode 100644
index 0000000000000..e16d43f4438ee
--- /dev/null
+++ b/drivers/firmware/efi/poison.c
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Runtime side of the LINUX_EFI_POISONED_MEMORY table: one bit per
+ * EFI_POISON_UNIT_SIZE, set here as frames go bad, honored by the next kernel.
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2026 Breno Leitao <leitao@debian.org>
+ */
+
+#define pr_fmt(fmt) "efi: " fmt
+
+#include <linux/bitmap.h>
+#include <linux/efi.h>
+#include <linux/io.h>
+#include <linux/log2.h>
+#include <linux/memblock.h>
+#include <linux/mm.h>
+#include <linux/overflow.h>
+
+static bool __init
+efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)
+{
+	u64 nbits;
+
+	/* Whole words, and a bit count that can be taken without wrapping. */
+	if (!pm->size || !IS_ALIGNED(pm->size, sizeof(unsigned long)) ||
+	    check_mul_overflow(pm->size, (u64)BITS_PER_BYTE, &nbits))
+		return false;
+
+	if (pm->unit_size < PAGE_SIZE || !is_power_of_2(pm->unit_size))
+		return false;
+
+	return IS_ALIGNED(pm->phys_base, pm->unit_size);
+}
+
+/* The table may come from an earlier kernel, so vet it before using it. */
+static bool __init
+efi_poison_table_valid(const struct linux_efi_poisoned_memory *pm)
+{
+	if (pm->version != 1) {
+		pr_warn("Ignoring poisoned-memory table with version %u\n",
+			pm->version);
+		return false;
+	}
+
+	if (!efi_poison_geometry_valid(pm)) {
+		pr_warn("Ignoring malformed poisoned-memory table\n");
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Vet the inherited table and hand its pages to memblock, the way the
+ * unaccepted memory table is handled. It is EFI ACPI reclaim memory, which
+ * becomes E820_TYPE_ACPI and would otherwise stay out of the direct map, and
+ * touching it then faults. Called from efi_config_parse_tables(), so
+ * everything later can reach it with efi_poisoned_memory().
+ */
+void __init efi_poisoned_memory_reserve(void)
+{
+	struct linux_efi_poisoned_memory *pm;
+	phys_addr_t start, end;
+
+	if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
+		return;
+
+	pm = early_memremap(efi.poisoned_memory, sizeof(*pm));
+	if (!pm) {
+		pr_warn("Could not map poisoned-memory table\n");
+		efi.poisoned_memory = EFI_INVALID_TABLE_ADDR;
+		return;
+	}
+
+	if (!efi_poison_table_valid(pm)) {
+		efi.poisoned_memory = EFI_INVALID_TABLE_ADDR;
+		early_memunmap(pm, sizeof(*pm));
+		return;
+	}
+
+	start = PAGE_ALIGN_DOWN(efi.poisoned_memory);
+	end = PAGE_ALIGN(efi.poisoned_memory + sizeof(*pm) + pm->size);
+	early_memunmap(pm, sizeof(*pm));
+
+	memblock_add(start, end - start);
+	memblock_reserve(start, end - start);
+}
+
+/* The table, vetted at parse time, or NULL if this boot has none. */
+static struct linux_efi_poisoned_memory *efi_poisoned_memory(void)
+{
+	if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
+		return NULL;
+
+	return phys_to_virt(efi.poisoned_memory);
+}
+
+/* Does the range cover a unit an earlier kernel recorded as bad? */
+bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size)
+{
+	struct linux_efi_poisoned_memory *pm = efi_poisoned_memory();
+	u64 first, last, nbits;
+
+	if (!pm)
+		return false;
+
+	nbits = pm->size * BITS_PER_BYTE;
+
+	if (start + size <= pm->phys_base)
+		return false;
+	if (start < pm->phys_base)
+		start = pm->phys_base;
+
+	first = (start - pm->phys_base) / pm->unit_size;
+	if (first >= nbits)
+		return false;
+
+	last = (start + size - 1 - pm->phys_base) / pm->unit_size;
+	last = min(last, nbits - 1);
+
+	return find_next_bit(pm->bitmap, last + 1, first) <= last;
+}
+
+/*
+ * A bit is never cleared: it stands for a whole EFI_POISON_UNIT_SIZE, so an
+ * unpoison cannot tell whether the unit as a whole is good again.
+ */
+void efi_hwpoison_record_pfn(unsigned long pfn)
+{
+	struct linux_efi_poisoned_memory *pm = efi_poisoned_memory();
+	phys_addr_t addr = PFN_PHYS(pfn);
+	u64 unit;
+
+	if (!pm || addr < pm->phys_base)
+		return;
+
+	unit = (addr - pm->phys_base) / pm->unit_size;
+	if (unit < pm->size * BITS_PER_BYTE)
+		set_bit(unit, pm->bitmap);
+}
diff --git a/include/linux/efi.h b/include/linux/efi.h
index c35446a0b66fa..56402fdccd114 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,25 @@ 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
+
+#ifdef CONFIG_EFI_POISONED_MEMORY
+void __init efi_poisoned_memory_reserve(void);
+void efi_hwpoison_record_pfn(unsigned long pfn);
+#else
+static inline void efi_poisoned_memory_reserve(void) { }
+static inline void efi_hwpoison_record_pfn(unsigned long pfn) { }
+#endif
+
 void __init efi_arch_mem_reserve(phys_addr_t addr, u64 size);
 
 /*
diff --git a/include/linux/mm.h b/include/linux/mm.h
index 274fa880077c5..9d9f2e8fdc136 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -5225,6 +5225,7 @@ extern const struct attribute_group memory_failure_attr_group;
 extern void memory_failure_queue(unsigned long pfn, int flags);
 void num_poisoned_pages_inc(unsigned long pfn);
 void num_poisoned_pages_sub(unsigned long pfn, long i);
+void __meminit hwpoison_boot_page(struct page *page);
 phys_addr_t range_first_hwpoison(phys_addr_t start, unsigned long size);
 phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size);
 #else
@@ -5232,6 +5233,10 @@ static inline void memory_failure_queue(unsigned long pfn, int flags)
 {
 }
 
+static inline void hwpoison_boot_page(struct page *page)
+{
+}
+
 static inline void num_poisoned_pages_inc(unsigned long pfn)
 {
 }
@@ -5387,6 +5392,20 @@ static inline bool pfn_is_unaccepted_memory(unsigned long pfn)
 	return range_contains_unaccepted_memory(pfn << PAGE_SHIFT, PAGE_SIZE);
 }
 
+#ifdef CONFIG_EFI_POISONED_MEMORY
+
+bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size);
+
+#else
+
+static inline bool range_contains_poisoned_memory(phys_addr_t start,
+						  unsigned long size)
+{
+	return false;
+}
+
+#endif
+
 void vma_pgtable_walk_begin(struct vm_area_struct *vma);
 void vma_pgtable_walk_end(struct vm_area_struct *vma);
 
diff --git a/mm/memory-failure.c b/mm/memory-failure.c
index a2ca8df501cae..f6afdb2a89a94 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>
@@ -136,6 +137,21 @@ phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size)
 	return range_hwpoison(start, size, false);
 }
 
+static void update_per_node_mf_stats(unsigned long pfn, enum mf_result result);
+
+/* Not num_poisoned_pages_inc(): its per block half divides by zero this early. */
+void __meminit hwpoison_boot_page(struct page *page)
+{
+	if (PageHWPoison(page))
+		return;
+
+	SetPageHWPoison(page);
+	set_page_count(page, 1);
+	/* The page has been completely isolated == MF_RECOVERED */
+	update_per_node_mf_stats(page_to_pfn(page), MF_RECOVERED);
+	atomic_long_inc(&num_poisoned_pages);
+}
+
 /**
  * MF_ATTR_RO - Create sysfs entry for each memory failure statistics.
  * @_name: name of the file in the per NUMA sysfs directory.
@@ -1326,6 +1342,8 @@ static int action_result(unsigned long pfn, enum mf_action_page_type type,
 	if (type != MF_MSG_ALREADY_POISONED && type != MF_MSG_PFN_MAP) {
 		num_poisoned_pages_inc(pfn);
 		update_per_node_mf_stats(pfn, result);
+		/* Only hard offlines are carried over to the next kernel. */
+		efi_hwpoison_record_pfn(pfn);
 	}
 
 	pr_err("%#lx: recovery action for %s: %s\n",
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index 404896b53003e..9e2ce833fd409 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -1579,6 +1579,19 @@ static void __free_pages_ok(struct page *page, unsigned int order,
 		free_one_page(zone, page, pfn, order, fpi_flags);
 }
 
+/* Flag the frames an earlier kernel recorded as bad. */
+static void __meminit poison_block(struct page *page, unsigned int order)
+{
+	unsigned long i, nr_pages = 1UL << order;
+
+	for (i = 0; i < nr_pages; i++) {
+		struct page *p = page + i;
+
+		if (range_contains_poisoned_memory(page_to_phys(p), PAGE_SIZE))
+			hwpoison_boot_page(p);
+	}
+}
+
 void __meminit __free_pages_core(struct page *page, unsigned int order,
 		enum meminit_context context)
 {
@@ -1613,6 +1626,18 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,
 		atomic_long_add(nr_pages, &page_zone(page)->managed_pages);
 	}
 
+	/* First: a block parked by __free_unaccepted() never returns here. */
+	if (range_contains_poisoned_memory(page_to_phys(page),
+					   PAGE_SIZE << order)) {
+		poison_block(page, order);
+		/*
+		 * TODO: free the frames in the block that are not poisoned.
+		 * They stay out of the allocator and still count in
+		 * managed_pages, so a unit costs up to a block.
+		 */
+		return;
+	}
+
 	if (page_contains_unaccepted(page, order)) {
 		if (order == MAX_PAGE_ORDER && __free_unaccepted(page))
 			return;

3/2 2026/09/09 14:09 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 10199
							output: 30
							thoughts: 387
						

Thoughts:
**My Initial Assessment of the Patch Series**

Okay, let's dive into this patch series. My initial focus is going to be on the triage process, making sure we have a solid understanding of how it's being evaluated. I need to understand the core principles, and it seems reachability is the primary concern – can the modified code actually execute in a typical virtualized environment like GCE or QEMU? That's a reasonable starting point.

Next, I need to get a handle on the extent of the changes. The files modified are: `arch/x86/platform/efi/efi.c`, `drivers/firmware/efi/Kconfig`, `drivers/firmware/efi/Makefile`, `drivers/firmware/efi/efi.c`, `drivers/firmware/efi/libstub/efi-stub-helper.c`, `drivers/firmware/efi/libstub/efi-stub.c`, `drivers/firmware/efi/libstub/efistub.h`, `drivers/firmware/efi/libstub/x86-stub.c`, `drivers/firmware/efi/poison.c`, `include/linux/efi.h`, `include/linux/mm.h`, `mm/memory-failure.c`, and `mm/page_alloc.c`. That's a fairly extensive touch, mostly concentrated around EFI and memory management.

Now, the critical question: what parts of this code are active during runtime versus boot time? And what syscalls or interfaces can *reach* this code? I need to understand the attack surface.

Specifically, I need to look at `efi_hwpoison_record_pfn(pfn)`. I see it's called from `action_result()` in `mm/memory-failure.c`. That means I have to trace how `mm/memory-failure.c` gets triggered at runtime. I need to verify how `memory_failure` or `action_result` are called or exposed. Can a `madvise(MADV_HWPOISON)` call, or maybe something through debugfs or sysfs, trigger this code path? Grep is going to be my friend here.



4/2 2026/09/09 14:09 tool grepper
0m Args:
{
  "Expression": "action_result",
  "PathPrefix": "mm/memory-failure.c"
}

Results:
{
  "Output": "mm/memory-failure.c=1324=static bool panic_on_unrecoverable_mf(enum mf_action_page_type type,\n--\nmm/memory-failure.c-1336- */\nmm/memory-failure.c:1337:static int action_result(unsigned long pfn, enum mf_action_page_type type,\nmm/memory-failure.c-1338-\t\t\t enum mf_result result)\n--\nmm/memory-failure.c=1358=static int page_action(struct page_state *ps, struct page *p,\n--\nmm/memory-failure.c-1370-\nmm/memory-failure.c:1371:\treturn action_result(pfn, ps-\u003etype, result);\nmm/memory-failure.c-1372-}\n--\nmm/memory-failure.c=2139=static int try_memory_failure_hugetlb(unsigned long pfn, int flags)\n--\nmm/memory-failure.c-2156-\t\t}\nmm/memory-failure.c:2157:\t\treturn action_result(pfn, MF_MSG_GET_HWPOISON, MF_IGNORED);\nmm/memory-failure.c-2158-\tcase MF_HUGETLB_FOLIO_PRE_POISONED:\n--\nmm/memory-failure.c-2163-\t\tif (res == MF_HUGETLB_PAGE_PRE_POISONED)\nmm/memory-failure.c:2164:\t\t\taction_result(pfn, MF_MSG_ALREADY_POISONED, MF_FAILED);\nmm/memory-failure.c-2165-\t\telse\nmm/memory-failure.c:2166:\t\t\taction_result(pfn, MF_MSG_HUGE, MF_FAILED);\nmm/memory-failure.c-2167-\t\treturn rv;\n--\nmm/memory-failure.c-2197-\t\t}\nmm/memory-failure.c:2198:\t\treturn action_result(pfn, MF_MSG_FREE_HUGE, res);\nmm/memory-failure.c-2199-\t}\n--\nmm/memory-failure.c-2204-\t\tfolio_unlock(folio);\nmm/memory-failure.c:2205:\t\treturn action_result(pfn, MF_MSG_UNMAP_FAILED, MF_FAILED);\nmm/memory-failure.c-2206-\t}\n--\nmm/memory-failure.c=2232=static int memory_failure_dev_pagemap(unsigned long pfn, int flags,\n--\nmm/memory-failure.c-2259-\tif (rc != -EOPNOTSUPP)\nmm/memory-failure.c:2260:\t\taction_result(pfn, MF_MSG_DAX, rc ? MF_FAILED : MF_RECOVERED);\nmm/memory-failure.c-2261-\treturn rc;\n--\nmm/memory-failure.c=2378=static int memory_failure_pfn(unsigned long pfn, int flags)\n--\nmm/memory-failure.c-2402-\t\tif (!mf_handled)\nmm/memory-failure.c:2403:\t\t\treturn action_result(pfn, MF_MSG_PFN_MAP, MF_IGNORED);\nmm/memory-failure.c-2404-\t}\n--\nmm/memory-failure.c-2415-\nmm/memory-failure.c:2416:\treturn action_result(pfn, MF_MSG_PFN_MAP, MF_RECOVERED);\nmm/memory-failure.c-2417-}\n--\nmm/memory-failure.c=2444=int memory_failure(unsigned long pfn, int flags)\n--\nmm/memory-failure.c-2502-\t\t\tput_page(p);\nmm/memory-failure.c:2503:\t\taction_result(pfn, MF_MSG_ALREADY_POISONED, MF_FAILED);\nmm/memory-failure.c-2504-\t\tgoto unlock_mutex;\n--\nmm/memory-failure.c-2533-\t\t\t}\nmm/memory-failure.c:2534:\t\t\tres = action_result(pfn, MF_MSG_BUDDY, res);\nmm/memory-failure.c-2535-\t\t} else {\nmm/memory-failure.c:2536:\t\t\tres = action_result(pfn, MF_MSG_KERNEL_HIGH_ORDER, MF_IGNORED);\nmm/memory-failure.c-2537-\t\t}\n--\nmm/memory-failure.c-2547-\t\t */\nmm/memory-failure.c:2548:\t\tres = action_result(pfn, MF_MSG_KERNEL, MF_IGNORED);\nmm/memory-failure.c-2549-\t\tgoto unlock_mutex;\n--\nmm/memory-failure.c-2551-\t\t/* Transient lifecycle race with the page allocator. */\nmm/memory-failure.c:2552:\t\tres = action_result(pfn, MF_MSG_GET_HWPOISON, MF_IGNORED);\nmm/memory-failure.c-2553-\t\tgoto unlock_mutex;\n--\nmm/memory-failure.c-2599-\t\t\tput_page(p);\nmm/memory-failure.c:2600:\t\t\taction_result(pfn, MF_MSG_UNSPLIT_THP, MF_FAILED);\nmm/memory-failure.c-2601-\t\t\tgoto unlock_mutex;\n--\nmm/memory-failure.c-2653-\tif (!hwpoison_user_mappings(folio, p, pfn, flags)) {\nmm/memory-failure.c:2654:\t\tres = action_result(pfn, MF_MSG_UNMAP_FAILED, MF_FAILED);\nmm/memory-failure.c-2655-\t\tgoto unlock_page;\n--\nmm/memory-failure.c-2662-\t    folio-\u003emapping == NULL) {\nmm/memory-failure.c:2663:\t\tres = action_result(pfn, MF_MSG_TRUNCATED_LRU, MF_IGNORED);\nmm/memory-failure.c-2664-\t\tgoto unlock_page;\n"
}

5/2 2026/09/09 14:09 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 12656
							output: 28
							thoughts: 59
						

Thoughts:
Okay, let's break this down.

**Diving into the `madvise(MADV_HWPOISON)` Rabbit Hole**

Right, so I'm looking at `madvise(MADV_HWPOISON)` and how it works under the hood. My immediate thought is, "Okay, that call seems to lead to `madvise_inject_error()`, which is interesting because that directly calls `memory_failure()`." Interesting, so `madvise` is using the same infrastructure as other memory error injection mechanisms.

I need to understand what that injection is. Let's see how `madvise` actually calls this `madvise_inject_error` function. I will need to look deeper into `madvise` to see where `MADV_HWPOISON` is handled and how it decides to inject an error. Alternatively, perhaps it's more direct and I should look at `madvise_inject_error` and see where it's being called, which might give me more context. Both approaches are valid.



6/2 2026/09/09 14:09 tool grepper
0m Args:
{
  "Expression": "MADV_HWPOISON",
  "PathPrefix": "mm/"
}

Results:
{
  "Output": "mm/madvise.c=1504=static bool is_memory_failure(struct madvise_behavior *madv_behavior)\n--\nmm/madvise.c-1506-\tswitch (madv_behavior-\u003ebehavior) {\nmm/madvise.c:1507:\tcase MADV_HWPOISON:\nmm/madvise.c-1508-\tcase MADV_SOFT_OFFLINE:\n--\nmm/madvise.c=1530=madvise_behavior_valid(int behavior)\n--\nmm/madvise.c-1563-\tcase MADV_SOFT_OFFLINE:\nmm/madvise.c:1564:\tcase MADV_HWPOISON:\nmm/madvise.c-1565-#endif\n--\nmm/madvise.c=1899=static int madvise_do_behavior(unsigned long start, size_t len_in,\n--\nmm/madvise.c-1955- *  MADV_KEEPONFORK - undo the effect of MADV_WIPEONFORK\nmm/madvise.c:1956: *  MADV_HWPOISON - trigger memory error handler as if the given memory range\nmm/madvise.c-1957- *\t\twere corrupted by unrecoverable hardware memory failure.\n"
}

7/2 2026/09/09 14:09 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 13314
							output: 22
							thoughts: 62
						

Thoughts:
**Investigating Memory Poisoning in the Kernel**

Okay, so I need to track down the usage of `range_contains_poisoned_memory()`. My initial thought is to check two likely locations: `mm/page_alloc.c` and `drivers/firmware/efi/poison.c`. Those seem like the most relevant spots given the function's name and general memory management concepts.

But, I need to know *where* it's actually being called. No sense in just staring at the function definition. So, let's fire up `grepper`. That should quickly uncover all the call sites across the codebase. With `grepper`, I should be able to get a comprehensive list of every place this function is used and then narrow it down based on the context.



8/2 2026/09/09 14:09 tool grepper
0m Args:
{
  "Expression": "range_contains_poisoned_memory"
}

Results:
{
  "Output": "drivers/firmware/efi/poison.c=91=static struct linux_efi_poisoned_memory *efi_poisoned_memory(void)\n--\ndrivers/firmware/efi/poison.c-99-/* Does the range cover a unit an earlier kernel recorded as bad? */\ndrivers/firmware/efi/poison.c:100:bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size)\ndrivers/firmware/efi/poison.c-101-{\n--\ninclude/linux/mm.h=5390=static inline bool pfn_is_unaccepted_memory(unsigned long pfn)\n--\ninclude/linux/mm.h-5396-\ninclude/linux/mm.h:5397:bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size);\ninclude/linux/mm.h-5398-\n--\ninclude/linux/mm.h-5400-\ninclude/linux/mm.h:5401:static inline bool range_contains_poisoned_memory(phys_addr_t start,\ninclude/linux/mm.h-5402-\t\t\t\t\t\t  unsigned long size)\n--\nmm/page_alloc.c=1583=static void __meminit poison_block(struct page *page, unsigned int order)\n--\nmm/page_alloc.c-1589-\nmm/page_alloc.c:1590:\t\tif (range_contains_poisoned_memory(page_to_phys(p), PAGE_SIZE))\nmm/page_alloc.c-1591-\t\t\thwpoison_boot_page(p);\n--\nmm/page_alloc.c=1595=void __meminit __free_pages_core(struct page *page, unsigned int order,\n--\nmm/page_alloc.c-1629-\t/* First: a block parked by __free_unaccepted() never returns here. */\nmm/page_alloc.c:1630:\tif (range_contains_poisoned_memory(page_to_phys(page),\nmm/page_alloc.c-1631-\t\t\t\t\t   PAGE_SIZE \u003c\u003c order)) {\n"
}

9/2 2026/09/09 14:09 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 14104
							output: 20
							thoughts: 601
						

Thoughts:
**My Current Analysis of the EFI Hardware Poisoning Logic**

Okay, let's break this down systematically. I need to understand how `efi_hwpoison_record_pfn` works and when it gets called, especially in the context of memory failure handling. First, I see it's called within `action_result()` in `mm/memory-failure.c`, specifically when the memory failure type isn't already poisoned or a PFN map. This makes sense; it's recording the failure for persistent tracking across reboots in those cases.

Now, let's dive into what `efi_hwpoison_record_pfn` itself does. Looking at `drivers/firmware/efi/poison.c`, it seems to be setting a bit in a bitmap within the `linux_efi_poisoned_memory` structure. This structure's allocation and initialization is what I'm really curious about. It appears that it's using the Physical Frame Number (PFN) to calculate a unit within that bitmap and then sets the corresponding bit. The check for `pm` being NULL or the address being outside the expected range is crucial, which could be an issue if we have different EFI configurations.

A thought: What if EFI is enabled, like in a QEMU/OVMF environment and `CONFIG_EFI_POISONED_MEMORY` is also enabled? Or, if a tool like syzkaller or a userspace application uses `madvise(..., MADV_HWPOISON)` or intentionally injects memory failures? In standard syzkaller runs, does QEMU even boot with UEFI/EFI? Even without UEFI, if `efi.poisoned_memory` is `EFI_INVALID_TABLE_ADDR`, then `efi_poisoned_memory()` returns NULL! And that seems to be the case when looking at `efi_hwpoison_record_pfn`.

Wait, can `madvise(..., MADV_HWPOISON)` trigger `efi_hwpoison_record_pfn`? Yes, because `action_result` is responsible for calling it, which receives the PFN from memory failure handling logic.

Now, there's `hwpoison_boot_page()` in `memory-failure.c`. But it's only called during early boot within `__free_pages_core()`. Hang on a second, though, because `__free_pages_core` also gets called during memory hotplug. That means this entire sequence of events could play out during hotplugging too, making this even more complicated to verify. I need to check where else `__free_pages_core` is called. It would appear this could cause a potential race and/or affect what we were trying to determine.



10/2 2026/09/09 14:09 tool grepper
0m Args:
{
  "Expression": "__free_pages_core"
}

Results:
{
  "Output": "mm/kmsan/init.c=168=static void do_collection(void)\n--\nmm/kmsan/init.c-176-\t\tkmsan_setup_meta(page, shadow, origin, collect.order);\nmm/kmsan/init.c:177:\t\t__free_pages_core(page, collect.order, MEMINIT_EARLY);\nmm/kmsan/init.c-178-\t}\n--\nmm/memory_hotplug.c=648=void generic_online_page(struct page *page, unsigned int order)\nmm/memory_hotplug.c-649-{\nmm/memory_hotplug.c:650:\t__free_pages_core(page, order, MEMINIT_HOTPLUG);\nmm/memory_hotplug.c-651-}\n--\nmm/mm_init.c=1957=static void __init deferred_free_pages(unsigned long pfn,\n--\nmm/mm_init.c-1972-\tif (nr_pages == MAX_ORDER_NR_PAGES \u0026\u0026 IS_MAX_ORDER_ALIGNED(pfn)) {\nmm/mm_init.c:1973:\t\t__free_pages_core(page, MAX_PAGE_ORDER, MEMINIT_EARLY);\nmm/mm_init.c-1974-\t\treturn;\n--\nmm/mm_init.c-1980-\tfor (i = 0; i \u003c nr_pages; i++)\nmm/mm_init.c:1981:\t\t__free_pages_core(page + i, 0, MEMINIT_EARLY);\nmm/mm_init.c-1982-}\n--\nmm/mm_init.c=2452=void __init memblock_free_pages(unsigned long pfn, unsigned int order)\n--\nmm/mm_init.c-2469-\tclear_page_tag_ref(page);\nmm/mm_init.c:2470:\t__free_pages_core(page, order, MEMINIT_EARLY);\nmm/mm_init.c-2471-}\n--\nmm/page_alloc.c=1583=static void __meminit poison_block(struct page *page, unsigned int order)\n--\nmm/page_alloc.c-1594-\nmm/page_alloc.c:1595:void __meminit __free_pages_core(struct page *page, unsigned int order,\nmm/page_alloc.c-1596-\t\tenum meminit_context context)\n--\nmm/page_alloc.h=214=static inline struct page *pageblock_pfn_to_page(unsigned long start_pfn,\n--\nmm/page_alloc.h-222-\nmm/page_alloc.h:223:extern void __free_pages_core(struct page *page, unsigned int order,\nmm/page_alloc.h-224-\t\tenum meminit_context context);\n"
}

11/2 2026/09/09 14:09 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 16044
							output: 20
							thoughts: 144
						

Thoughts:
**Analyzing Memory Hotplug and Potential Failure Injection**

Okay, so I'm looking at how memory hotplug is handled within the kernel, specifically focusing on the `generic_online_page()` function in `mm/memory_hotplug.c`. This is where the core memory deallocation using `__free_pages_core()` happens, flagged with `MEMINIT_HOTPLUG`. My immediate thought is to check the various triggers for this process, which, as I recall, come through sysfs interfaces like `/sys/devices/system/memory/memoryX/state`, or perhaps via `dax_kmem` or even `virtio-mem` in some virtualized environments. I need to understand the control flow.

Now, I'm getting interested in how this interacts with memory failure scenarios.  I suspect there might be a connection between hotplug and failure injection, potentially through mechanisms like `madvise(MADV_HWPOISON)`. Therefore, I need to investigate where `action_result` is called, specifically within `mm/memory-failure.c`. I need to review all the call paths to `action_result` from that file, including any that originate from `madvise_inject_error`.  I need to understand what those calls do and how they might relate to memory hotplug events and potential hardware issues. I'm looking for a clear path connecting these seemingly disparate areas.  This investigation should illuminate any potential vulnerabilities or unexpected behaviors during hotplug operations.



12/2 2026/09/09 14:09 tool grepper
0m Args:
{
  "Expression": "MADV_HWPOISON"
}

Results:
{
  "Output": "Documentation/mm/hwpoison.rst=106=Testing\n--\nDocumentation/mm/hwpoison.rst-108-\nDocumentation/mm/hwpoison.rst:109:* madvise(MADV_HWPOISON, ....) (as root) - Poison a page in the\nDocumentation/mm/hwpoison.rst-110-  process for testing\n--\nDocumentation/translations/zh_CN/mm/hwpoison.rst=96=PR_MCE_KILL_GET\n--\nDocumentation/translations/zh_CN/mm/hwpoison.rst-101-\nDocumentation/translations/zh_CN/mm/hwpoison.rst:102:* madvise(MADV_HWPOISON, ....) (as root) - 在测试过程中Poison一个页面\nDocumentation/translations/zh_CN/mm/hwpoison.rst-103-\n--\narch/mips/include/uapi/asm/mman.h-85-#define MADV_UNMERGEABLE 13\t\t/* KSM may not merge identical pages */\narch/mips/include/uapi/asm/mman.h:86:#define MADV_HWPOISON\t 100\t\t/* poison a page for testing */\narch/mips/include/uapi/asm/mman.h-87-\n--\narch/parisc/include/uapi/asm/mman.h-74-\narch/parisc/include/uapi/asm/mman.h:75:#define MADV_HWPOISON     100\t\t/* poison a page for testing */\narch/parisc/include/uapi/asm/mman.h-76-#define MADV_SOFT_OFFLINE 101\t\t/* soft offline page for testing */\n--\ninclude/uapi/asm-generic/mman-common.h-55-#define MADV_DOFORK\t11\t\t/* do inherit across fork */\ninclude/uapi/asm-generic/mman-common.h:56:#define MADV_HWPOISON\t100\t\t/* poison a page for testing */\ninclude/uapi/asm-generic/mman-common.h-57-#define MADV_SOFT_OFFLINE 101\t\t/* soft offline page for testing */\n--\nmm/madvise.c=1504=static bool is_memory_failure(struct madvise_behavior *madv_behavior)\n--\nmm/madvise.c-1506-\tswitch (madv_behavior-\u003ebehavior) {\nmm/madvise.c:1507:\tcase MADV_HWPOISON:\nmm/madvise.c-1508-\tcase MADV_SOFT_OFFLINE:\n--\nmm/madvise.c=1530=madvise_behavior_valid(int behavior)\n--\nmm/madvise.c-1563-\tcase MADV_SOFT_OFFLINE:\nmm/madvise.c:1564:\tcase MADV_HWPOISON:\nmm/madvise.c-1565-#endif\n--\nmm/madvise.c=1899=static int madvise_do_behavior(unsigned long start, size_t len_in,\n--\nmm/madvise.c-1955- *  MADV_KEEPONFORK - undo the effect of MADV_WIPEONFORK\nmm/madvise.c:1956: *  MADV_HWPOISON - trigger memory error handler as if the given memory range\nmm/madvise.c-1957- *\t\twere corrupted by unrecoverable hardware memory failure.\n--\ntools/arch/alpha/include/uapi/asm/mman.h-37-#define PROT_WRITE\t0x2\ntools/arch/alpha/include/uapi/asm/mman.h:38:/* MADV_HWPOISON is undefined on alpha, fix it for perf */\ntools/arch/alpha/include/uapi/asm/mman.h:39:#define MADV_HWPOISON\t100\ntools/arch/alpha/include/uapi/asm/mman.h-40-/* MADV_SOFT_OFFLINE is undefined on alpha, fix it for perf */\n--\ntools/arch/mips/include/uapi/asm/mman.h-10-#define MADV_HUGEPAGE\t14\ntools/arch/mips/include/uapi/asm/mman.h:11:#define MADV_HWPOISON\t 100\ntools/arch/mips/include/uapi/asm/mman.h-12-#define MADV_MERGEABLE\t 12\n--\ntools/arch/parisc/include/uapi/asm/mman.h-37-#define PROT_WRITE\t0x2\ntools/arch/parisc/include/uapi/asm/mman.h:38:#define MADV_HWPOISON\t100\ntools/arch/parisc/include/uapi/asm/mman.h-39-#define MADV_SOFT_OFFLINE 101\n--\ntools/arch/xtensa/include/uapi/asm/mman.h-37-#define PROT_WRITE\t0x2\ntools/arch/xtensa/include/uapi/asm/mman.h:38:/* MADV_HWPOISON is undefined on xtensa, fix it for perf */\ntools/arch/xtensa/include/uapi/asm/mman.h:39:#define MADV_HWPOISON\t100\ntools/arch/xtensa/include/uapi/asm/mman.h-40-/* MADV_SOFT_OFFLINE is undefined on xtensa, fix it for perf */\n--\ntools/include/uapi/asm-generic/mman-common.h-55-#define MADV_DOFORK\t11\t\t/* do inherit across fork */\ntools/include/uapi/asm-generic/mman-common.h:56:#define MADV_HWPOISON\t100\t\t/* poison a page for testing */\ntools/include/uapi/asm-generic/mman-common.h-57-#define MADV_SOFT_OFFLINE 101\t\t/* soft offline page for testing */\n--\ntools/testing/selftests/mm/hugetlb-read-hwpoison.c=176=test_hugetlb_read_hwpoison(int fd, size_t len, size_t wr_chunk_size,\n--\ntools/testing/selftests/mm/hugetlb-read-hwpoison.c-205-\thwp_addr = filemap + len / 2 + pagesize;\ntools/testing/selftests/mm/hugetlb-read-hwpoison.c:206:\tif (madvise(hwp_addr, pagesize, MADV_HWPOISON) \u003c 0) {\ntools/testing/selftests/mm/hugetlb-read-hwpoison.c:207:\t\tksft_perror(\"MADV_HWPOISON failed\");\ntools/testing/selftests/mm/hugetlb-read-hwpoison.c-208-\t\tgoto unmap;\n--\ntools/testing/selftests/mm/memory-failure.c=58=static int madv_hard_inject(FIXTURE_DATA(memory_failure) * self, void *vaddr)\ntools/testing/selftests/mm/memory-failure.c-59-{\ntools/testing/selftests/mm/memory-failure.c:60:\treturn madvise(vaddr, self-\u003epage_size, MADV_HWPOISON);\ntools/testing/selftests/mm/memory-failure.c-61-}\n"
}

13/2 2026/09/09 14:09 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 18377
							output: 21
							thoughts: 23
						

Thoughts:
Here's my summary, as if I were thinking these thoughts:

**Diving into `madvise_inject_error`**

Alright, time to dig into this. I'm going to head straight for `mm/madvise.c` and specifically, I'll be looking at the `madvise_inject_error` function. This one is key, and I know it. I need to understand how this is handled, the mechanics of how we're injecting errors using madvise. I want to see the error injection behavior details. This is likely where the rubber meets the road when it comes to understanding exactly how this particular control path behaves. Let's see what the code has to say.



14/2 2026/09/09 14:09 tool grepper
0m Args:
{
  "Expression": "madvise_inject_error"
}

Results:
{
  "Output": "mm/madvise.c=1350=static int madvise_vma_behavior(struct madvise_behavior *madv_behavior)\n--\nmm/madvise.c-1458- */\nmm/madvise.c:1459:static int madvise_inject_error(struct madvise_behavior *madv_behavior)\nmm/madvise.c-1460-{\n--\nmm/madvise.c=1504=static bool is_memory_failure(struct madvise_behavior *madv_behavior)\n--\nmm/madvise.c-1516-\nmm/madvise.c:1517:static int madvise_inject_error(struct madvise_behavior *madv_behavior)\nmm/madvise.c-1518-{\n--\nmm/madvise.c=1899=static int madvise_do_behavior(unsigned long start, size_t len_in,\n--\nmm/madvise.c-1908-\t\trange-\u003eend = start + len_in;\nmm/madvise.c:1909:\t\treturn madvise_inject_error(madv_behavior);\nmm/madvise.c-1910-\t}\n"
}

15/2 2026/09/09 14:09 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 18891
							output: 36
							thoughts: 15
						

Thoughts:
Here's my attempt at summarizing the thought process, tailored for an expert audience and written in the first person:

**Diving into `madvise_inject_error`**

Okay, time to dissect the `madvise_inject_error` implementation. I need to get a clear picture of how this function is supposed to work and, more importantly, how it might deviate from expected behavior or interact with the kernel in unexpected ways. My focus is on potential edge cases, race conditions, and any subtle interactions with the memory management unit that could lead to vulnerabilities or subtle bugs. I'm going to pay close attention to the error injection logic itself – what triggers it, how precisely is the error communicated or simulated, and are there any obvious (or non-obvious) flaws in the error handling? I will look for any areas that potentially bypass intended memory protection. I want to see how this interacts with the page fault handling paths, and consider potential race conditions with other operations that manipulate the same memory regions. Let's see... where does the kernel actually inject the "error" after `madvise`? Are there any obvious security considerations related to the error state? Is the error truly an error, or a trigger that exposes the underlying memory allocation mechanisms? Let's get started.



16/2 2026/09/09 14:09 tool read-file
0m Args:
{
  "File": "mm/madvise.c",
  "FirstLine": 1455,
  "LineCount": 40
}

Results:
{
  "Contents": "1455:\t#ifdef CONFIG_MEMORY_FAILURE\n1456:\t/*\n1457:\t * Error injection support for memory error handling.\n1458:\t */\n1459:\tstatic int madvise_inject_error(struct madvise_behavior *madv_behavior)\n1460:\t{\n1461:\t\tunsigned long size;\n1462:\t\tunsigned long start = madv_behavior-\u003erange.start;\n1463:\t\tunsigned long end = madv_behavior-\u003erange.end;\n1464:\t\n1465:\t\tif (!capable(CAP_SYS_ADMIN))\n1466:\t\t\treturn -EPERM;\n1467:\t\n1468:\t\tfor (; start \u003c end; start += size) {\n1469:\t\t\tunsigned long pfn;\n1470:\t\t\tstruct page *page;\n1471:\t\t\tint ret;\n1472:\t\n1473:\t\t\tret = get_user_pages_fast(start, 1, 0, \u0026page);\n1474:\t\t\tif (ret != 1)\n1475:\t\t\t\treturn ret;\n1476:\t\t\tpfn = page_to_pfn(page);\n1477:\t\n1478:\t\t\t/*\n1479:\t\t\t * When soft offlining hugepages, after migrating the page\n1480:\t\t\t * we dissolve it, therefore in the second loop \"page\" will\n1481:\t\t\t * no longer be a compound page.\n1482:\t\t\t */\n1483:\t\t\tsize = page_size(compound_head(page));\n1484:\t\n1485:\t\t\tif (madv_behavior-\u003ebehavior == MADV_SOFT_OFFLINE) {\n1486:\t\t\t\tpr_info(\"Soft offlining pfn %#lx at process virtual address %#lx\\n\",\n1487:\t\t\t\t\t pfn, start);\n1488:\t\t\t\tret = soft_offline_page(pfn, MF_COUNT_INCREASED);\n1489:\t\t\t} else {\n1490:\t\t\t\tpr_info(\"Injecting memory failure for pfn %#lx at process virtual address %#lx\\n\",\n1491:\t\t\t\t\t pfn, start);\n1492:\t\t\t\tret = memory_failure(pfn, MF_ACTION_REQUIRED | MF_COUNT_INCREASED | MF_SW_SIMULATED);\n1493:\t\t\t\tif (ret == -EOPNOTSUPP)\n1494:\t\t\t\t\tret = 0;\n"
}

17/2 2026/09/09 14:09 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 19842
							output: 37
							thoughts: 556
						

Thoughts:
**My Deep Dive into EFI Hardware Poisoning in the Kernel**

Okay, let's break this down. My initial thought was to trace the path of hardware poisoning events. It looks like `madvise(addr, len, MADV_HWPOISON)` triggers a chain of events, starting with `memory_failure()`, leading to `action_result()`, and ultimately calling `efi_hwpoison_record_pfn(pfn)`. That's the core function I need to understand.

I also see other injection methods mentioned: the debugfs entry `/sys/kernel/debug/hwpoison/corrupt-pfn` for direct injection, and the RAS/MCE injection mechanisms using `/dev/mcelog` and APEI. These provide ways to simulate the hardware error that ultimately reaches the same point.

Now, let's zero in on `drivers/firmware/efi/poison.c`. The key function here is clearly `efi_hwpoison_record_pfn(unsigned long pfn)`.  Looking at the code, it's pretty straightforward. It gets a pointer to a `struct linux_efi_poisoned_memory` via `efi_poisoned_memory()`, converts the page frame number (`pfn`) to a physical address, and then maps the address to a bit within a bitmap. This bitmap seems to track which memory units are considered "poisoned". A simple check to ensure we're within the valid poisoned region is also present. If the address is valid, it then sets the relevant bit in the `pm->bitmap`.

I also noticed `range_contains_poisoned_memory`.  This code's job is to check if a specified physical memory range contains any poisoned memory. It does similar address calculations to find the appropriate bit range within the poisoned memory bitmap. I see it handles the edge cases where the provided range might start before or extend beyond the known poisoned memory region. It uses `find_next_bit` to efficiently check if any bit is set within the range.

The next step is to examine `drivers/firmware/efi/poison.c` completely. I need to understand the full context. I'll read this file from top to bottom, paying attention to the initialization of the poisoned memory region and how the bitmap is managed, to build a full picture.



18/2 2026/09/09 14:09 tool read-file
0m Args:
{
  "File": "drivers/firmware/efi/poison.c",
  "FirstLine": 1,
  "LineCount": 100
}

Results:
{
  "Contents": "   1:\t// SPDX-License-Identifier: GPL-2.0-only\n   2:\t/*\n   3:\t * Runtime side of the LINUX_EFI_POISONED_MEMORY table: one bit per\n   4:\t * EFI_POISON_UNIT_SIZE, set here as frames go bad, honored by the next kernel.\n   5:\t *\n   6:\t * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.\n   7:\t * Copyright (c) 2026 Breno Leitao \u003cleitao@debian.org\u003e\n   8:\t */\n   9:\t\n  10:\t#define pr_fmt(fmt) \"efi: \" fmt\n  11:\t\n  12:\t#include \u003clinux/bitmap.h\u003e\n  13:\t#include \u003clinux/efi.h\u003e\n  14:\t#include \u003clinux/io.h\u003e\n  15:\t#include \u003clinux/log2.h\u003e\n  16:\t#include \u003clinux/memblock.h\u003e\n  17:\t#include \u003clinux/mm.h\u003e\n  18:\t#include \u003clinux/overflow.h\u003e\n  19:\t\n  20:\tstatic bool __init\n  21:\tefi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)\n  22:\t{\n  23:\t\tu64 nbits;\n  24:\t\n  25:\t\t/* Whole words, and a bit count that can be taken without wrapping. */\n  26:\t\tif (!pm-\u003esize || !IS_ALIGNED(pm-\u003esize, sizeof(unsigned long)) ||\n  27:\t\t    check_mul_overflow(pm-\u003esize, (u64)BITS_PER_BYTE, \u0026nbits))\n  28:\t\t\treturn false;\n  29:\t\n  30:\t\tif (pm-\u003eunit_size \u003c PAGE_SIZE || !is_power_of_2(pm-\u003eunit_size))\n  31:\t\t\treturn false;\n  32:\t\n  33:\t\treturn IS_ALIGNED(pm-\u003ephys_base, pm-\u003eunit_size);\n  34:\t}\n  35:\t\n  36:\t/* The table may come from an earlier kernel, so vet it before using it. */\n  37:\tstatic bool __init\n  38:\tefi_poison_table_valid(const struct linux_efi_poisoned_memory *pm)\n  39:\t{\n  40:\t\tif (pm-\u003eversion != 1) {\n  41:\t\t\tpr_warn(\"Ignoring poisoned-memory table with version %u\\n\",\n  42:\t\t\t\tpm-\u003eversion);\n  43:\t\t\treturn false;\n  44:\t\t}\n  45:\t\n  46:\t\tif (!efi_poison_geometry_valid(pm)) {\n  47:\t\t\tpr_warn(\"Ignoring malformed poisoned-memory table\\n\");\n  48:\t\t\treturn false;\n  49:\t\t}\n  50:\t\n  51:\t\treturn true;\n  52:\t}\n  53:\t\n  54:\t/*\n  55:\t * Vet the inherited table and hand its pages to memblock, the way the\n  56:\t * unaccepted memory table is handled. It is EFI ACPI reclaim memory, which\n  57:\t * becomes E820_TYPE_ACPI and would otherwise stay out of the direct map, and\n  58:\t * touching it then faults. Called from efi_config_parse_tables(), so\n  59:\t * everything later can reach it with efi_poisoned_memory().\n  60:\t */\n  61:\tvoid __init efi_poisoned_memory_reserve(void)\n  62:\t{\n  63:\t\tstruct linux_efi_poisoned_memory *pm;\n  64:\t\tphys_addr_t start, end;\n  65:\t\n  66:\t\tif (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)\n  67:\t\t\treturn;\n  68:\t\n  69:\t\tpm = early_memremap(efi.poisoned_memory, sizeof(*pm));\n  70:\t\tif (!pm) {\n  71:\t\t\tpr_warn(\"Could not map poisoned-memory table\\n\");\n  72:\t\t\tefi.poisoned_memory = EFI_INVALID_TABLE_ADDR;\n  73:\t\t\treturn;\n  74:\t\t}\n  75:\t\n  76:\t\tif (!efi_poison_table_valid(pm)) {\n  77:\t\t\tefi.poisoned_memory = EFI_INVALID_TABLE_ADDR;\n  78:\t\t\tearly_memunmap(pm, sizeof(*pm));\n  79:\t\t\treturn;\n  80:\t\t}\n  81:\t\n  82:\t\tstart = PAGE_ALIGN_DOWN(efi.poisoned_memory);\n  83:\t\tend = PAGE_ALIGN(efi.poisoned_memory + sizeof(*pm) + pm-\u003esize);\n  84:\t\tearly_memunmap(pm, sizeof(*pm));\n  85:\t\n  86:\t\tmemblock_add(start, end - start);\n  87:\t\tmemblock_reserve(start, end - start);\n  88:\t}\n  89:\t\n  90:\t/* The table, vetted at parse time, or NULL if this boot has none. */\n  91:\tstatic struct linux_efi_poisoned_memory *efi_poisoned_memory(void)\n  92:\t{\n  93:\t\tif (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)\n  94:\t\t\treturn NULL;\n  95:\t\n  96:\t\treturn phys_to_virt(efi.poisoned_memory);\n  97:\t}\n  98:\t\n  99:\t/* Does the range cover a unit an earlier kernel recorded as bad? */\n 100:\tbool range_contains_poisoned_memory(phys_addr_t start, unsigned long size)\n"
}

19/2 2026/09/09 14:09 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 22401
							output: 38
							thoughts: 19
						

Thoughts:
Okay, here's my interpretation of what's going through my mind as I approach this `drivers/firmware/efi/poison.c` file, tailored for an expert audience:

**Diving into the Poison**

Alright, time to get into the heart of this `poison.c` file within the EFI firmware drivers. Given the name, "poison" immediately flags a potential area of interest for memory corruption vulnerabilities, defensive programming techniques, or perhaps even a deliberate, if unusual, memory sanitization strategy. I'm expecting to find code that deals with either deliberately overwriting memory with known patterns, potentially to detect use-after-free conditions or other memory errors, or code that is defensive in nature.

I'll be looking specifically for how memory regions are handled, allocated, deallocated, and especially how they are initialized and potentially overwritten. I'll be watching for any use of magic values, sentinel values, or debug-oriented patterns that scream "this is a canary in the coal mine." The EFI context suggests I should be paying close attention to any interaction with firmware tables, runtime services, or potentially even SMRAM. It is vital to see how these potentially sensitive regions are protected.

My first read-through will probably be a broad scan, but I'll zero in on functions related to memory allocation, copying, and initialization. I'll be thinking about potential exploit vectors as I go, such as buffer overflows, off-by-one errors, or incorrect pointer arithmetic, particularly in the context of the EFI environment. Any custom allocation or deallocation routines would be especially interesting, as those often harbor subtle bugs. I'm hoping to understand how "poison" is applied and what the intended purpose of that "poison" might be.



20/2 2026/09/09 14:10 tool read-file
0m Args:
{
  "File": "drivers/firmware/efi/poison.c",
  "FirstLine": 100,
  "LineCount": 45
}

Results:
{
  "Contents": " 100:\tbool range_contains_poisoned_memory(phys_addr_t start, unsigned long size)\n 101:\t{\n 102:\t\tstruct linux_efi_poisoned_memory *pm = efi_poisoned_memory();\n 103:\t\tu64 first, last, nbits;\n 104:\t\n 105:\t\tif (!pm)\n 106:\t\t\treturn false;\n 107:\t\n 108:\t\tnbits = pm-\u003esize * BITS_PER_BYTE;\n 109:\t\n 110:\t\tif (start + size \u003c= pm-\u003ephys_base)\n 111:\t\t\treturn false;\n 112:\t\tif (start \u003c pm-\u003ephys_base)\n 113:\t\t\tstart = pm-\u003ephys_base;\n 114:\t\n 115:\t\tfirst = (start - pm-\u003ephys_base) / pm-\u003eunit_size;\n 116:\t\tif (first \u003e= nbits)\n 117:\t\t\treturn false;\n 118:\t\n 119:\t\tlast = (start + size - 1 - pm-\u003ephys_base) / pm-\u003eunit_size;\n 120:\t\tlast = min(last, nbits - 1);\n 121:\t\n 122:\t\treturn find_next_bit(pm-\u003ebitmap, last + 1, first) \u003c= last;\n 123:\t}\n 124:\t\n 125:\t/*\n 126:\t * A bit is never cleared: it stands for a whole EFI_POISON_UNIT_SIZE, so an\n 127:\t * unpoison cannot tell whether the unit as a whole is good again.\n 128:\t */\n 129:\tvoid efi_hwpoison_record_pfn(unsigned long pfn)\n 130:\t{\n 131:\t\tstruct linux_efi_poisoned_memory *pm = efi_poisoned_memory();\n 132:\t\tphys_addr_t addr = PFN_PHYS(pfn);\n 133:\t\tu64 unit;\n 134:\t\n 135:\t\tif (!pm || addr \u003c pm-\u003ephys_base)\n 136:\t\t\treturn;\n 137:\t\n 138:\t\tunit = (addr - pm-\u003ephys_base) / pm-\u003eunit_size;\n 139:\t\tif (unit \u003c pm-\u003esize * BITS_PER_BYTE)\n 140:\t\t\tset_bit(unit, pm-\u003ebitmap);\n 141:\t}\n"
}

21/2 2026/09/09 14:10 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 23475
							output: 202
							thoughts: 1330
						

Thoughts:
**Fuzzing Target Identification: Poisoned Memory in the Linux Kernel**

Okay, let's break this down. My initial focus is on the `mm/page_alloc.c` and `mm/memory-failure.c` files, specifically looking at the interaction of memory poisoning within the page allocation and memory failure handling subsystems. The code snippets for `poison_block` and `hwpoison_boot_page` are particularly interesting, especially how they interact with `range_contains_poisoned_memory`. I need to understand how poisoned memory is identified and then handled during page freeing, particularly the TODO comment in `__free_pages_core`.

Now, the Kconfig options. `EFI_POISONED_MEMORY` is what flags the feature, and it depends on `EFI_STUB`, `MEMORY_FAILURE`, and `64BIT`.  This implies a UEFI environment, something I can readily test in QEMU.  `CONFIG_EFI` is a prerequisite for `EFI_STUB`.

Next, the execution flow. In a QEMU/UEFI setup, the EFI stub initializes, sets up memory tables, the kernel boots, processes these tables (e.g. `efi_config_parse_tables`), and then calls `efi_poisoned_memory_reserve()`.  Runtime injection (via `madvise` or sysfs/debugfs) then triggers `efi_hwpoison_record_pfn`. Critically, `range_contains_poisoned_memory` also comes into play during boot and hotplug operations.

Is this fuzzable? Absolutely. This is core MM and EFI changes. Functions I want to focus on: `efi_hwpoison_record_pfn`, `range_contains_poisoned_memory`, `hwpoison_boot_page`, and `poison_block`. Also, since the fuzzing is targeting EFI environments, other calls are worth testing, `action_result`, and `memory_failure`.

I'll pinpoint the functions introduced or modified by this patch.  `drivers/firmware/efi/poison.c` introduces `efi_hwpoison_record_pfn`, `range_contains_poisoned_memory`, and `efi_poisoned_memory_reserve`.  `mm/memory-failure.c` gives us `hwpoison_boot_page`, `action_result`, and `memory_failure`.  `mm/page_alloc.c` has `__free_pages_core`. However, `__free_pages_core` is only during boot/hotplug, which may not require focus. The relevant functions are `efi_hwpoison_record_pfn`, `range_contains_poisoned_memory`, and `hwpoison_boot_page`.

I'll check the patch for the other functions: `install_poisoned_memory_table`, `efi_poisoned_memory_reserve`, `efi_hwpoison_record_pfn`, `range_contains_poisoned_memory`, and `hwpoison_boot_page`.

`action_result` is static, but `memory_failure` and the others are global symbols.

Finally, I need to confirm the required configurations are enabled: `EFI_POISONED_MEMORY`, `MEMORY_FAILURE`, `EFI_STUB`, and `EFI`.

Therefore, based on my expert assessment:

*   **WorthFuzzing:** TRUE
*   **EnableConfigs:** `["EFI_POISONED_MEMORY", "MEMORY_FAILURE", "EFI_STUB", "EFI"]`
*   **FocusSymbols:** `["efi_hwpoison_record_pfn", "range_contains_poisoned_memory", "hwpoison_boot_page", "memory_failure"]`
*   **Reasoning:** The intersection of core memory management, EFI/UEFI interactions, and a feature designed to detect and handle memory failures presents a high-value fuzzing target. The potential for bugs is high.



22/2 2026/09/09 14:10 tool set-results
0m Args:
{
  "EnableConfigs": [
    "EFI_POISONED_MEMORY",
    "MEMORY_FAILURE",
    "EFI_STUB",
    "EFI"
  ],
  "FocusSymbols": [
    "efi_hwpoison_record_pfn",
    "range_contains_poisoned_memory",
    "hwpoison_boot_page",
    "memory_failure"
  ],
  "Reasoning": "The patch adds support for tracking hardware-poisoned memory across kexec via an EFI configuration table (CONFIG_EFI_POISONED_MEMORY). It introduces hooks in core memory-failure handling (efi_hwpoison_record_pfn in action_result) and memory initialization/hotplug (__free_pages_core, hwpoison_boot_page). These paths are reachable at runtime via memory failure mechanisms such as madvise(MADV_HWPOISON) and memory hotplugging in virtualized environments with EFI.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "EFI_POISONED_MEMORY",
    "MEMORY_FAILURE",
    "EFI_STUB",
    "EFI"
  ],
  "FocusSymbols": [
    "efi_hwpoison_record_pfn",
    "range_contains_poisoned_memory",
    "hwpoison_boot_page",
    "memory_failure"
  ],
  "Reasoning": "The patch adds support for tracking hardware-poisoned memory across kexec via an EFI configuration table (CONFIG_EFI_POISONED_MEMORY). It introduces hooks in core memory-failure handling (efi_hwpoison_record_pfn in action_result) and memory initialization/hotplug (__free_pages_core, hwpoison_boot_page). These paths are reachable at runtime via memory failure mechanisms such as madvise(MADV_HWPOISON) and memory hotplugging in virtualized environments with EFI.",
  "WorthFuzzing": true
}

23/1 2026/09/09 14:10 action If
0m Args:
{
  "WorthFuzzing": true
}

Results:
null

24/2 2026/09/09 14:10 agent kmsan-evaluator
0m Model:
core

Args:
null

Results:
{
  "KMSANReasoning": "The patch series introduces EFI poisoned memory tracking (CONFIG_EFI_POISONED_MEMORY) to preserve hardware-poisoned page information across kexec reboots. \n\n1. Allocation \u0026 Initialization: The table structure `struct linux_efi_poisoned_memory` and its trailing bitmap are allocated in the EFI stub and explicitly fully initialized (all header fields assigned, and `memset(pm-\u003ebitmap, 0, bitmap_size)` clears the bitmap).\n2. Memory Accesses: Runtime operations (`range_contains_poisoned_memory`, `efi_hwpoison_record_pfn`, `poison_block`, and `hwpoison_boot_page`) only read and update fully initialized memory structures and struct page flags.\n3. No Uninitialized Leaks or Reads: There are no data transfers to user space, no conditional branches on uninitialized variables, and no exposed padding.\n4. KASAN vs KMSAN: Potential risks such as invalid pointer math, out-of-bounds bitmap accesses, or memory reservation errors are properly handled by KASAN and standard kernel debugging tools. KMSAN is not required.",
  "NeedsKMSAN": false
}

Instruction:
You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.

CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
  a comprehensive suite of debugging tools and sanitizers, including KASAN
  (out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
  (locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
  or page allocations) and kernel-to-user memory info-leaks.

Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.

Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.

Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
   attributes, ioctl output arguments, socket options, or BPF buffers) where fields
   or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
   or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
   could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
   uninitialized bytes of existing buffers.

Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
  dereferences, locking deadlocks, or use-after-free bugs (these are already caught
  by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
  or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.

Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit a79cbb105c59817af32a22a00171f7bb91467654
Author: syz-cluster <triage@syzkaller.com>
Date:   Wed Sep 9 14:09:05 2026 +0000

    syz-cluster: applied patch under review

diff --git a/arch/x86/platform/efi/efi.c b/arch/x86/platform/efi/efi.c
index 0c39adb96b912..2b37b96a36e09 100644
--- a/arch/x86/platform/efi/efi.c
+++ b/arch/x86/platform/efi/efi.c
@@ -93,6 +93,9 @@ static const unsigned long * const efi_tables[] = {
 #ifdef CONFIG_UNACCEPTED_MEMORY
 	&efi.unaccepted,
 #endif
+#ifdef CONFIG_EFI_POISONED_MEMORY
+	&efi.poisoned_memory,
+#endif
 };
 
 u64 efi_setup;		/* efi setup_data physical address */
diff --git a/drivers/firmware/efi/Kconfig b/drivers/firmware/efi/Kconfig
index 29e0729299f5b..aafcd41bc0063 100644
--- a/drivers/firmware/efi/Kconfig
+++ b/drivers/firmware/efi/Kconfig
@@ -263,6 +263,14 @@ config EFI_COCO_SECRET
 	  virt/coco/efi_secret module to access the secrets, which in turn
 	  allows userspace programs to access the injected secrets.
 
+config EFI_POISONED_MEMORY
+	def_bool y
+	depends on EFI_STUB && MEMORY_FAILURE && 64BIT
+	help
+	  Record page frames that are hardware-poisoned while this kernel runs
+	  into an EFI configuration table, and honor that table early on the
+	  next kernel so a kexec does not hand known-bad RAM back out.
+
 config OVMF_DEBUG_LOG
 	bool "Expose OVMF firmware debug log via sysfs"
 	depends on EFI
diff --git a/drivers/firmware/efi/Makefile b/drivers/firmware/efi/Makefile
index 8efbcf699e4ff..05d0a490923e5 100644
--- a/drivers/firmware/efi/Makefile
+++ b/drivers/firmware/efi/Makefile
@@ -43,4 +43,5 @@ obj-$(CONFIG_EFI_EARLYCON)		+= earlycon.o
 obj-$(CONFIG_UEFI_CPER_ARM)		+= cper-arm.o
 obj-$(CONFIG_UEFI_CPER_X86)		+= cper-x86.o
 obj-$(CONFIG_UNACCEPTED_MEMORY)		+= unaccepted_memory.o
+obj-$(CONFIG_EFI_POISONED_MEMORY)	+= poison.o
 obj-$(CONFIG_TEE_STMM_EFI)		+= stmm/tee_stmm_efi.o
diff --git a/drivers/firmware/efi/efi.c b/drivers/firmware/efi/efi.c
index 6d987d7f97781..55b2ee53fc268 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
@@ -877,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/libstub/efi-stub-helper.c b/drivers/firmware/efi/libstub/efi-stub-helper.c
index 48f93f7758e9e..5cbe675491333 100644
--- a/drivers/firmware/efi/libstub/efi-stub-helper.c
+++ b/drivers/firmware/efi/libstub/efi-stub-helper.c
@@ -774,3 +774,103 @@ void efi_remap_image(unsigned long image_base, unsigned alloc_size,
 			efi_warn("Failed to remap data region non-executable\n");
 	}
 }
+
+#ifdef CONFIG_EFI_POISONED_MEMORY
+/*
+ * Find the base and top of the memory, so, we can create the bitmap for
+ * the full range.
+ */
+static efi_status_t efi_get_ram_range(u64 *base, u64 *top)
+{
+	struct efi_boot_memmap *map __free(efi_pool) = NULL;
+	u64 ram_base = ULLONG_MAX, ram_top = 0;
+	efi_status_t status;
+	int i, nr_desc;
+
+	status = efi_get_memory_map(&map, false);
+	if (status != EFI_SUCCESS)
+		return status;
+
+	nr_desc = map->map_size / map->desc_size;
+	for (i = 0; i < nr_desc; i++) {
+		efi_memory_desc_t *d;
+
+		d = efi_memdesc_ptr((unsigned long)map->map, map->desc_size, i);
+		ram_base = min(ram_base, d->phys_addr);
+		ram_top = max(ram_top,
+			      d->phys_addr + d->num_pages * EFI_PAGE_SIZE);
+	}
+	if (!ram_top || ram_base == ULLONG_MAX)
+		return EFI_NOT_FOUND;
+
+	*base = round_down(ram_base, EFI_POISON_UNIT_SIZE);
+	*top = round_up(ram_top, EFI_POISON_UNIT_SIZE);
+
+	return EFI_SUCCESS;
+}
+
+/* The size of the bitmap */
+static u64 efi_poison_bitmap_size(u64 span)
+{
+	u64 bytes = DIV_ROUND_UP(DIV_ROUND_UP(span, EFI_POISON_UNIT_SIZE),
+				 BITS_PER_BYTE);
+
+	return round_up(bytes, sizeof(unsigned long));
+}
+
+static struct linux_efi_poisoned_memory *efi_poison_alloc(u64 phys_base,
+							  u64 bitmap_size)
+{
+	struct linux_efi_poisoned_memory *pm;
+	efi_status_t status;
+
+	status = efi_bs_call(allocate_pool, EFI_ACPI_RECLAIM_MEMORY,
+			     sizeof(*pm) + bitmap_size, (void **)&pm);
+	if (status != EFI_SUCCESS)
+		return NULL;
+
+	pm->version = 1;
+	pm->unit_size = EFI_POISON_UNIT_SIZE;
+	pm->phys_base = phys_base;
+	pm->size = bitmap_size;
+	memset(pm->bitmap, 0, bitmap_size);
+
+	return pm;
+}
+
+/* This needs to be done while boot service is still active */
+void install_poisoned_memory_table(void)
+{
+	efi_guid_t poisoned_memory_table_guid = LINUX_EFI_POISONED_MEMORY_TABLE_GUID;
+	struct linux_efi_poisoned_memory *pm;
+	u64 ram_base, ram_top, bitmap_size;
+	efi_status_t status;
+
+	/* A table installed by an earlier boot rides the system table across kexec. */
+	pm = get_efi_config_table(poisoned_memory_table_guid);
+	if (pm) {
+		if (pm->version != 1)
+			efi_err("Unknown version of poisoned-memory table\n");
+		return;
+	}
+
+	if (efi_get_ram_range(&ram_base, &ram_top) != EFI_SUCCESS) {
+		efi_err("Failed to size the poisoned-memory table!\n");
+		return;
+	}
+
+	bitmap_size = efi_poison_bitmap_size(ram_top - ram_base);
+	pm = efi_poison_alloc(ram_base, bitmap_size);
+	if (!pm) {
+		efi_err("Failed to allocate poisoned-memory table!\n");
+		return;
+	}
+
+	status = efi_bs_call(install_configuration_table,
+			     &poisoned_memory_table_guid, pm);
+	if (status != EFI_SUCCESS) {
+		efi_bs_call(free_pool, pm);
+		efi_err("Failed to install poisoned-memory config table!\n");
+	}
+}
+#endif
diff --git a/drivers/firmware/efi/libstub/efi-stub.c b/drivers/firmware/efi/libstub/efi-stub.c
index 235c9738da2d6..22a315e2814a1 100644
--- a/drivers/firmware/efi/libstub/efi-stub.c
+++ b/drivers/firmware/efi/libstub/efi-stub.c
@@ -179,6 +179,7 @@ efi_status_t efi_stub_common(efi_handle_t handle,
 			 EFI_RT_SUPPORTED_SET_VIRTUAL_ADDRESS_MAP);
 
 	install_memreserve_table();
+	install_poisoned_memory_table();
 
 	status = efi_boot_kernel(handle, image, image_addr, cmdline_ptr);
 
diff --git a/drivers/firmware/efi/libstub/efistub.h b/drivers/firmware/efi/libstub/efistub.h
index fd91fc15ec810..44436869c4efe 100644
--- a/drivers/firmware/efi/libstub/efistub.h
+++ b/drivers/firmware/efi/libstub/efistub.h
@@ -1169,6 +1169,12 @@ efi_enable_reset_attack_mitigation(void) { }
 
 void efi_retrieve_eventlog(void);
 
+#ifdef CONFIG_EFI_POISONED_MEMORY
+void install_poisoned_memory_table(void);
+#else
+static inline void install_poisoned_memory_table(void) { }
+#endif
+
 struct sysfb_display_info *alloc_primary_display(void);
 struct sysfb_display_info *__alloc_primary_display(void);
 void free_primary_display(struct sysfb_display_info *dpy);
diff --git a/drivers/firmware/efi/libstub/x86-stub.c b/drivers/firmware/efi/libstub/x86-stub.c
index 0bae0f06b6763..3136132b9628a 100644
--- a/drivers/firmware/efi/libstub/x86-stub.c
+++ b/drivers/firmware/efi/libstub/x86-stub.c
@@ -1024,6 +1024,8 @@ void __noreturn efi_stub_entry(efi_handle_t handle,
 
 	setup_unaccepted_memory();
 
+	install_poisoned_memory_table();
+
 	status = exit_boot(boot_params, handle);
 	if (status != EFI_SUCCESS) {
 		efi_err("exit_boot() failed!\n");
diff --git a/drivers/firmware/efi/poison.c b/drivers/firmware/efi/poison.c
new file mode 100644
index 0000000000000..e16d43f4438ee
--- /dev/null
+++ b/drivers/firmware/efi/poison.c
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Runtime side of the LINUX_EFI_POISONED_MEMORY table: one bit per
+ * EFI_POISON_UNIT_SIZE, set here as frames go bad, honored by the next kernel.
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2026 Breno Leitao <leitao@debian.org>
+ */
+
+#define pr_fmt(fmt) "efi: " fmt
+
+#include <linux/bitmap.h>
+#include <linux/efi.h>
+#include <linux/io.h>
+#include <linux/log2.h>
+#include <linux/memblock.h>
+#include <linux/mm.h>
+#include <linux/overflow.h>
+
+static bool __init
+efi_poison_geometry_valid(const struct linux_efi_poisoned_memory *pm)
+{
+	u64 nbits;
+
+	/* Whole words, and a bit count that can be taken without wrapping. */
+	if (!pm->size || !IS_ALIGNED(pm->size, sizeof(unsigned long)) ||
+	    check_mul_overflow(pm->size, (u64)BITS_PER_BYTE, &nbits))
+		return false;
+
+	if (pm->unit_size < PAGE_SIZE || !is_power_of_2(pm->unit_size))
+		return false;
+
+	return IS_ALIGNED(pm->phys_base, pm->unit_size);
+}
+
+/* The table may come from an earlier kernel, so vet it before using it. */
+static bool __init
+efi_poison_table_valid(const struct linux_efi_poisoned_memory *pm)
+{
+	if (pm->version != 1) {
+		pr_warn("Ignoring poisoned-memory table with version %u\n",
+			pm->version);
+		return false;
+	}
+
+	if (!efi_poison_geometry_valid(pm)) {
+		pr_warn("Ignoring malformed poisoned-memory table\n");
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Vet the inherited table and hand its pages to memblock, the way the
+ * unaccepted memory table is handled. It is EFI ACPI reclaim memory, which
+ * becomes E820_TYPE_ACPI and would otherwise stay out of the direct map, and
+ * touching it then faults. Called from efi_config_parse_tables(), so
+ * everything later can reach it with efi_poisoned_memory().
+ */
+void __init efi_poisoned_memory_reserve(void)
+{
+	struct linux_efi_poisoned_memory *pm;
+	phys_addr_t start, end;
+
+	if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
+		return;
+
+	pm = early_memremap(efi.poisoned_memory, sizeof(*pm));
+	if (!pm) {
+		pr_warn("Could not map poisoned-memory table\n");
+		efi.poisoned_memory = EFI_INVALID_TABLE_ADDR;
+		return;
+	}
+
+	if (!efi_poison_table_valid(pm)) {
+		efi.poisoned_memory = EFI_INVALID_TABLE_ADDR;
+		early_memunmap(pm, sizeof(*pm));
+		return;
+	}
+
+	start = PAGE_ALIGN_DOWN(efi.poisoned_memory);
+	end = PAGE_ALIGN(efi.poisoned_memory + sizeof(*pm) + pm->size);
+	early_memunmap(pm, sizeof(*pm));
+
+	memblock_add(start, end - start);
+	memblock_reserve(start, end - start);
+}
+
+/* The table, vetted at parse time, or NULL if this boot has none. */
+static struct linux_efi_poisoned_memory *efi_poisoned_memory(void)
+{
+	if (efi.poisoned_memory == EFI_INVALID_TABLE_ADDR)
+		return NULL;
+
+	return phys_to_virt(efi.poisoned_memory);
+}
+
+/* Does the range cover a unit an earlier kernel recorded as bad? */
+bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size)
+{
+	struct linux_efi_poisoned_memory *pm = efi_poisoned_memory();
+	u64 first, last, nbits;
+
+	if (!pm)
+		return false;
+
+	nbits = pm->size * BITS_PER_BYTE;
+
+	if (start + size <= pm->phys_base)
+		return false;
+	if (start < pm->phys_base)
+		start = pm->phys_base;
+
+	first = (start - pm->phys_base) / pm->unit_size;
+	if (first >= nbits)
+		return false;
+
+	last = (start + size - 1 - pm->phys_base) / pm->unit_size;
+	last = min(last, nbits - 1);
+
+	return find_next_bit(pm->bitmap, last + 1, first) <= last;
+}
+
+/*
+ * A bit is never cleared: it stands for a whole EFI_POISON_UNIT_SIZE, so an
+ * unpoison cannot tell whether the unit as a whole is good again.
+ */
+void efi_hwpoison_record_pfn(unsigned long pfn)
+{
+	struct linux_efi_poisoned_memory *pm = efi_poisoned_memory();
+	phys_addr_t addr = PFN_PHYS(pfn);
+	u64 unit;
+
+	if (!pm || addr < pm->phys_base)
+		return;
+
+	unit = (addr - pm->phys_base) / pm->unit_size;
+	if (unit < pm->size * BITS_PER_BYTE)
+		set_bit(unit, pm->bitmap);
+}
diff --git a/include/linux/efi.h b/include/linux/efi.h
index c35446a0b66fa..56402fdccd114 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,25 @@ 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
+
+#ifdef CONFIG_EFI_POISONED_MEMORY
+void __init efi_poisoned_memory_reserve(void);
+void efi_hwpoison_record_pfn(unsigned long pfn);
+#else
+static inline void efi_poisoned_memory_reserve(void) { }
+static inline void efi_hwpoison_record_pfn(unsigned long pfn) { }
+#endif
+
 void __init efi_arch_mem_reserve(phys_addr_t addr, u64 size);
 
 /*
diff --git a/include/linux/mm.h b/include/linux/mm.h
index 274fa880077c5..9d9f2e8fdc136 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -5225,6 +5225,7 @@ extern const struct attribute_group memory_failure_attr_group;
 extern void memory_failure_queue(unsigned long pfn, int flags);
 void num_poisoned_pages_inc(unsigned long pfn);
 void num_poisoned_pages_sub(unsigned long pfn, long i);
+void __meminit hwpoison_boot_page(struct page *page);
 phys_addr_t range_first_hwpoison(phys_addr_t start, unsigned long size);
 phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size);
 #else
@@ -5232,6 +5233,10 @@ static inline void memory_failure_queue(unsigned long pfn, int flags)
 {
 }
 
+static inline void hwpoison_boot_page(struct page *page)
+{
+}
+
 static inline void num_poisoned_pages_inc(unsigned long pfn)
 {
 }
@@ -5387,6 +5392,20 @@ static inline bool pfn_is_unaccepted_memory(unsigned long pfn)
 	return range_contains_unaccepted_memory(pfn << PAGE_SHIFT, PAGE_SIZE);
 }
 
+#ifdef CONFIG_EFI_POISONED_MEMORY
+
+bool range_contains_poisoned_memory(phys_addr_t start, unsigned long size);
+
+#else
+
+static inline bool range_contains_poisoned_memory(phys_addr_t start,
+						  unsigned long size)
+{
+	return false;
+}
+
+#endif
+
 void vma_pgtable_walk_begin(struct vm_area_struct *vma);
 void vma_pgtable_walk_end(struct vm_area_struct *vma);
 
diff --git a/mm/memory-failure.c b/mm/memory-failure.c
index a2ca8df501cae..f6afdb2a89a94 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>
@@ -136,6 +137,21 @@ phys_addr_t range_last_hwpoison(phys_addr_t start, unsigned long size)
 	return range_hwpoison(start, size, false);
 }
 
+static void update_per_node_mf_stats(unsigned long pfn, enum mf_result result);
+
+/* Not num_poisoned_pages_inc(): its per block half divides by zero this early. */
+void __meminit hwpoison_boot_page(struct page *page)
+{
+	if (PageHWPoison(page))
+		return;
+
+	SetPageHWPoison(page);
+	set_page_count(page, 1);
+	/* The page has been completely isolated == MF_RECOVERED */
+	update_per_node_mf_stats(page_to_pfn(page), MF_RECOVERED);
+	atomic_long_inc(&num_poisoned_pages);
+}
+
 /**
  * MF_ATTR_RO - Create sysfs entry for each memory failure statistics.
  * @_name: name of the file in the per NUMA sysfs directory.
@@ -1326,6 +1342,8 @@ static int action_result(unsigned long pfn, enum mf_action_page_type type,
 	if (type != MF_MSG_ALREADY_POISONED && type != MF_MSG_PFN_MAP) {
 		num_poisoned_pages_inc(pfn);
 		update_per_node_mf_stats(pfn, result);
+		/* Only hard offlines are carried over to the next kernel. */
+		efi_hwpoison_record_pfn(pfn);
 	}
 
 	pr_err("%#lx: recovery action for %s: %s\n",
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index 404896b53003e..9e2ce833fd409 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -1579,6 +1579,19 @@ static void __free_pages_ok(struct page *page, unsigned int order,
 		free_one_page(zone, page, pfn, order, fpi_flags);
 }
 
+/* Flag the frames an earlier kernel recorded as bad. */
+static void __meminit poison_block(struct page *page, unsigned int order)
+{
+	unsigned long i, nr_pages = 1UL << order;
+
+	for (i = 0; i < nr_pages; i++) {
+		struct page *p = page + i;
+
+		if (range_contains_poisoned_memory(page_to_phys(p), PAGE_SIZE))
+			hwpoison_boot_page(p);
+	}
+}
+
 void __meminit __free_pages_core(struct page *page, unsigned int order,
 		enum meminit_context context)
 {
@@ -1613,6 +1626,18 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,
 		atomic_long_add(nr_pages, &page_zone(page)->managed_pages);
 	}
 
+	/* First: a block parked by __free_unaccepted() never returns here. */
+	if (range_contains_poisoned_memory(page_to_phys(page),
+					   PAGE_SIZE << order)) {
+		poison_block(page, order);
+		/*
+		 * TODO: free the frames in the block that are not poisoned.
+		 * They stay out of the allocator and still count in
+		 * managed_pages, so a unit costs up to a block.
+		 */
+		return;
+	}
+
 	if (page_contains_unaccepted(page, order)) {
 		if (order == MAX_PAGE_ORDER && __free_unaccepted(page))
 			return;

25/3 2026/09/09 14:10 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 9411
							output: 259
							thoughts: 1296
						

Thoughts:
**KMSAN Fuzzing Session Assessment: EFI Poisoned Memory Table Management**

Okay, let's break down this patch series and determine if it justifies a KMSAN fuzzing run. My initial assessment is leaning towards a "no," but we need to be thorough.

First, I need to understand what this patch fundamentally does. It introduces `CONFIG_EFI_POISONED_MEMORY` and, critically, a mechanism for managing a "poisoned memory" table within the EFI environment. This table seems to track regions of RAM that are considered "poisoned" – likely marked for memory failure injection.

The core of the logic involves allocating a `struct linux_efi_poisoned_memory` structure. This allocation happens via EFI boot services (`allocate_pool`). Importantly, the allocated bitmap is immediately zeroed via `memset`. This is key. The code then initializes the structure's fields: `version`, `unit_size`, `phys_base`, `size` – all explicitly assigned. This table is then installed as a config table.

During kernel boot, the code parses the EFI config tables and calls `efi_poisoned_memory_reserve()` to validate, map and reserve the described memory using `memblock_reserve()`. The critical bit is that during page freeing, `__free_pages_core`, if a page falls within the defined "poisoned" region, a `hwpoison` flag is set using `hwpoison_boot_page()`.  Later, in the event of a memory failure, the `efi_hwpoison_record_pfn()` function updates the bitmap to record the poisoned page. This bitmap is then checked later using `range_contains_poisoned_memory()`.

Now, the main concern for KMSAN is uninitialized memory. Does this patch introduce new pathways for uninitialized data to leak or be used?

1.  **User-space exposure?** No, there are no user-space copies, sysfs entries, or ioctls introduced.
2.  **Initialization?** The code does seem meticulously initialized. `memset` on the bitmap, and explicit field assignments within the struct. The functions like `efi_get_ram_range`, `range_contains_poisoned_memory` all appear to handle their internal state correctly.
3.  **Control flow based on uninitialized data?**  No, this appears to be just hardware poison tracking across kexec / EFI boot. The bitmap and state variables are all initialized.
4.  **KMSAN Specific Concerns?** KMSAN’s primary focus is on uninitialized memory reads (stack, heap, pages) and kernel-to-user memory leaks.  This patch *doesn't* appear to have any. Everything revolves around early boot memory reservation/table creation/bit setting and checking in the initialized bitmap. Any potential errors here would be address translation errors, out-of-bounds access, or incorrect memory reservation. These are things that are handled by KASAN or more standard verification techniques.

Let's do a quick double-check of all functions and data structures.

*   `efi_get_ram_range`: Initializes and returns, the variables are initialized.
*   `efi_poison_bitmap_size`: A pure calculation.
*   `efi_poison_alloc`: Allocates and correctly initializes the bitmap.
*   `install_poisoned_memory_table`: Initializes locals or checks statuses.
*   `efi_poison_geometry_valid`, `efi_poison_table_valid`: Validation on the table fields.
*   `efi_poisoned_memory_reserve`: Includes early memremap, validation, and memblock.
*   `efi_poisoned_memory`: Returns a pointer.
*   `range_contains_poisoned_memory`: Boundary checks and `find_next_bit` calls.
*   `efi_hwpoison_record_pfn`: Includes boundary checks and `set_bit` calls.
*   `hwpoison_boot_page`: Operates on `struct page`, with set\_page\_count, atomic\_long\_inc, etc.
*   `poison_block`: Iterates over the pages and calls `range_contains_poisoned_memory` and `hwpoison_boot_page`.

Okay, the conclusion remains: **NeedsKMSAN = false.**

Here’s the distilled reasoning, clearly contrasting KMSAN and KASAN:

*   KMSAN is looking for *uninitialized* memory reads/leaks.
*   This patch explicitly initializes the memory table and the bitmap with `memset`. Every struct field is explicitly initialized.
*   The runtime functions only read and modify initialized data.
*   There's no introduction of new user-space copies or uninitialized conditional logic.
*   Any bugs would relate to address


26/3 2026/09/09 14:10 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch series introduces EFI poisoned memory tracking (CONFIG_EFI_POISONED_MEMORY) to preserve hardware-poisoned page information across kexec reboots. \n\n1. Allocation \u0026 Initialization: The table structure `struct linux_efi_poisoned_memory` and its trailing bitmap are allocated in the EFI stub and explicitly fully initialized (all header fields assigned, and `memset(pm-\u003ebitmap, 0, bitmap_size)` clears the bitmap).\n2. Memory Accesses: Runtime operations (`range_contains_poisoned_memory`, `efi_hwpoison_record_pfn`, `poison_block`, and `hwpoison_boot_page`) only read and update fully initialized memory structures and struct page flags.\n3. No Uninitialized Leaks or Reads: There are no data transfers to user space, no conditional branches on uninitialized variables, and no exposed padding.\n4. KASAN vs KMSAN: Potential risks such as invalid pointer math, out-of-bounds bitmap accesses, or memory reservation errors are properly handled by KASAN and standard kernel debugging tools. KMSAN is not required.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch series introduces EFI poisoned memory tracking (CONFIG_EFI_POISONED_MEMORY) to preserve hardware-poisoned page information across kexec reboots. \n\n1. Allocation \u0026 Initialization: The table structure `struct linux_efi_poisoned_memory` and its trailing bitmap are allocated in the EFI stub and explicitly fully initialized (all header fields assigned, and `memset(pm-\u003ebitmap, 0, bitmap_size)` clears the bitmap).\n2. Memory Accesses: Runtime operations (`range_contains_poisoned_memory`, `efi_hwpoison_record_pfn`, `poison_block`, and `hwpoison_boot_page`) only read and update fully initialized memory structures and struct page flags.\n3. No Uninitialized Leaks or Reads: There are no data transfers to user space, no conditional branches on uninitialized variables, and no exposed padding.\n4. KASAN vs KMSAN: Potential risks such as invalid pointer math, out-of-bounds bitmap accesses, or memory reservation errors are properly handled by KASAN and standard kernel debugging tools. KMSAN is not required.",
  "NeedsKMSAN": false
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)