From: Mushahid Hussain <[email protected]> Back a 2 MiB-aligned region with a transparent hugepage and give it alternating NX and NW protection attributes on its 4 KiB pages. The guest reads an NX page, which maps the region, then writes the NW page next to it: the write must exit with KVM_EXIT_MEMORY_FAULT and leave the page unchanged, instead of going through a 2 MiB leaf that inherited the NX page's permissions. Clearing the attribute lets the write through.
This pins the hugepage side of the VSM memory protection boundary: a single 4 KiB protection set by a higher VTL must not be widened away by a neighbouring gfn's fault installing a hugepage whose access was computed from the neighbour alone. The host refuses that hugepage via the KVM_LPAGE_MIXED_FLAG tracking which kvm_arch_post_set_memory_attributes() maintains over the whole attribute word, NR/NW/NX included. [dwmw2: re-done by hand from Mushahid's original, adapted from the KVM_SET_PLANE_MEMORY_ATTRIBUTES uAPI to the per-plane memory attributes design: plane 0 attributes are set through KVM_SET_MEMORY_ATTRIBUTES with the NR/NW/NX protection bits, no plane runtime is needed, and the fault exit carries no plane field] Assisted-by: LLM Signed-off-by: Mushahid Hussain <[email protected]> Signed-off-by: David Woodhouse <[email protected]> --- tools/testing/selftests/kvm/Makefile.kvm | 1 + .../selftests/kvm/x86/memattr_hugepage_test.c | 132 ++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 tools/testing/selftests/kvm/x86/memattr_hugepage_test.c diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm index 14e385750ed1..66b16987d6e2 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -92,6 +92,7 @@ TEST_GEN_PROGS_x86 += x86/hyperv_tlb_flush TEST_GEN_PROGS_x86 += x86/kvm_clock_test TEST_GEN_PROGS_x86 += x86/kvm_pv_test TEST_GEN_PROGS_x86 += x86/kvm_buslock_test +TEST_GEN_PROGS_x86 += x86/memattr_hugepage_test TEST_GEN_PROGS_x86 += x86/monitor_mwait_test TEST_GEN_PROGS_x86 += x86/msrs_test TEST_GEN_PROGS_x86 += x86/nested_close_kvm_test diff --git a/tools/testing/selftests/kvm/x86/memattr_hugepage_test.c b/tools/testing/selftests/kvm/x86/memattr_hugepage_test.c new file mode 100644 index 000000000000..156036eda5db --- /dev/null +++ b/tools/testing/selftests/kvm/x86/memattr_hugepage_test.c @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test hugepage sizing over mixed memory protection attributes on x86. + * + * Back a 2 MiB-aligned region with a transparent hugepage and give it + * alternating KVM_MEMORY_ATTRIBUTE_NX and _NW on its 4 KiB pages. The + * guest reads an NX page, which maps the region, then writes the NW page + * next to it: the write must exit with KVM_EXIT_MEMORY_FAULT and leave + * the page unchanged, instead of going through a 2 MiB leaf that + * inherited the NX page's permissions. Clearing the attribute lets the + * write through. + * + * This pins the VSM memory protection boundary: a VTL1 guest protecting + * one 4 KiB page of VTL0 memory must not have the protection widened + * away by a neighbouring fault's hugepage. + */ +#include "kvm_util.h" +#include "processor.h" +#include "test_util.h" +#include "ucall_common.h" + +#define MIXED_GPA 0x40000000ul +#define MIXED_SLOT 10 +#define MIXED_PAGES 1024 /* 4 MiB: a whole 2 MiB hugepage inside */ + +/* + * MADV_HUGEPAGE is advisory and thp_configured() only checks that THP + * exists in the host kernel: if no 2 MiB THP actually backs the region + * (runtime policy "never", fragmentation), KVM maps 4 KiB leaves and + * the test "passes" without covering the hugepage path it exists for. + * Read the smaps entry covering @hva and report whether at least one + * anonymous hugepage backs it. + */ +static bool anon_hugepage_backed(void *hva) +{ + unsigned long start, end, kb, addr = (unsigned long)hva; + bool in_vma = false; + char line[256]; + FILE *f; + + f = fopen("/proc/self/smaps", "r"); + TEST_ASSERT(f, "Failed to open /proc/self/smaps"); + + while (fgets(line, sizeof(line), f)) { + if (sscanf(line, "%lx-%lx ", &start, &end) == 2) { + in_vma = addr >= start && addr < end; + continue; + } + if (in_vma && sscanf(line, "AnonHugePages: %lu kB", &kb) == 1) { + fclose(f); + return kb >= 2048; + } + } + + fclose(f); + TEST_FAIL("No smaps entry covers %p", hva); + return false; +} + +static void guest_mixed_attrs(void) +{ + u64 *nx_page = (u64 *)MIXED_GPA; + u64 *nw_page = (u64 *)(MIXED_GPA + PAGE_SIZE); + + /* The first touch maps the region: a read of a no-exec page. */ + GUEST_SYNC(READ_ONCE(*nx_page)); + /* A write to the no-write page next to it must be denied. */ + WRITE_ONCE(*nw_page, 0xbad); + GUEST_SYNC(READ_ONCE(*nw_page)); + GUEST_DONE(); +} + +int main(int argc, char *argv[]) +{ + struct kvm_vcpu *vcpu; + struct kvm_run *run; + struct kvm_vm *vm; + struct ucall uc; + u64 *host; + int i, r; + + TEST_REQUIRE(kvm_check_cap(KVM_CAP_MEMORY_ATTRIBUTES) & + KVM_MEMORY_ATTRIBUTE_NW); + TEST_REQUIRE(thp_configured()); + + vm = vm_create_with_one_vcpu(&vcpu, guest_mixed_attrs); + run = vcpu->run; + + vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS_THP, MIXED_GPA, + MIXED_SLOT, MIXED_PAGES, 0); + virt_map(vm, MIXED_GPA, MIXED_GPA, MIXED_PAGES); + host = addr_gpa2hva(vm, MIXED_GPA); + memset(host, 0, MIXED_PAGES * getpagesize()); + host[0] = 0x11; + host[PAGE_SIZE / sizeof(u64)] = 0x22; + + /* The memset faulted the region in; a THP must have materialised. */ + TEST_REQUIRE(anon_hugepage_backed(host)); + + /* As VTL1 would: alternate NX and NW over the hugepage's 4 KiB pages. */ + for (i = 0; i < 512; i++) + vm_set_memory_attributes(vm, MIXED_GPA + i * PAGE_SIZE, + PAGE_SIZE, + (i & 1) ? KVM_MEMORY_ATTRIBUTE_NW : + KVM_MEMORY_ATTRIBUTE_NX); + + vcpu_run(vcpu); + TEST_ASSERT_EQ(get_ucall(vcpu, &uc), UCALL_SYNC); + TEST_ASSERT_EQ(uc.args[1], 0x11); + + r = _vcpu_run(vcpu); + TEST_ASSERT(r == -1 && errno == EFAULT, + "write to the NW page: ret %d errno %d, exit %u", + r, errno, run->exit_reason); + TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_MEMORY_FAULT); + TEST_ASSERT_EQ(run->memory_fault.gpa, MIXED_GPA + PAGE_SIZE); + TEST_ASSERT(run->memory_fault.flags & KVM_MEMORY_EXIT_FLAG_WRITE, + "expected a write fault, flags 0x%llx", + run->memory_fault.flags); + TEST_ASSERT_EQ(host[PAGE_SIZE / sizeof(u64)], 0x22); + + /* Lift the protection: the write goes through at the right level. */ + vm_set_memory_attributes(vm, MIXED_GPA + PAGE_SIZE, PAGE_SIZE, 0); + vcpu_run(vcpu); + TEST_ASSERT_EQ(get_ucall(vcpu, &uc), UCALL_SYNC); + TEST_ASSERT_EQ(uc.args[1], 0xbad); + vcpu_run(vcpu); + TEST_ASSERT_EQ(get_ucall(vcpu, &uc), UCALL_DONE); + + kvm_vm_free(vm); + return 0; +} -- 2.50.1

