| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Memory-failure functional tests. | |
| * | |
| * Author(s): Miaohe Lin <linmiaohe@huawei.com> | |
| */ | |
| #include "../kselftest_harness.h" | |
| #include <sys/mman.h> | |
| #include <linux/mman.h> | |
| #include <linux/string.h> | |
| #include <unistd.h> | |
| #include <signal.h> | |
| #include <setjmp.h> | |
| #include <fcntl.h> | |
| #include <sys/vfs.h> | |
| #include <linux/magic.h> | |
| #include <errno.h> | |
| #include "vm_util.h" | |
| enum inject_type { | |
| MADV_HARD, | |
| MADV_SOFT, | |
| }; | |
| enum result_type { | |
| MADV_HARD_ANON, | |
| MADV_HARD_CLEAN_PAGECACHE, | |
| MADV_HARD_DIRTY_PAGECACHE, | |
| MADV_SOFT_ANON, | |
| MADV_SOFT_CLEAN_PAGECACHE, | |
| MADV_SOFT_DIRTY_PAGECACHE, | |
| }; | |
| static jmp_buf signal_jmp_buf; | |
| static siginfo_t siginfo; | |
| const char *pagemap_proc = "/proc/self/pagemap"; | |
| const char *kpageflags_proc = "/proc/kpageflags"; | |
| FIXTURE(memory_failure) | |
| { | |
| unsigned long page_size; | |
| unsigned long corrupted_size; | |
| unsigned long pfn; | |
| int pagemap_fd; | |
| int kpageflags_fd; | |
| bool injection_attempted; | |
| }; | |
| FIXTURE_VARIANT(memory_failure) | |
| { | |
| enum inject_type type; | |
| int (*inject)(FIXTURE_DATA(memory_failure) * self, void *vaddr); | |
| }; | |
| static int madv_hard_inject(FIXTURE_DATA(memory_failure) * self, void *vaddr) | |
| { | |
| return madvise(vaddr, self->page_size, MADV_HWPOISON); | |
| } | |
| FIXTURE_VARIANT_ADD(memory_failure, madv_hard) | |
| { | |
| .type = MADV_HARD, | |
| .inject = madv_hard_inject, | |
| }; | |
| static int madv_soft_inject(FIXTURE_DATA(memory_failure) * self, void *vaddr) | |
| { | |
| return madvise(vaddr, self->page_size, MADV_SOFT_OFFLINE); | |
| } | |
| FIXTURE_VARIANT_ADD(memory_failure, madv_soft) | |
| { | |
| .type = MADV_SOFT, | |
| .inject = madv_soft_inject, | |
| }; | |
| static void sigbus_action(int signo, siginfo_t *si, void *args) | |
| { | |
| memcpy(&siginfo, si, sizeof(siginfo_t)); | |
| siglongjmp(signal_jmp_buf, 1); | |
| } | |
| static int setup_sighandler(void) | |
| { | |
| struct sigaction sa = { | |
| .sa_sigaction = sigbus_action, | |
| .sa_flags = SA_SIGINFO, | |
| }; | |
| return sigaction(SIGBUS, &sa, NULL); | |
| } | |
| FIXTURE_SETUP(memory_failure) | |
| { | |
| memset(self, 0, sizeof(*self)); | |
| self->page_size = (unsigned long)sysconf(_SC_PAGESIZE); | |
| memset(&siginfo, 0, sizeof(siginfo)); | |
| if (setup_sighandler()) | |
| SKIP(return, "setup sighandler failed.\n"); | |
| self->pagemap_fd = open(pagemap_proc, O_RDONLY); | |
| if (self->pagemap_fd == -1) | |
| SKIP(return, "open %s failed.\n", pagemap_proc); | |
| self->kpageflags_fd = open(kpageflags_proc, O_RDONLY); | |
| if (self->kpageflags_fd == -1) | |
| SKIP(return, "open %s failed.\n", kpageflags_proc); | |
| } | |
| static void teardown_sighandler(void) | |
| { | |
| struct sigaction sa = { | |
| .sa_handler = SIG_DFL, | |
| .sa_flags = SA_SIGINFO, | |
| }; | |
| sigaction(SIGBUS, &sa, NULL); | |
| } | |
| static void prepare(struct __test_metadata *_metadata, FIXTURE_DATA(memory_failure) * self, | |
| void *vaddr) | |
| { | |
| self->pfn = pagemap_get_pfn(self->pagemap_fd, vaddr); | |
| ASSERT_NE(self->pfn, -1UL); | |
| ASSERT_EQ(get_hardware_corrupted_size(&self->corrupted_size), 0); | |
| } | |
| static bool check_memory(void *vaddr, unsigned long size) | |
| { | |
| char buf[64]; | |
| memset(buf, 0xce, sizeof(buf)); | |
| while (size >= sizeof(buf)) { | |
| if (memcmp(vaddr, buf, sizeof(buf))) | |
| return false; | |
| size -= sizeof(buf); | |
| vaddr += sizeof(buf); | |
| } | |
| return true; | |
| } | |
| static void check(struct __test_metadata *_metadata, FIXTURE_DATA(memory_failure) * self, | |
| void *vaddr, enum result_type type, int setjmp) | |
| { | |
| unsigned long size; | |
| uint64_t pfn_flags; | |
| switch (type) { | |
| case MADV_SOFT_ANON: | |
| case MADV_HARD_CLEAN_PAGECACHE: | |
| case MADV_SOFT_CLEAN_PAGECACHE: | |
| case MADV_SOFT_DIRTY_PAGECACHE: | |
| /* It is not expected to receive a SIGBUS signal. */ | |
| ASSERT_EQ(setjmp, 0); | |
| /* The page content should remain unchanged. */ | |
| ASSERT_TRUE(check_memory(vaddr, self->page_size)); | |
| /* The backing pfn of addr should have changed. */ | |
| ASSERT_NE(pagemap_get_pfn(self->pagemap_fd, vaddr), self->pfn); | |
| break; | |
| case MADV_HARD_ANON: | |
| case MADV_HARD_DIRTY_PAGECACHE: | |
| /* The SIGBUS signal should have been received. */ | |
| ASSERT_EQ(setjmp, 1); | |
| /* Check if siginfo contains correct SIGBUS context. */ | |
| ASSERT_EQ(siginfo.si_signo, SIGBUS); | |
| ASSERT_EQ(siginfo.si_code, BUS_MCEERR_AR); | |
| ASSERT_EQ(1UL << siginfo.si_addr_lsb, self->page_size); | |
| ASSERT_EQ(siginfo.si_addr, vaddr); | |
| /* XXX Check backing pte is hwpoison entry when supported. */ | |
| ASSERT_TRUE(pagemap_is_swapped(self->pagemap_fd, vaddr)); | |
| break; | |
| default: | |
| SKIP(return, "unexpected inject type %d.\n", type); | |
| } | |
| /* Check if the value of HardwareCorrupted has increased. */ | |
| ASSERT_EQ(get_hardware_corrupted_size(&size), 0); | |
| ASSERT_EQ(size, self->corrupted_size + self->page_size / 1024); | |
| /* Check if HWPoison flag is set. */ | |
| ASSERT_EQ(pageflags_get(self->pfn, self->kpageflags_fd, &pfn_flags), 0); | |
| ASSERT_EQ(pfn_flags & KPF_HWPOISON, KPF_HWPOISON); | |
| } | |
| static void cleanup(struct __test_metadata *_metadata, FIXTURE_DATA(memory_failure) * self) | |
| { | |
| unsigned long size; | |
| uint64_t pfn_flags; | |
| ASSERT_EQ(unpoison_memory(self->pfn), 0); | |
| /* Check if HWPoison flag is cleared. */ | |
| ASSERT_EQ(pageflags_get(self->pfn, self->kpageflags_fd, &pfn_flags), 0); | |
| ASSERT_NE(pfn_flags & KPF_HWPOISON, KPF_HWPOISON); | |
| /* Check if the value of HardwareCorrupted has decreased. */ | |
| ASSERT_EQ(get_hardware_corrupted_size(&size), 0); | |
| ASSERT_EQ(size, self->corrupted_size); | |
| } | |
| FIXTURE_TEARDOWN(memory_failure) | |
| { | |
| /* | |
| * Injection may poison the page before failing or delivering SIGBUS, so | |
| * clean up after every injection attempt. | |
| */ | |
| if (self->injection_attempted) | |
| cleanup(_metadata, self); | |
| close(self->kpageflags_fd); | |
| close(self->pagemap_fd); | |
| teardown_sighandler(); | |
| } | |
| TEST_F(memory_failure, anon) | |
| { | |
| char *addr; | |
| int ret; | |
| addr = mmap(0, self->page_size, PROT_READ | PROT_WRITE, | |
| MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); | |
| if (addr == MAP_FAILED) | |
| SKIP(return, "mmap failed, not enough memory.\n"); | |
| memset(addr, 0xce, self->page_size); | |
| prepare(_metadata, self, addr); | |
| ret = sigsetjmp(signal_jmp_buf, 1); | |
| if (!self->injection_attempted) { | |
| self->injection_attempted = true; | |
| ASSERT_EQ(variant->inject(self, addr), 0); | |
| FORCE_READ(*addr); | |
| } | |
| if (variant->type == MADV_HARD) | |
| check(_metadata, self, addr, MADV_HARD_ANON, ret); | |
| else | |
| check(_metadata, self, addr, MADV_SOFT_ANON, ret); | |
| ASSERT_EQ(munmap(addr, self->page_size), 0); | |
| } | |
| static int prepare_file(const char *fname, unsigned long size) | |
| { | |
| int fd; | |
| fd = open(fname, O_RDWR | O_CREAT, 0664); | |
| if (fd >= 0) { | |
| unlink(fname); | |
| ftruncate(fd, size); | |
| } | |
| return fd; | |
| } | |
| /* Borrowed from mm/gup_longterm.c. */ | |
| static int get_fs_type(int fd) | |
| { | |
| struct statfs fs; | |
| int ret; | |
| do { | |
| ret = fstatfs(fd, &fs); | |
| } while (ret && errno == EINTR); | |
| return ret ? 0 : (int)fs.f_type; | |
| } | |
| TEST_F(memory_failure, clean_pagecache) | |
| { | |
| int fd; | |
| char *addr; | |
| int ret; | |
| int fs_type; | |
| fd = prepare_file("./clean-page-cache-test-file", self->page_size); | |
| if (fd < 0) | |
| SKIP(return, "failed to open test file.\n"); | |
| fs_type = get_fs_type(fd); | |
| if (!fs_type || fs_type == TMPFS_MAGIC) { | |
| close(fd); | |
| SKIP(return, "unsupported filesystem :%x\n", fs_type); | |
| } | |
| addr = mmap(0, self->page_size, PROT_READ | PROT_WRITE, | |
| MAP_SHARED, fd, 0); | |
| if (addr == MAP_FAILED) | |
| SKIP(return, "mmap failed, not enough memory.\n"); | |
| memset(addr, 0xce, self->page_size); | |
| fsync(fd); | |
| prepare(_metadata, self, addr); | |
| ret = sigsetjmp(signal_jmp_buf, 1); | |
| if (!self->injection_attempted) { | |
| self->injection_attempted = true; | |
| ASSERT_EQ(variant->inject(self, addr), 0); | |
| FORCE_READ(*addr); | |
| } | |
| if (variant->type == MADV_HARD) | |
| check(_metadata, self, addr, MADV_HARD_CLEAN_PAGECACHE, ret); | |
| else | |
| check(_metadata, self, addr, MADV_SOFT_CLEAN_PAGECACHE, ret); | |
| ASSERT_EQ(munmap(addr, self->page_size), 0); | |
| ASSERT_EQ(close(fd), 0); | |
| } | |
| TEST_F(memory_failure, dirty_pagecache) | |
| { | |
| int fd; | |
| char *addr; | |
| int ret; | |
| int fs_type; | |
| fd = prepare_file("./dirty-page-cache-test-file", self->page_size); | |
| if (fd < 0) | |
| SKIP(return, "failed to open test file.\n"); | |
| fs_type = get_fs_type(fd); | |
| /* | |
| * MADV_HARD poisoning of dirty page-cache data records an expected | |
| * -EIO in the file mapping. NFS reports this error on close(), so | |
| * skip this variant. | |
| */ | |
| if (!fs_type || fs_type == TMPFS_MAGIC || | |
| (fs_type == NFS_SUPER_MAGIC && variant->type == MADV_HARD)) { | |
| close(fd); | |
| SKIP(return, "unsupported filesystem :%x\n", fs_type); | |
| } | |
| addr = mmap(0, self->page_size, PROT_READ | PROT_WRITE, | |
| MAP_SHARED, fd, 0); | |
| if (addr == MAP_FAILED) | |
| SKIP(return, "mmap failed, not enough memory.\n"); | |
| memset(addr, 0xce, self->page_size); | |
| prepare(_metadata, self, addr); | |
| ret = sigsetjmp(signal_jmp_buf, 1); | |
| if (!self->injection_attempted) { | |
| self->injection_attempted = true; | |
| ASSERT_EQ(variant->inject(self, addr), 0); | |
| FORCE_READ(*addr); | |
| } | |
| if (variant->type == MADV_HARD) | |
| check(_metadata, self, addr, MADV_HARD_DIRTY_PAGECACHE, ret); | |
| else | |
| check(_metadata, self, addr, MADV_SOFT_DIRTY_PAGECACHE, ret); | |
| ASSERT_EQ(munmap(addr, self->page_size), 0); | |
| ASSERT_EQ(close(fd), 0); | |
| } | |
| TEST_HARNESS_MAIN |