]> git.baikalelectronics.ru Git - kernel.git/commitdiff
KVM: selftests: Add NX huge pages test
authorBen Gardon <bgardon@google.com>
Mon, 13 Jun 2022 21:25:19 +0000 (21:25 +0000)
committerPaolo Bonzini <pbonzini@redhat.com>
Fri, 24 Jun 2022 08:51:50 +0000 (04:51 -0400)
There's currently no test coverage of NX hugepages in KVM selftests, so
add a basic test to ensure that the feature works as intended.

The test creates a VM with a data slot backed with huge pages. The
memory in the data slot is filled with op-codes for the return
instruction. The guest then executes a series of accesses on the memory,
some reads, some instruction fetches. After each operation, the guest
exits and the test performs some checks on the backing page counts to
ensure that NX page splitting an reclaim work as expected.

Reviewed-by: David Matlack <dmatlack@google.com>
Signed-off-by: Ben Gardon <bgardon@google.com>
Message-Id: <20220613212523.3436117-7-bgardon@google.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
tools/testing/selftests/kvm/.gitignore
tools/testing/selftests/kvm/Makefile
tools/testing/selftests/kvm/include/kvm_util_base.h
tools/testing/selftests/kvm/lib/kvm_util.c
tools/testing/selftests/kvm/x86_64/nx_huge_pages_test.c [new file with mode: 0644]
tools/testing/selftests/kvm/x86_64/nx_huge_pages_test.sh [new file with mode: 0755]

index f3c2f074b948b0621c22e969b38f50226aea00fd..c478b4fefc572f8f3483b65b79f1594f076523d3 100644 (file)
@@ -29,6 +29,7 @@
 /x86_64/mmio_warning_test
 /x86_64/mmu_role_test
 /x86_64/monitor_mwait_test
+/x86_64/nx_huge_pages_test
 /x86_64/platform_info_test
 /x86_64/pmu_event_filter_test
 /x86_64/set_boot_cpu_id
index ad1634e5659f10c9f51b6259f5c4f04d563348dd..fa3e0687e9d51a4d4155872c565798a1f794b5a8 100644 (file)
@@ -70,6 +70,10 @@ LIBKVM_s390x += lib/s390x/ucall.c
 LIBKVM_riscv += lib/riscv/processor.c
 LIBKVM_riscv += lib/riscv/ucall.c
 
+# Non-compiled test targets
+TEST_PROGS_x86_64 += x86_64/nx_huge_pages_test.sh
+
+# Compiled test targets
 TEST_GEN_PROGS_x86_64 = x86_64/cpuid_test
 TEST_GEN_PROGS_x86_64 += x86_64/cr4_cpuid_sync_test
 TEST_GEN_PROGS_x86_64 += x86_64/get_msr_index_features
@@ -135,6 +139,9 @@ TEST_GEN_PROGS_x86_64 += steal_time
 TEST_GEN_PROGS_x86_64 += kvm_binary_stats_test
 TEST_GEN_PROGS_x86_64 += system_counter_offset_test
 
+# Compiled outputs used by test targets
+TEST_GEN_PROGS_EXTENDED_x86_64 += x86_64/nx_huge_pages_test
+
 TEST_GEN_PROGS_aarch64 += aarch64/arch_timer
 TEST_GEN_PROGS_aarch64 += aarch64/debug-exceptions
 TEST_GEN_PROGS_aarch64 += aarch64/get-reg-list
@@ -174,7 +181,9 @@ TEST_GEN_PROGS_riscv += kvm_page_table_test
 TEST_GEN_PROGS_riscv += set_memory_region_test
 TEST_GEN_PROGS_riscv += kvm_binary_stats_test
 
+TEST_PROGS += $(TEST_PROGS_$(UNAME_M))
 TEST_GEN_PROGS += $(TEST_GEN_PROGS_$(UNAME_M))
+TEST_GEN_PROGS_EXTENDED += $(TEST_GEN_PROGS_EXTENDED_$(UNAME_M))
 LIBKVM += $(LIBKVM_$(UNAME_M))
 
 INSTALL_HDR_PATH = $(top_srcdir)/usr
@@ -221,6 +230,7 @@ $(LIBKVM_S_OBJ): $(OUTPUT)/%.o: %.S
 
 x := $(shell mkdir -p $(sort $(dir $(TEST_GEN_PROGS))))
 $(TEST_GEN_PROGS): $(LIBKVM_OBJS)
