Commit 8448ec59 authored by Ben Gardon's avatar Ben Gardon Committed by Paolo Bonzini

KVM: selftests: Add NX huge pages test

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: default avatarDavid Matlack <dmatlack@google.com>
Signed-off-by: default avatarBen Gardon <bgardon@google.com>
Message-Id: <20220613212523.3436117-7-bgardon@google.com>
Signed-off-by: default avatarPaolo Bonzini <pbonzini@redhat.com>
parent 084cc29f
......@@ -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
......
......@@ -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:
......
......@@ -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,
......
......@@ -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);
}
// 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;
}
#!/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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment