[RFC PATCH 52/55] KVM: selftests: Add Hyper-V reference TSC page memory attribute test

From: David Woodhouse

Date: Sat Oct 17 2026 - 08:50:52 EST


From: David Woodhouse <dwmw@xxxxxxxxxxxx>

The reference TSC page is written by the host, not by the guest, so
the guest-side memory attribute tests never cover it. Add a test
that a page at an unprotected gfn is maintained as usual, that
pointing HV_X64_MSR_REFERENCE_TSC at a gfn carrying
KVM_MEMORY_ATTRIBUTE_NW leaves the page contents untouched — on the
MSR write itself and on a subsequent KVM_SET_CLOCK-forced update —
and that re-pointing the MSR at a writable page recovers.

Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
Assisted-by: LLM
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/x86/hyperv_tsc_page_attrs.c | 129 ++++++++++++++++++
2 files changed, 130 insertions(+)
create mode 100644 tools/testing/selftests/kvm/x86/hyperv_tsc_page_attrs.c

diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 47c7a4c732eb..6edeb6693375 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -89,6 +89,7 @@ TEST_GEN_PROGS_x86 += x86/hyperv_forward_hcall
TEST_GEN_PROGS_x86 += x86/hyperv_ipi
TEST_GEN_PROGS_x86 += x86/hyperv_svm_test
TEST_GEN_PROGS_x86 += x86/hyperv_tlb_flush
+TEST_GEN_PROGS_x86 += x86/hyperv_tsc_page_attrs
TEST_GEN_PROGS_x86 += x86/kvm_clock_test
TEST_GEN_PROGS_x86 += x86/kvm_pv_test
TEST_GEN_PROGS_x86 += x86/kvm_buslock_test
diff --git a/tools/testing/selftests/kvm/x86/hyperv_tsc_page_attrs.c b/tools/testing/selftests/kvm/x86/hyperv_tsc_page_attrs.c
new file mode 100644
index 000000000000..efa47ae69780
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/hyperv_tsc_page_attrs.c
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Test that KVM honours plane memory attributes when it maintains the
+ * Hyper-V reference TSC page. A page whose gfn carries
+ * KVM_MEMORY_ATTRIBUTE_NW in the owning plane's attributes must never
+ * be written by the host, on the HV_X64_MSR_REFERENCE_TSC write or on
+ * any later masterclock update; an unprotected page keeps working, and
+ * re-pointing the MSR at a writable page recovers.
+ */
+#include "test_util.h"
+#include "kvm_util.h"
+#include "processor.h"
+#include "hyperv.h"
+
+struct ms_hyperv_tsc_page {
+ volatile u32 tsc_sequence;
+ u32 reserved1;
+ volatile u64 tsc_scale;
+ volatile s64 tsc_offset;
+} __packed;
+
+#define SENTINEL_SEQ 0xdeadbeef
+#define SENTINEL_U64 0xcafef00dcafef00dULL
+
+static void guest_main(struct ms_hyperv_tsc_page *page_a, gpa_t page_a_gpa,
+ struct ms_hyperv_tsc_page *page_b, gpa_t page_b_gpa)
+{
+ /* Set Guest OS id to enable Hyper-V emulation */
+ wrmsr(HV_X64_MSR_GUEST_OS_ID, HYPERV_LINUX_OS_ID);
+
+ /* An unprotected page is maintained by KVM as usual. */
+ wrmsr(HV_X64_MSR_REFERENCE_TSC, page_a_gpa | 0x1);
+ GUEST_ASSERT(page_a->tsc_sequence != 0);
+
+ /* Fill the page about to be protected, while it is still writable. */
+ page_b->tsc_sequence = SENTINEL_SEQ;
+ page_b->tsc_scale = SENTINEL_U64;
+ page_b->tsc_offset = SENTINEL_U64;
+
+ GUEST_SYNC(1); /* host sets KVM_MEMORY_ATTRIBUTE_NW on page_b */
+
+ /*
+ * Pointing the reference TSC MSR at the protected page must not
+ * have KVM write to it: not on the MSR write itself, and not on
+ * the masterclock update the write requests.
+ */
+ wrmsr(HV_X64_MSR_REFERENCE_TSC, page_b_gpa | 0x1);
+ GUEST_ASSERT(page_b->tsc_sequence == SENTINEL_SEQ);
+ GUEST_ASSERT(page_b->tsc_scale == SENTINEL_U64);
+ GUEST_ASSERT(page_b->tsc_offset == SENTINEL_U64);
+
+ GUEST_SYNC(2); /* host resets kvmclock, forcing a TSC page update */
+
+ GUEST_ASSERT(page_b->tsc_sequence == SENTINEL_SEQ);
+ GUEST_ASSERT(page_b->tsc_scale == SENTINEL_U64);
+ GUEST_ASSERT(page_b->tsc_offset == SENTINEL_U64);
+
+ /* Re-pointing the MSR at a writable page recovers. */
+ wrmsr(HV_X64_MSR_REFERENCE_TSC, page_a_gpa | 0x1);
+ GUEST_ASSERT(page_a->tsc_sequence != 0);
+
+ GUEST_DONE();
+}
+
+int main(void)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ struct ucall uc;
+ gva_t page_a_gva, page_b_gva;
+ gpa_t page_b_gpa;
+ int stage;
+
+ TEST_REQUIRE(kvm_has_cap(KVM_CAP_HYPERV_TIME));
+ TEST_REQUIRE(kvm_check_cap(KVM_CAP_MEMORY_ATTRIBUTES) &
+ KVM_MEMORY_ATTRIBUTE_NW);
+ TEST_REQUIRE(sys_clocksource_is_based_on_tsc());
+
+ vm = vm_create_with_one_vcpu(&vcpu, guest_main);
+
+ vcpu_set_hv_cpuid(vcpu);
+
+ page_a_gva = vm_alloc_page(vm);
+ page_b_gva = vm_alloc_page(vm);
+ memset(addr_gva2hva(vm, page_a_gva), 0x0, getpagesize());
+ memset(addr_gva2hva(vm, page_b_gva), 0x0, getpagesize());
+ page_b_gpa = addr_gva2gpa(vm, page_b_gva);
+
+ vcpu_args_set(vcpu, 4, page_a_gva, addr_gva2gpa(vm, page_a_gva),
+ page_b_gva, page_b_gpa);
+
+ for (stage = 1;; stage++) {
+ vcpu_run(vcpu);
+ TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO);
+
+ switch (get_ucall(vcpu, &uc)) {
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ /* NOT REACHED */
+ case UCALL_SYNC:
+ break;
+ case UCALL_DONE:
+ /* Keep in sync with guest_main() */
+ TEST_ASSERT(stage == 3, "Testing ended prematurely, stage %d",
+ stage);
+ goto out;
+ default:
+ TEST_FAIL("Unknown ucall %lu", uc.cmd);
+ }
+
+ TEST_ASSERT(uc.args[1] == stage,
+ "Stage %d: Unexpected sync, got %lx",
+ stage, (ulong)uc.args[1]);
+
+ if (stage == 1)
+ vm_set_memory_attributes(vm, page_b_gpa, getpagesize(),
+ KVM_MEMORY_ATTRIBUTE_NW);
+
+ /* Reset kvmclock triggering TSC page update */
+ if (stage == 2) {
+ struct kvm_clock_data clock = {0};
+
+ vm_ioctl(vm, KVM_SET_CLOCK, &clock);
+ }
+ }
+
+out:
+ kvm_vm_free(vm);
+}
--
2.50.1