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