+$(TEST_GEN_PROGS_EXTENDED): $(LIBKVM_OBJS)
 
 cscope: include_paths = $(LINUX_TOOL_INCLUDE) $(LINUX_HDR_PATH) include lib ..
 cscope:
index cec3af45e18de4a28f7ecc0486666fc33079a8a7..c6c1d66136d623d103b463a5f0d41511740e7018 100644 (file)
@@ -345,6 +345,17 @@ void read_stat_data(int stats_fd, struct kvm_stats_header *header,
                    struct kvm_stats_desc *desc, uint64_t *data,
                    size_t max_elements);
 
+void __vm_get_stat(struct kvm_vm *vm, const char *stat_name, uint64_t *data,
+                  size_t max_elements);
+
+static inline uint64_t vm_get_stat(struct kvm_vm *vm, const char *stat_name)
+{
+       uint64_t data;
+
+       __vm_get_stat(vm, stat_name, &data, 1);
+       return data;
+}
+
 void vm_create_irqchip(struct kvm_vm *vm);
 
 void vm_set_user_memory_region(struct kvm_vm *vm, uint32_t slot, uint32_t flags,
index e44eb510fcc1463481b904d22365cc7f75761dcd..5a0fd368503f81cb16703642e71ece9181692b50 100644 (file)
@@ -1918,3 +1918,49 @@ void read_stat_data(int stats_fd, struct kvm_stats_header *header,
                    "pread() on stat '%s' read %ld bytes, wanted %lu bytes",
                    desc->name, size, ret);
 }
