[RFC PATCH v2 29/31] KVM: selftests: Add Hyper-V reference TSC page memory attribute test

From: David Woodhouse

Date: Fri Nov 13 2026 - 15:16:29 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, both 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. The guest
overwrites that page before the recovery, so only a fresh publish
passes.

It stays a test of its own rather than a case in memory_attributes.c:
it follows the Hyper-V MSR flow and needs Hyper-V CPUID. struct
ms_hyperv_tsc_page moves from hyperv_clock.c into include/x86/hyperv.h
for both tests to use.

Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/include/x86/hyperv.h | 8 ++
.../testing/selftests/kvm/x86/hyperv_clock.c | 7 -
.../selftests/kvm/x86/hyperv_tsc_page_attrs.c | 131 ++++++++++++++++++
4 files changed, 140 insertions(+), 7 deletions(-)
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 2b8a238c32e8..5f58fff2d543 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -95,6 +95,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/include/x86/hyperv.h b/tools/testing/selftests/kvm/include/x86/hyperv.h
index 78003f5a22f3..c1d13a35fc0e 100644
--- a/tools/testing/selftests/kvm/include/x86/hyperv.h
+++ b/tools/testing/selftests/kvm/include/x86/hyperv.h
@@ -358,4 +358,12 @@ void vcpu_set_hv_cpuid(struct kvm_vcpu *vcpu);

bool kvm_hv_cpu_has(struct kvm_x86_cpu_feature feature);

+/* HV_REFERENCE_TSC_PAGE */
+struct ms_hyperv_tsc_page {
+ volatile u32 tsc_sequence;
+ u32 reserved1;
+ volatile u64 tsc_scale;
+ volatile s64 tsc_offset;
+} __packed;
+
#endif /* !SELFTEST_KVM_HYPERV_H */
diff --git a/tools/testing/selftests/kvm/x86/hyperv_clock.c b/tools/testing/selftests/kvm/x86/hyperv_clock.c
index 7a1b3b5d1e62..0db068af0863 100644
--- a/tools/testing/selftests/kvm/x86/hyperv_clock.c
+++ b/tools/testing/selftests/kvm/x86/hyperv_clock.c
@@ -9,13 +9,6 @@
#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;
-
/* Simplified mul_u64_u64_shr() */
static inline u64 mul_u64_u64_shr64(u64 a, u64 b)
{
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..7e618ffec6a6
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/hyperv_tsc_page_attrs.c
@@ -0,0 +1,131 @@
+// 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"
+
+#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. Overwrite the
+ * page first, so that only a fresh publish can pass the checks.
+ */
+ page_a->tsc_sequence = SENTINEL_SEQ;
+ page_a->tsc_scale = SENTINEL_U64;
+ page_a->tsc_offset = SENTINEL_U64;
+ wrmsr(HV_X64_MSR_REFERENCE_TSC, page_a_gpa | 0x1);
+ GUEST_ASSERT(page_a->tsc_sequence != 0);
+ GUEST_ASSERT(page_a->tsc_sequence != SENTINEL_SEQ);
+ GUEST_ASSERT(page_a->tsc_scale != SENTINEL_U64);
+ GUEST_ASSERT(page_a->tsc_offset != SENTINEL_U64);
+
+ 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