From: Shivam Kalra <[email protected]>

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 <[email protected]>
---
 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



Reply via email to