[PATCH 3/3] selftests/mm: test hwpoison recovery of mappingless swapcache THPs
From: Shivam Kalra via B4 Relay
Date: Wed Aug 05 2026 - 07:23:32 EST
From: Shivam Kalra <shivamkalra98@xxxxxxxxxxx>
Exercise memory_failure() on a tail page of a shmem THP after MADV_PAGEOUT
has made it a mappingless swapcache folio. Verify that the folio is split,
only the target PFN is poisoned, and the remaining mapping can be faulted
back in with its original data.
Use a temporary loop-backed swap device so the test also works when the
kselftest directory is on 9p or NFS. Register the wrapper in the
memory-failure test suite and clean up the loop device and swap file on
every exit path.
Signed-off-by: Shivam Kalra <shivamkalra98@xxxxxxxxxxx>
---
tools/testing/selftests/mm/Makefile | 2 +
tools/testing/selftests/mm/run_vmtests.sh | 1 +
.../selftests/mm/split_hwpoison_swapcache.sh | 57 +++++
.../selftests/mm/split_hwpoison_swapcache_test.c | 261 +++++++++++++++++++++
4 files changed, 321 insertions(+)
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index 2d5366196e30..f0ae0b5685a6 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -93,6 +93,7 @@ TEST_GEN_FILES += uffd-stress
TEST_GEN_FILES += uffd-unit-tests
TEST_GEN_FILES += uffd-wp-mremap
TEST_GEN_FILES += split_huge_page_test
+TEST_GEN_FILES += split_hwpoison_swapcache_test
TEST_GEN_FILES += ksm_tests
TEST_GEN_FILES += ksm_functional_tests
TEST_GEN_FILES += mdwe_test
@@ -177,6 +178,7 @@ TEST_PROGS += ksft_vmalloc.sh
TEST_FILES := test_vmalloc.sh
TEST_FILES += test_hmm.sh
TEST_FILES += va_high_addr_switch.sh
+TEST_FILES += split_hwpoison_swapcache.sh
TEST_FILES += charge_reserved_hugetlb.sh
TEST_FILES += hugetlb_reparenting_test.sh
TEST_FILES += test_page_frag.sh
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 687d115e3bd8..39c8cf92ccc1 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -459,6 +459,7 @@ CATEGORY="page_frag" run_test ./test_page_frag.sh nonaligned
CATEGORY="rmap" run_test ./rmap
CATEGORY="memory-failure" run_test ./memory-failure
+CATEGORY="memory-failure" run_test ./split_hwpoison_swapcache.sh
echo "SUMMARY: PASS=${count_pass} SKIP=${count_skip} FAIL=${count_fail}" | tap_prefix
echo "1..${count_total}" | tap_output
diff --git a/tools/testing/selftests/mm/split_hwpoison_swapcache.sh b/tools/testing/selftests/mm/split_hwpoison_swapcache.sh
new file mode 100755
index 000000000000..fe986a09f697
--- /dev/null
+++ b/tools/testing/selftests/mm/split_hwpoison_swapcache.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Setup swap and run the mappingless swapcache hwpoison split test
+
+ksft_skip=4
+swap_file=
+loop_dev=
+
+skip() {
+ echo "skip: $*"
+ exit "$ksft_skip"
+}
+
+cleanup() {
+ if [ -n "$loop_dev" ]; then
+ swapoff "$loop_dev" 2>/dev/null || true
+ losetup -d "$loop_dev" 2>/dev/null || true
+ fi
+ if [ -n "$swap_file" ]; then
+ rm -f "$swap_file"
+ fi
+}
+
+trap cleanup EXIT
+trap 'exit 1' HUP INT TERM
+
+if [ "$(id -u)" -ne 0 ]; then
+ skip "must run as root"
+fi
+
+if [ ! -w /sys/kernel/debug/hwpoison/corrupt-pfn ]; then
+ skip "hwpoison injection is not available"
+fi
+
+# Use a loop device because the kselftest directory may be on 9p or NFS.
+if ! swap_file=$(mktemp /tmp/hwpoison_swap.XXXXXX); then
+ echo "FAIL: could not create a temporary swap file"
+ exit 1
+fi
+if ! dd if=/dev/zero of="$swap_file" bs=1M count=128 status=none; then
+ echo "FAIL: could not initialize the temporary swap file"
+ exit 1
+fi
+if ! loop_dev=$(losetup --find --show "$swap_file"); then
+ skip "no loop device is available"
+fi
+if ! mkswap "$loop_dev" >/dev/null; then
+ echo "FAIL: could not initialize swap on $loop_dev"
+ exit 1
+fi
+if ! swapon "$loop_dev"; then
+ echo "FAIL: could not enable swap on $loop_dev"
+ exit 1
+fi
+
+"$(dirname "$(readlink -f "$0")")"/split_hwpoison_swapcache_test
diff --git a/tools/testing/selftests/mm/split_hwpoison_swapcache_test.c b/tools/testing/selftests/mm/split_hwpoison_swapcache_test.c
new file mode 100644
index 000000000000..2d956ae517cf
--- /dev/null
+++ b/tools/testing/selftests/mm/split_hwpoison_swapcache_test.c
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test that memory failure can split a mappingless shmem THP in swap cache.
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "../kselftest.h"
+#include "hugepage_settings.h"
+#include "vm_util.h"
+
+#define HWPOISON_PATH "/sys/kernel/debug/hwpoison/corrupt-pfn"
+
+#define KPF_MMAP BIT_ULL(11)
+#define KPF_SWAPCACHE BIT_ULL(13)
+#define KPF_SWAPBACKED BIT_ULL(14)
+
+#define TEST_BYTE 0x5a
+#define PAGEOUT_RETRIES 100
+#define PAGEOUT_DELAY_US 100000
+
+static bool is_swapcache_thp(uint64_t flags, bool head)
+{
+ uint64_t required = KPF_SWAPCACHE | KPF_SWAPBACKED | KPF_THP;
+
+ required |= head ? KPF_COMPOUND_HEAD : KPF_COMPOUND_TAIL;
+ return (flags & required) == required && !(flags & KPF_MMAP);
+}
+
+static int wait_for_swapcache_thp(unsigned long head_pfn, unsigned long target_pfn,
+ int kpageflags_fd)
+{
+ uint64_t head_flags = 0;
+ uint64_t target_flags = 0;
+ int i;
+
+ for (i = 0; i < PAGEOUT_RETRIES; i++) {
+ if (pageflags_get(head_pfn, kpageflags_fd, &head_flags) ||
+ pageflags_get(target_pfn, kpageflags_fd, &target_flags))
+ return -1;
+
+ if (is_swapcache_thp(head_flags, true) &&
+ is_swapcache_thp(target_flags, false))
+ return 0;
+
+ usleep(PAGEOUT_DELAY_US);
+ }
+
+ ksft_print_msg("Swapcache THP flags: head=%#llx target=%#llx\n",
+ (unsigned long long)head_flags,
+ (unsigned long long)target_flags);
+ return 1;
+}
+
+static bool folio_was_split(unsigned long head_pfn, unsigned long target_pfn,
+ unsigned long nr_pages, int kpageflags_fd)
+{
+ const uint64_t compound = KPF_COMPOUND_HEAD | KPF_COMPOUND_TAIL;
+ uint64_t flags;
+ unsigned long i;
+
+ for (i = 0; i < nr_pages; i++) {
+ if (pageflags_get(head_pfn + i, kpageflags_fd, &flags))
+ return false;
+ if (flags & compound) {
+ ksft_print_msg("PFN %#lx is still compound (flags=%#llx)\n",
+ head_pfn + i,
+ (unsigned long long)flags);
+ return false;
+ }
+ if ((head_pfn + i == target_pfn) != !!(flags & KPF_HWPOISON)) {
+ ksft_print_msg(
+ "Unexpected HWPoison state at PFN %#lx (flags=%#llx)\n",
+ head_pfn + i, (unsigned long long)flags);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool mapping_has_expected_data(const unsigned char *addr, size_t size)
+{
+ size_t i;
+
+ for (i = 0; i < size; i++) {
+ if (addr[i] != TEST_BYTE) {
+ ksft_print_msg("Data mismatch at offset %#zx: %#x != %#x\n",
+ i, addr[i], TEST_BYTE);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static int inject_hwpoison(unsigned long pfn)
+{
+ char buf[32];
+ ssize_t written;
+ int fd;
+ int len;
+ int saved_errno;
+
+ fd = open(HWPOISON_PATH, O_WRONLY);
+ if (fd < 0)
+ return -errno;
+
+ len = snprintf(buf, sizeof(buf), "%#lx\n", pfn);
+ written = write(fd, buf, len);
+ saved_errno = errno;
+ close(fd);
+
+ if (written != len)
+ return written < 0 ? -saved_errno : -EIO;
+
+ return 0;
+}
+
+int main(void)
+{
+ struct thp_settings settings;
+ unsigned long target_pfn;
+ unsigned long head_pfn;
+ unsigned long nr_pages;
+ unsigned long page_size;
+ unsigned long pmd_size;
+ unsigned char *mapping;
+ unsigned char *addr;
+ uint64_t flags;
+ bool poisoned = false;
+ bool pass = false;
+ int kpageflags_fd = -1;
+ int pagemap_fd = -1;
+ int memfd = -1;
+ int ret;
+
+ ksft_print_header();
+ ksft_set_plan(1);
+
+ if (geteuid())
+ ksft_exit_skip("Please run the test as root\n");
+
+ pmd_size = read_pmd_pagesize();
+ if (!thp_available() || !pmd_size)
+ ksft_exit_skip("Transparent Huge Pages are not available\n");
+
+ if (access(HWPOISON_PATH, W_OK))
+ ksft_exit_skip("HWPoison injection is not available\n");
+
+ page_size = getpagesize();
+ if (pmd_size % page_size)
+ ksft_exit_fail_msg("Invalid PMD page size %#lx\n", pmd_size);
+ nr_pages = pmd_size / page_size;
+
+ thp_save_settings();
+ thp_read_settings(&settings);
+ settings.shmem_enabled = SHMEM_ADVISE;
+ thp_write_settings(&settings);
+
+ memfd = memfd_create("split_hwpoison_swapcache", MFD_CLOEXEC);
+ if (memfd < 0)
+ ksft_exit_fail_perror("memfd_create");
+ if (ftruncate(memfd, pmd_size))
+ ksft_exit_fail_perror("ftruncate");
+
+ /* Reserve enough space to obtain a PMD-aligned file mapping. */
+ mapping = mmap(NULL, 2 * pmd_size, PROT_NONE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (mapping == MAP_FAILED)
+ ksft_exit_fail_perror("mmap");
+ addr = (unsigned char *)(((uintptr_t)mapping + pmd_size - 1) &
+ ~(pmd_size - 1));
+ if (mmap(addr, pmd_size, PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_FIXED, memfd, 0) == MAP_FAILED)
+ ksft_exit_fail_perror("mmap");
+
+ if (madvise(addr, pmd_size, MADV_HUGEPAGE))
+ ksft_exit_fail_perror("madvise(MADV_HUGEPAGE)");
+ memset(addr, TEST_BYTE, pmd_size);
+
+ if (!check_huge_shmem(addr, 1, pmd_size))
+ ksft_exit_skip("Failed to allocate a PMD-sized shmem THP\n");
+
+ pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+ if (pagemap_fd < 0)
+ ksft_exit_fail_perror("open(/proc/self/pagemap)");
+ kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
+ if (kpageflags_fd < 0)
+ ksft_exit_fail_perror("open(/proc/kpageflags)");
+
+ head_pfn = pagemap_get_pfn(pagemap_fd, (char *)addr);
+ if (head_pfn == -1UL)
+ ksft_exit_fail_msg("Failed to obtain the shmem THP PFN\n");
+
+ /* Poison a tail page so success necessarily requires a real split. */
+ target_pfn = head_pfn + nr_pages / 2;
+ if (pageflags_get(head_pfn, kpageflags_fd, &flags) ||
+ (flags & (KPF_THP | KPF_COMPOUND_HEAD)) !=
+ (KPF_THP | KPF_COMPOUND_HEAD))
+ ksft_exit_fail_msg("PFN %#lx is not a THP head\n", head_pfn);
+ if (pageflags_get(target_pfn, kpageflags_fd, &flags) ||
+ (flags & (KPF_THP | KPF_COMPOUND_TAIL)) !=
+ (KPF_THP | KPF_COMPOUND_TAIL))
+ ksft_exit_fail_msg("PFN %#lx is not a THP tail\n", target_pfn);
+
+ if (madvise(addr, pmd_size, MADV_PAGEOUT))
+ ksft_exit_skip("madvise(MADV_PAGEOUT) failed: %s\n",
+ strerror(errno));
+
+ ret = wait_for_swapcache_thp(head_pfn, target_pfn, kpageflags_fd);
+ if (ret < 0)
+ ksft_exit_fail_msg("Failed to read kpageflags\n");
+ if (ret > 0)
+ ksft_exit_skip("Failed to create a mappingless swapcache THP; "
+ "is swap enabled?\n");
+
+ ksft_print_msg("Injecting HWPoison into tail PFN %#lx of THP %#lx\n",
+ target_pfn, head_pfn);
+ ret = inject_hwpoison(target_pfn);
+ poisoned = true;
+ if (ret) {
+ ksft_print_msg("HWPoison injection failed: %s\n", strerror(-ret));
+ goto out;
+ }
+
+ if (!folio_was_split(head_pfn, target_pfn, nr_pages, kpageflags_fd))
+ goto out;
+
+ /*
+ * A clean poisoned swapcache page is discarded. Faulting the mapping
+ * back in must recover the original data from swap.
+ */
+ if (!mapping_has_expected_data(addr, pmd_size))
+ goto out;
+
+ pass = true;
+out:
+ if (poisoned && unpoison_memory(target_pfn)) {
+ ksft_print_msg("Failed to unpoison PFN %#lx\n", target_pfn);
+ pass = false;
+ }
+ if (kpageflags_fd >= 0)
+ close(kpageflags_fd);
+ if (pagemap_fd >= 0)
+ close(pagemap_fd);
+ munmap(mapping, 2 * pmd_size);
+ close(memfd);
+
+ ksft_test_result(pass, "memory failure splits a mappingless swapcache THP\n");
+ ksft_finished();
+}
--
2.43.0