+
+/*
+ * Read the data of the named stat
+ *
+ * Input Args:
+ *   vm - the VM for which the stat should be read
+ *   stat_name - the name of the stat to read
+ *   max_elements - the maximum number of 8-byte values to read into data
+ *
+ * Output Args:
+ *   data - the buffer into which stat data should be read
+ *
+ * Read the data values of a specified stat from the binary stats interface.
+ */
+void __vm_get_stat(struct kvm_vm *vm, const char *stat_name, uint64_t *data,
+                  size_t max_elements)
+{
+       struct kvm_stats_desc *stats_desc;
+       struct kvm_stats_header header;
+       struct kvm_stats_desc *desc;
+       size_t size_desc;
+       int stats_fd;
+       int i;
+
+       stats_fd = vm_get_stats_fd(vm);
+
+       read_stats_header(stats_fd, &header);
+
+       stats_desc = read_stats_descriptors(stats_fd, &header);
+
+       size_desc = get_stats_descriptor_size(&header);
+
+       for (i = 0; i < header.num_desc; ++i) {
+               desc = (void *)stats_desc + (i * size_desc);
+
+               if (strcmp(desc->name, stat_name))
+                       continue;
+
+               read_stat_data(stats_fd, &header, desc, data, max_elements);
+
+               break;
+       }
+
+       free(stats_desc);
+       close(stats_fd);
+}
diff --git a/tools/testing/selftests/kvm/x86_64/nx_huge_pages_test.c b/tools/testing/selftests/kvm/x86_64/nx_huge_pages_test.c
new file mode 100644 (file)
index 0000000..5fa61d2
--- /dev/null
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * tools/testing/selftests/kvm/nx_huge_page_test.c
+ *
+ * Usage: to be run via nx_huge_page_test.sh, which does the necessary
+ * environment setup and teardown
+ *
+ * Copyright (C) 2022, Google LLC.
+ */
+
+#define _GNU_SOURCE
+
+#include <fcntl.h>
+#include <stdint.h>
+#include <time.h>
+
+#include <test_util.h>
+#include "kvm_util.h"
+#include "processor.h"
+
+#define HPAGE_SLOT             10
+#define HPAGE_GPA              (4UL << 30) /* 4G prevents collision w/ slot 0 */
+#define HPAGE_GVA              HPAGE_GPA /* GVA is arbitrary, so use GPA. */
+#define PAGES_PER_2MB_HUGE_PAGE 512
+#define HPAGE_SLOT_NPAGES      (3 * PAGES_PER_2MB_HUGE_PAGE)
+
+/*
+ * Passed by nx_huge_pages_test.sh to provide an easy warning if this test is
+ * being run without it.
+ */
+#define MAGIC_TOKEN 887563923
+
+/*
+ * x86 opcode for the return instruction. Used to call into, and then
+ * immediately return from, memory backed with hugepages.
+ */
+#define RETURN_OPCODE 0xC3
+
+/* Call the specified memory address. */
+static void guest_do_CALL(uint64_t target)
+{
+       ((void (*)(void)) target)();
+}
+
+/*
+ * Exit the VM after each memory access so that the userspace component of the
+ * test can make assertions about the pages backing the VM.
+ *
+ * See the below for an explanation of how each access should affect the
+ * backing mappings.
+ */
+void guest_code(void)
+{
+       uint64_t hpage_1 = HPAGE_GVA;
+       uint64_t hpage_2 = hpage_1 + (PAGE_SIZE * 512);
+       uint64_t hpage_3 = hpage_2 + (PAGE_SIZE * 512);
+
+       READ_ONCE(*(uint64_t *)hpage_1);
+       GUEST_SYNC(1);
+
+       READ_ONCE(*(uint64_t *)hpage_2);
+       GUEST_SYNC(2);
+
+       guest_do_CALL(hpage_1);
+       GUEST_SYNC(3);
+
+       guest_do_CALL(hpage_3);
+       GUEST_SYNC(4);
+
+       READ_ONCE(*(uint64_t *)hpage_1);
+       GUEST_SYNC(5);
+
+       READ_ONCE(*(uint64_t *)hpage_3);
+       GUEST_SYNC(6);
+}
+
+static void check_2m_page_count(struct kvm_vm *vm, int expected_pages_2m)
+{
+       int actual_pages_2m;
+
+       actual_pages_2m = vm_get_stat(vm, "pages_2m");
+
+       TEST_ASSERT(actual_pages_2m == expected_pages_2m,
+                   "Unexpected 2m page count. Expected %d, got %d",
+                   expected_pages_2m, actual_pages_2m);
+}
+
+static void check_split_count(struct kvm_vm *vm, int expected_splits)
+{
+       int actual_splits;
+
+       actual_splits = vm_get_stat(vm, "nx_lpage_splits");
+
+       TEST_ASSERT(actual_splits == expected_splits,
+                   "Unexpected NX huge page split count. Expected %d, got %d",
+                   expected_splits, actual_splits);
+}
+
+static void wait_for_reclaim(int reclaim_period_ms)
+{
+       long reclaim_wait_ms;
+       struct timespec ts;
+
+       reclaim_wait_ms = reclaim_period_ms * 5;
+       ts.tv_sec = reclaim_wait_ms / 1000;
+       ts.tv_nsec = (reclaim_wait_ms - (ts.tv_sec * 1000)) * 1000000;
+       nanosleep(&ts, NULL);
+}
+
+static void help(char *name)
+{
+       puts("");
+       printf("usage: %s [-h] [-p period_ms] [-t token]\n", name);
+       puts("");
+       printf(" -p: The NX reclaim period in miliseconds.\n");
+       printf(" -t: The magic token to indicate environment setup is done.\n");
+       puts("");
+       exit(0);
+}
+
+int main(int argc, char **argv)
+{
+       int reclaim_period_ms = 0, token = 0, opt;
+       struct kvm_vcpu *vcpu;
+       struct kvm_vm *vm;
+       void *hva;
+
+       while ((opt = getopt(argc, argv, "hp:t:")) != -1) {
+               switch (opt) {
+               case 'p':
+                       reclaim_period_ms = atoi(optarg);
+                       break;
+               case 't':
+                       token = atoi(optarg);
+                       break;
+               case 'h':
+               default:
+                       help(argv[0]);
+                       break;
+               }
+       }
+
+       if (token != MAGIC_TOKEN) {
+               print_skip("This test must be run with the magic token %d.\n"
+                          "This is done by nx_huge_pages_test.sh, which\n"
+                          "also handles environment setup for the test.",
+                          MAGIC_TOKEN);
+               exit(KSFT_SKIP);
+       }
+
+       if (!reclaim_period_ms) {
+               print_skip("The NX reclaim period must be specified and non-zero");
+               exit(KSFT_SKIP);
+       }
+
+       vm = vm_create(1);
+       vcpu = vm_vcpu_add(vm, 0, guest_code);
+
+       vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS_HUGETLB,
+                                   HPAGE_GPA, HPAGE_SLOT,
+                                   HPAGE_SLOT_NPAGES, 0);
+
+       virt_map(vm, HPAGE_GVA, HPAGE_GPA, HPAGE_SLOT_NPAGES);
+
+       hva = addr_gpa2hva(vm, HPAGE_GPA);
+       memset(hva, RETURN_OPCODE, HPAGE_SLOT_NPAGES * PAGE_SIZE);
+
+       check_2m_page_count(vm, 0);
+       check_split_count(vm, 0);
+
+       /*
+        * The guest code will first read from the first hugepage, resulting
+        * in a huge page mapping being created.
+        */
+       vcpu_run(vcpu);
+       check_2m_page_count(vm, 1);
+       check_split_count(vm, 0);
+
+       /*
+        * Then the guest code will read from the second hugepage, resulting
+        * in another huge page mapping being created.
+        */
+       vcpu_run(vcpu);
+       check_2m_page_count(vm, 2);
+       check_split_count(vm, 0);
+
+       /*
+        * Next, the guest will execute from the first huge page, causing it
+        * to be remapped at 4k.
+        */
+       vcpu_run(vcpu);
+       check_2m_page_count(vm, 1);
+       check_split_count(vm, 1);
+
+       /*
+        * Executing from the third huge page (previously unaccessed) will
+        * cause part to be mapped at 4k.
+        */
+       vcpu_run(vcpu);
+       check_2m_page_count(vm, 1);
+       check_split_count(vm, 2);
+
+       /* Reading from the first huge page again should have no effect. */
+       vcpu_run(vcpu);
+       check_2m_page_count(vm, 1);
+       check_split_count(vm, 2);
+
+       /* Give recovery thread time to run. */
+       wait_for_reclaim(reclaim_period_ms);
+
+       /*
+        * Now that the reclaimer has run, all the split pages should be gone.
+        */
+       check_2m_page_count(vm, 1);
+       check_split_count(vm, 0);
+
+       /*
+        * The 4k mapping on hpage 3 should have been removed, so check that
+        * reading from it causes a huge page mapping to be installed.
+        */
+       vcpu_run(vcpu);
+       check_2m_page_count(vm, 2);
+       check_split_count(vm, 0);
+
+       kvm_vm_free(vm);
+
+       return 0;
+}
+
diff --git a/tools/testing/selftests/kvm/x86_64/nx_huge_pages_test.sh b/tools/testing/selftests/kvm/x86_64/nx_huge_pages_test.sh
new file mode 100755 (executable)
index 0000000..4e090a8
--- /dev/null
@@ -0,0 +1,40 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0-only */
+#
+# Wrapper script which performs setup and cleanup for nx_huge_pages_test.
+# Makes use of root privileges to set up huge pages and KVM module parameters.
+#
+# tools/testing/selftests/kvm/nx_huge_page_test.sh
+# Copyright (C) 2022, Google LLC.
+
+set -e
+
+NX_HUGE_PAGES=$(cat /sys/module/kvm/parameters/nx_huge_pages)
+NX_HUGE_PAGES_RECOVERY_RATIO=$(cat /sys/module/kvm/parameters/nx_huge_pages_recovery_ratio)
+NX_HUGE_PAGES_RECOVERY_PERIOD=$(cat /sys/module/kvm/parameters/nx_huge_pages_recovery_period_ms)
+HUGE_PAGES=$(cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages)
+
+set +e
+
+function sudo_echo () {
+       echo "$1" | sudo tee -a "$2" > /dev/null
+}
+
+(
+       set -e
+
+       sudo_echo 1 /sys/module/kvm/parameters/nx_huge_pages
+       sudo_echo 1 /sys/module/kvm/parameters/nx_huge_pages_recovery_ratio
+       sudo_echo 100 /sys/module/kvm/parameters/nx_huge_pages_recovery_period_ms
+       sudo_echo "$(( $HUGE_PAGES + 3 ))" /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
+
+       "$(dirname $0)"/nx_huge_pages_test -t 887563923 -p 100
+)
+RET=$?
+
+sudo_echo "$NX_HUGE_PAGES" /sys/module/kvm/parameters/nx_huge_pages
+sudo_echo "$NX_HUGE_PAGES_RECOVERY_RATIO" /sys/module/kvm/parameters/nx_huge_pages_recovery_ratio
+sudo_echo "$NX_HUGE_PAGES_RECOVERY_PERIOD" /sys/module/kvm/parameters/nx_huge_pages_recovery_period_ms
+sudo_echo "$HUGE_PAGES" /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
+
+exit $RET