Add self tests for Page Detective, it contains testing of several memory types, and also some negative/bad input tests. Signed-off-by: Pasha Tatashin <pasha.tatashin@xxxxxxxxxx> --- MAINTAINERS | 1 + tools/testing/selftests/Makefile | 1 + .../selftests/page_detective/.gitignore | 1 + .../testing/selftests/page_detective/Makefile | 7 + tools/testing/selftests/page_detective/config | 4 + .../page_detective/page_detective_test.c | 727 ++++++++++++++++++ 6 files changed, 741 insertions(+) create mode 100644 tools/testing/selftests/page_detective/.gitignore create mode 100644 tools/testing/selftests/page_detective/Makefile create mode 100644 tools/testing/selftests/page_detective/config create mode 100644 tools/testing/selftests/page_detective/page_detective_test.c diff --git a/MAINTAINERS b/MAINTAINERS index 654d4650670d..ec09b28776b0 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -17456,6 +17456,7 @@ L: linux-kernel@xxxxxxxxxxxxxxx S: Maintained F: Documentation/misc-devices/page_detective.rst F: drivers/misc/page_detective.c +F: tools/testing/selftests/page_detective/ PAGE POOL M: Jesper Dangaard Brouer <hawk@xxxxxxxxxx> diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index 363d031a16f7..9c828025fdfa 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -72,6 +72,7 @@ TARGETS += net/packetdrill TARGETS += net/rds TARGETS += net/tcp_ao TARGETS += nsfs +TARGETS += page_detective TARGETS += perf_events TARGETS += pidfd TARGETS += pid_namespace diff --git a/tools/testing/selftests/page_detective/.gitignore b/tools/testing/selftests/page_detective/.gitignore new file mode 100644 index 000000000000..21a78bee7b4a --- /dev/null +++ b/tools/testing/selftests/page_detective/.gitignore @@ -0,0 +1 @@ +page_detective_test diff --git a/tools/testing/selftests/page_detective/Makefile b/tools/testing/selftests/page_detective/Makefile new file mode 100644 index 000000000000..43c4dccb6a13 --- /dev/null +++ b/tools/testing/selftests/page_detective/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +CFLAGS += -g -Wall + +TEST_GEN_PROGS := page_detective_test + +include ../lib.mk + diff --git a/tools/testing/selftests/page_detective/config b/tools/testing/selftests/page_detective/config new file mode 100644 index 000000000000..ddfeed4ddf13 --- /dev/null +++ b/tools/testing/selftests/page_detective/config @@ -0,0 +1,4 @@ +CONFIG_PAGE_TABLE_CHECK=y +CONFIG_MEMCG=y +CONFIG_TRANSPARENT_HUGEPAGE=y +CONFIG_PAGE_DETECTIVE=y diff --git a/tools/testing/selftests/page_detective/page_detective_test.c b/tools/testing/selftests/page_detective/page_detective_test.c new file mode 100644 index 000000000000..f86cf0fdd8fc --- /dev/null +++ b/tools/testing/selftests/page_detective/page_detective_test.c @@ -0,0 +1,727 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2024, Google LLC. + * Pasha Tatashin <pasha.tatashin@xxxxxxxxxx> + */ + +#define _GNU_SOURCE +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <stdint.h> +#include <time.h> +#include <sys/mman.h> +#include <alloca.h> +#include "../kselftest.h" + +#define OPT_STR "hpvaAsnHtbBS" +#define HELP_STR \ +"Usage: %s [-h] [-p] [-v] [-a] [-A] [-s] [-n] [-H] [-t] [-b] [-S]\n" \ +"-h\tshow this help\n" \ +"Interfaces:\n" \ +"-p\tphysical address page detective interface\n" \ +"-v\tvirtual address page detective interface\n" \ +"Tests:\n" \ +"-a\ttest anonymous page\n" \ +"-A\ttest anonymous huge page\n" \ +"-s\ttest anonymous shared page\n" \ +"-n\ttest named shared page\n" \ +"-H\ttest HugeTLB shared page\n" \ +"-t\ttest tmpfs page\n" \ +"-b\ttest bad/fail input cases\n" \ +"-S\ttest stack page\n" \ +"If no arguments specified all tests are performed\n" \ + +#define FIRST_LINE_PREFIX "Page Detective: Investigating " +#define LAST_LINE_PREFIX "Page Detective: Finished investigation of " + +#define TMP_FILE "/tmp/page_detective_test.out" + +#define NR_HUGEPAGES "/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages" + +#define NANO 1000000000ul +#define MICRO 1000000ul +#define MILLI 1000ul +#define BIT(nr) (UL(1) << (nr)) + +#define ARG_INTERFACE_PHYS BIT(0) +#define ARG_INTERFACE_VIRT BIT(1) + +#define ARG_TEST_ANON BIT(2) +#define ARG_TEST_ANON_HUGE BIT(3) +#define ARG_TEST_ANON_SHARED BIT(4) +#define ARG_TEST_NAMED_SHARED BIT(5) +#define ARG_TEST_HUGETLB_SHARED BIT(6) +#define ARG_TEST_TMPFS BIT(7) +#define ARG_TEST_FAIL_CASES BIT(8) +#define ARG_TEST_STACK BIT(9) + +#define ARG_DEFAULT (~0) /* Run verything by default */ + +#define ARG_INTERFACE_MASK (ARG_INTERFACE_PHYS | ARG_INTERFACE_VIRT) +#define ARG_TEST_MASK (~ARG_INTERFACE_MASK) + +#define INTERFACE_NAME(in) (((in) == ARG_INTERFACE_PHYS) ? \ + "/sys/kernel/debug/page_detective/phys" : \ + "/sys/kernel/debug/page_detective/virt") + +#define PAGE_SIZE ((unsigned long)sysconf(_SC_PAGESIZE)) +#define HUGE_PAGE_SIZE (PAGE_SIZE * PAGE_SIZE / sizeof(uint64_t)) + +#ifndef MAP_HUGE_2MB +#define MAP_HUGE_2MB (21 << MAP_HUGE_SHIFT) +#endif + +#ifndef MFD_HUGEPAGE +#define MFD_HUGEPAGE (MFD_GOOGLE_SPECIFIC_BASE << 0) +#endif + +#ifndef MFD_GOOGLE_SPECIFIC_BASE +#define MFD_GOOGLE_SPECIFIC_BASE 0x0200U +#endif + +static int old_dmesg; + +static uint64_t virt_to_phys(uint64_t virt, uint64_t *physp) +{ + uint64_t tbloff, offset, tblen, pfn; + int fd, nr; + + fd = open("/proc/self/pagemap", O_RDONLY); + if (fd < 0) { + ksft_print_msg("%s open(/proc/self/pagemap): %s\n", __func__, + strerror(errno)); + return 1; + } + + /* see: Documentation/admin-guide/mm/pagemap.rst */ + tbloff = virt / PAGE_SIZE * sizeof(uint64_t); + offset = lseek(fd, tbloff, SEEK_SET); + if (offset == (off_t)-1) { + ksft_print_msg("%s lseek: %s\n", __func__, strerror(errno)); + return 1; + } + + if (offset != tbloff) { + ksft_print_msg("%s: cannot find virtual address\n", __func__); + return 1; + } + + nr = read(fd, &tblen, sizeof(uint64_t)); + if (nr != sizeof(uint64_t)) { + ksft_print_msg("%s: read\n", __func__); + return 1; + } + close(fd); + + if (!(tblen & (1ul << 63))) { + ksft_print_msg("%s: present bit is not set\n", __func__); + return 1; + } + + /* Bits 0-54 page frame number (PFN) if present */ + pfn = tblen & 0x7fffffffffffffULL; + + *physp = PAGE_SIZE * pfn | (virt & (PAGE_SIZE - 1)); + + return 0; +} + +static int __phys_test(unsigned long long phys) +{ + char phys_str[128]; + int fd, nr; + + fd = open("/sys/kernel/debug/page_detective/phys", O_WRONLY); + if (fd < 0) { + ksft_print_msg("%s open: %s\n", __func__, strerror(errno)); + return 4; + } + + sprintf(phys_str, "%llu", phys); + + nr = write(fd, phys_str, strlen(phys_str)); + if (nr != strlen(phys_str)) { + ksft_print_msg("%s write failed\n", __func__); + return 1; + } + close(fd); + + return 0; +} + +static int phys_test(char *mem) +{ + uint64_t phys; + + if (virt_to_phys((uint64_t)mem, &phys)) + return 1; + + return __phys_test(phys); +} + +static int __virt_test(int pid, unsigned long virt) +{ + char virt_str[128]; + int fd, nr; + + fd = open("/sys/kernel/debug/page_detective/virt", O_WRONLY); + if (fd < 0) { + ksft_print_msg("%s open: %s\n", __func__, strerror(errno)); + return 4; + } + + sprintf(virt_str, "%d %#lx", pid, virt); + nr = write(fd, virt_str, strlen(virt_str)); + if (nr != strlen(virt_str)) { + ksft_print_msg("%s: write(%s): %s\n", __func__, virt_str, + strerror(errno)); + close(fd); + + return 1; + } + close(fd); + + return 0; +} + +static int virt_test(char *mem) +{ + return __virt_test(getpid(), (unsigned long)mem); +} + +static int test_anon(int in) +{ + char *mem; + int rv; + + mem = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (mem == MAP_FAILED) { + ksft_print_msg("%s mmap: %s\n", __func__, strerror(errno)); + return 1; + } + + if (madvise(mem, PAGE_SIZE, MADV_NOHUGEPAGE)) { + ksft_print_msg("%s madvise: %s\n", __func__, strerror(errno)); + return 1; + } + + mem[0] = 0; + + if (in == ARG_INTERFACE_PHYS) + rv = phys_test(mem); + else + rv = virt_test(mem); + + munmap(mem, PAGE_SIZE); + + return rv; +} + +static int test_anon_huge(int in) +{ + uint64_t i; + char *mem; + int rv; + + mem = mmap(NULL, HUGE_PAGE_SIZE * 8, PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (mem == MAP_FAILED) { + ksft_print_msg("%s mmap: %s\n", __func__, strerror(errno)); + return 1; + } + + if (madvise(mem, HUGE_PAGE_SIZE * 8, MADV_HUGEPAGE)) { + ksft_print_msg("%s madvise: %s\n", __func__, strerror(errno)); + return 1; + } + + /* Fault huge pages */ + for (i = 0; i < HUGE_PAGE_SIZE * 8; i += HUGE_PAGE_SIZE) + mem[i] = 0; + + /* In case huge pages were not used for some reason */ + mem[HUGE_PAGE_SIZE * 7 + 101 * PAGE_SIZE] = 0; + + if (in == ARG_INTERFACE_PHYS) + rv = phys_test(mem + HUGE_PAGE_SIZE * 7 + 101 * PAGE_SIZE); + else + rv = virt_test(mem + HUGE_PAGE_SIZE * 7 + 101 * PAGE_SIZE); + + munmap(mem, HUGE_PAGE_SIZE * 8); + + return rv; +} + +static int test_anon_shared(int in) +{ + char *mem; + int rv; + + mem = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_SHARED, -1, 0); + if (mem == MAP_FAILED) { + ksft_print_msg("%s mmap: %s\n", __func__, strerror(errno)); + return 1; + } + + if (madvise(mem, PAGE_SIZE, MADV_NOHUGEPAGE)) { + ksft_print_msg("%s madvise: %s\n", __func__, strerror(errno)); + return 1; + } + + mem[0] = 0; + + if (in == ARG_INTERFACE_PHYS) + rv = phys_test(mem); + else + rv = virt_test(mem); + + munmap(mem, PAGE_SIZE); + + return rv; +} + +static int test_named_shared(int in) +{ + char *mem; + int rv; + + mem = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_SHARED, -1, 0); + if (mem == MAP_FAILED) { + ksft_print_msg("%s mmap: %s\n", __func__, strerror(errno)); + return 1; + } + + if (madvise(mem, PAGE_SIZE, MADV_NOHUGEPAGE)) { + ksft_print_msg("%s madvise: %s\n", __func__, strerror(errno)); + return 1; + } + + mem[0] = 0; + + if (in == ARG_INTERFACE_PHYS) + rv = phys_test(mem); + else + rv = virt_test(mem); + + munmap(mem, PAGE_SIZE); + + return rv; +} + +static int test_hugetlb_shared(int in) +{ + char hugepg_add_cmd[256], hugepg_rm_cmd[256]; + unsigned long nr_hugepages; + char *mem; + FILE *f; + int rv; + + f = fopen(NR_HUGEPAGES, "r"); + if (!f) { + ksft_print_msg("%s fopen: %s\n", __func__, strerror(errno)); + return 4; + } + + fscanf(f, "%lu", &nr_hugepages); + fclose(f); + sprintf(hugepg_add_cmd, "echo %lu > " NR_HUGEPAGES, nr_hugepages + 1); + sprintf(hugepg_rm_cmd, "echo %lu > " NR_HUGEPAGES, nr_hugepages); + + if (system(hugepg_add_cmd)) { + ksft_print_msg("%s system(hugepg_add_cmd): %s\n", __func__, + strerror(errno)); + return 4; + } + + mem = mmap(NULL, HUGE_PAGE_SIZE, PROT_READ | PROT_WRITE, + MAP_HUGETLB | MAP_HUGE_2MB | MAP_ANONYMOUS | MAP_SHARED, + -1, 0); + if (mem == MAP_FAILED) { + ksft_print_msg("%s mmap: %s\n", __func__, strerror(errno)); + return 1; + } + + mem[0] = 0; + + if (in == ARG_INTERFACE_PHYS) + rv = phys_test(mem); + else + rv = virt_test(mem); + + munmap(mem, HUGE_PAGE_SIZE); + + if (system(hugepg_rm_cmd)) { + ksft_print_msg("%s system(hugepg_rm_cmd): %s\n", __func__, + strerror(errno)); + return 1; + } + + return rv; +} + +static int test_tmpfs(int in) +{ + char *mem; + int fd; + int rv; + + fd = memfd_create("tmpfs_page", + MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (fd < 0) { + ksft_print_msg("%s memfd_create: %s\n", __func__, + strerror(errno)); + return 1; + } + + if (ftruncate(fd, PAGE_SIZE) == -1) { + ksft_print_msg("%s ftruncate: %s\n", __func__, strerror(errno)); + return 1; + } + + mem = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED, fd, 0); + if (mem == MAP_FAILED) { + ksft_print_msg("%s mmap: %s\n", __func__, strerror(errno)); + return 1; + } + + mem[0] = 0; + + if (in == ARG_INTERFACE_PHYS) + rv = phys_test(mem); + else + rv = virt_test(mem); + + munmap(mem, PAGE_SIZE); + close(fd); + + return rv; +} + +static int test_stack(int in) +{ + char *mem = alloca(PAGE_SIZE); + int rv; + + mem[0] = 0; + if (in == ARG_INTERFACE_PHYS) + rv = phys_test(mem); + else + rv = virt_test(mem); + + return rv; +} + +static int test_bad_phys(void) +{ + int rv; + + rv = __phys_test(0); + if (rv) + return rv; + + rv = __phys_test(1); + if (rv) + return rv; + + rv = __phys_test(~0); + + return rv; +} + +static int test_bad_virt(void) +{ + int rv; + + rv = __virt_test(0, 0); + if (rv == 4) + return rv; + + if (!rv) { + ksft_print_msg("%s: write(0, 0) did not fail\n", __func__); + return 1; + } + + if (!__virt_test(0, -1)) { + ksft_print_msg("%s: write(0, -1) did not fail\n", __func__); + return 1; + } + + if (!__virt_test(0, -1)) { + ksft_print_msg("%s: write(0, -1) did not fail\n", __func__); + return 1; + } + + if (!__virt_test(-1, 0)) { + ksft_print_msg("%s: write(-1, 0) did not fail\n", __func__); + return 1; + } + + return 0; +} + +static int test_fail_cases(int in) +{ + if (in == ARG_INTERFACE_VIRT) + return test_bad_virt(); + else + return test_bad_phys(); +} + +/* Return time in nanosecond since epoch */ +static uint64_t gethrtime(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_REALTIME, &ts); + + return (ts.tv_sec * NANO) + ts.tv_nsec; +} + +static int sanity_check_fail_cases(int in, int test_ttpe, FILE *f) +{ + char *line; + size_t len; + ssize_t nr; + + line = NULL; + len = 0; + while ((nr = getline(&line, &len, f)) != -1) { + char *l = strchr(line, ']') + 2; /* skip time stamp */ + + if (l == (char *)2) + continue; + + ksft_print_msg("%s", l); + } + + free(line); + + return 0; +} + +static int sanity_check_test_result(int in, int test_type, uint64_t start) +{ + uint64_t end = gethrtime() + NANO; + char dmesg[256], *line; + int first_line, last_line; + size_t len; + ssize_t nr; + FILE *f; + + sprintf(dmesg, "dmesg --since=@%ld.%06ld --until=@%ld.%06ld > " + TMP_FILE, start / NANO, (start / MILLI) % MICRO, end / NANO, + (end / MILLI) % MICRO); + + if (old_dmesg) + system("dmesg > " TMP_FILE); + else + system(dmesg); + f = fopen(TMP_FILE, "r"); + if (!f) { + ksft_print_msg("%s: %s", __func__, strerror(errno)); + return 1; + } + + if (test_type == ARG_TEST_FAIL_CASES) { + sanity_check_fail_cases(in, test_type, f); + fclose(f); + unlink(TMP_FILE); + return 0; + } + + line = NULL; + len = 0; + first_line = 0; + last_line = 0; + while ((nr = getline(&line, &len, f)) != -1) { + char *l = strchr(line, ']') + 2; /* skip time stamp */ + + if (l == (char *)2) + continue; + + if (!first_line) { + first_line = !strncmp(l, FIRST_LINE_PREFIX, + strlen(FIRST_LINE_PREFIX)); + } else if (!last_line) { + last_line = !strncmp(l, LAST_LINE_PREFIX, + strlen(LAST_LINE_PREFIX)); + if (last_line) + ksft_print_msg("%s", l); + } + + /* + * Output everything between the first and last line of page + * detective output + */ + if (first_line && !last_line) + ksft_print_msg("%s", l); + } + ksft_print_msg("\n"); + + free(line); + fclose(f); + unlink(TMP_FILE); + + if (!first_line) { + ksft_print_msg("Test failed, bad first line\n"); + return 1; + } + if (!last_line) { + ksft_print_msg("Test failed, bad last line\n"); + return 1; + } + + return 0; +} + +/* + * interface ARG_INTERFACE_VIRT or ARG_INTERFACE_PHYS + * test_arg one of the ARG_TEST_* + * test_f function for this test + * arg the provided user arguments. + * + * Run the test using the provided interface if it is requested interface arg. + */ +#define TEST(interface, test_type, test_f, arg) \ + do { \ + int __in = (interface); \ + int __tt = (test_type); \ + int __rv; \ + \ + if ((arg) & __tt) { \ + uint64_t start = gethrtime(); \ + \ + if (old_dmesg) \ + system("dmesg -C"); \ + else \ + usleep(100000); \ + \ + __rv = test_f(__in); \ + if (__rv == 4) { \ + ksft_test_result_skip(#test_f " via [%s]\n", \ + INTERFACE_NAME(__in)); \ + break; \ + } \ + \ + if (__rv) { \ + ksft_test_result_fail(#test_f " via [%s]\n", \ + INTERFACE_NAME(__in)); \ + break; \ + } \ + \ + if (sanity_check_test_result(__in, __tt, \ + start)) { \ + ksft_test_result_fail(#test_f " via [%s]\n", \ + INTERFACE_NAME(__in)); \ + break; \ + } \ + ksft_test_result_pass(#test_f " via [%s]\n", \ + INTERFACE_NAME(__in)); \ + } \ + } while (0) + +static void run_tests(int in, int arg) +{ + if (in & arg) { + TEST(in, ARG_TEST_ANON, test_anon, arg); + TEST(in, ARG_TEST_ANON_HUGE, test_anon_huge, arg); + TEST(in, ARG_TEST_ANON_SHARED, test_anon_shared, arg); + TEST(in, ARG_TEST_NAMED_SHARED, test_named_shared, arg); + TEST(in, ARG_TEST_HUGETLB_SHARED, test_hugetlb_shared, arg); + TEST(in, ARG_TEST_TMPFS, test_tmpfs, arg); + TEST(in, ARG_TEST_STACK, test_stack, arg); + TEST(in, ARG_TEST_FAIL_CASES, test_fail_cases, arg); + } +} + +static int count_tests(int in, int arg) +{ + int tests = 0; + + if (in & arg) { + tests += (arg & ARG_TEST_ANON) ? 1 : 0; + tests += (arg & ARG_TEST_ANON_HUGE) ? 1 : 0; + tests += (arg & ARG_TEST_ANON_SHARED) ? 1 : 0; + tests += (arg & ARG_TEST_NAMED_SHARED) ? 1 : 0; + tests += (arg & ARG_TEST_HUGETLB_SHARED) ? 1 : 0; + tests += (arg & ARG_TEST_TMPFS) ? 1 : 0; + tests += (arg & ARG_TEST_STACK) ? 1 : 0; + tests += (arg & ARG_TEST_FAIL_CASES) ? 1 : 0; + } + + return tests; +} + +int main(int argc, char **argv) +{ + int arg = 0; + int opt; + + while ((opt = getopt(argc, argv, OPT_STR)) != -1) { + switch (opt) { + case 'h': + printf(HELP_STR, argv[0]); + exit(EXIT_SUCCESS); + case 'v': + arg |= ARG_INTERFACE_VIRT; + break; + case 'p': + arg |= ARG_INTERFACE_PHYS; + break; + case 'a': + arg |= ARG_TEST_ANON; + break; + case 'A': + arg |= ARG_TEST_ANON_HUGE; + break; + case 's': + arg |= ARG_TEST_ANON_SHARED; + break; + case 'n': + arg |= ARG_TEST_NAMED_SHARED; + break; + case 'H': + arg |= ARG_TEST_HUGETLB_SHARED; + break; + case 't': + arg |= ARG_TEST_TMPFS; + break; + case 'S': + arg |= ARG_TEST_STACK; + break; + case 'b': + arg |= ARG_TEST_FAIL_CASES; + break; + default: + errx(EXIT_FAILURE, HELP_STR, argv[0]); + } + } + + if (arg == 0) + arg = ARG_DEFAULT; + if (!(arg & ARG_INTERFACE_MASK)) + errx(EXIT_FAILURE, "No page detective interface specified"); + + if (!(arg & ARG_TEST_MASK)) + errx(EXIT_FAILURE, "No tests specified"); + + /* Return 1 when dmesg does not have --since and --until arguments */ + old_dmesg = system("dmesg --since @0 > /dev/null 2>&1"); + + ksft_print_header(); + ksft_set_plan(count_tests(ARG_INTERFACE_VIRT, arg) + + count_tests(ARG_INTERFACE_PHYS, arg)); + + run_tests(ARG_INTERFACE_VIRT, arg); + run_tests(ARG_INTERFACE_PHYS, arg); + ksft_finished(); +} -- 2.47.0.338.g60cca15819-goog