]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
KVM: arm64: Selftest for pKVM transitions
authorQuentin Perret <qperret@google.com>
Wed, 16 Apr 2025 16:08:59 +0000 (16:08 +0000)
committerMarc Zyngier <maz@kernel.org>
Tue, 6 May 2025 08:56:18 +0000 (09:56 +0100)
We have recently found a bug [1] in the pKVM memory ownership
transitions by code inspection, but it could have been caught with a
test.

Introduce a boot-time selftest exercising all the known pKVM memory
transitions and importantly checks the rejection of illegal transitions.

The new test is hidden behind a new Kconfig option separate from
CONFIG_EL2_NVHE_DEBUG on purpose as that has side effects on the
transition checks ([1] doesn't reproduce with EL2 debug enabled).

[1] https://lore.kernel.org/kvmarm/20241128154406.602875-1-qperret@google.com/

Suggested-by: Will Deacon <will@kernel.org>
Signed-off-by: Quentin Perret <qperret@google.com>
Link: https://lore.kernel.org/r/20250416160900.3078417-4-qperret@google.com
Signed-off-by: Marc Zyngier <maz@kernel.org>
arch/arm64/kvm/hyp/include/nvhe/mem_protect.h
arch/arm64/kvm/hyp/nvhe/mem_protect.c
arch/arm64/kvm/hyp/nvhe/setup.c

index ea0a704da9b8962cda1c143084161ec44fe968c5..535722cd8417e2c4141548166200f28844dd8efc 100644 (file)
@@ -67,4 +67,10 @@ static __always_inline void __load_host_stage2(void)
        else
                write_sysreg(0, vttbr_el2);
 }
+
+#ifdef CONFIG_NVHE_EL2_DEBUG
+void pkvm_ownership_selftest(void);
+#else
+static inline void pkvm_ownership_selftest(void) { }
+#endif
 #endif /* __KVM_NVHE_MEM_PROTECT__ */
index cf6207aa1434ffb07de38d32c190c38283b26637..aee9359f5b9f7413cd642aa764ef82869e2cb9fe 100644 (file)
@@ -1090,3 +1090,114 @@ int __pkvm_host_mkyoung_guest(u64 gfn, struct pkvm_hyp_vcpu *vcpu)
 
        return 0;
 }
+
+#ifdef CONFIG_NVHE_EL2_DEBUG
+struct pkvm_expected_state {
+       enum pkvm_page_state host;
+       enum pkvm_page_state hyp;
+};
+
+static struct pkvm_expected_state selftest_state;
+static struct hyp_page *selftest_page;
+
+static void assert_page_state(void)
+{
+       void *virt = hyp_page_to_virt(selftest_page);
+       u64 size = PAGE_SIZE << selftest_page->order;
+       u64 phys = hyp_virt_to_phys(virt);
+
+       host_lock_component();
+       WARN_ON(__host_check_page_state_range(phys, size, selftest_state.host));
+       host_unlock_component();
+
+       hyp_lock_component();
+       WARN_ON(__hyp_check_page_state_range(phys, size, selftest_state.hyp));
+       hyp_unlock_component();
+}
+
+#define assert_transition_res(res, fn, ...)            \
+       do {                                            \
+               WARN_ON(fn(__VA_ARGS__) != res);        \
+               assert_page_state();                    \
+       } while (0)
+
+void pkvm_ownership_selftest(void)
+{
+       void *virt = hyp_alloc_pages(&host_s2_pool, 0);
+       u64 phys, size, pfn;
+
+       WARN_ON(!virt);
+       selftest_page = hyp_virt_to_page(virt);
+       selftest_page->refcount = 0;
+
+       size = PAGE_SIZE << selftest_page->order;
+       phys = hyp_virt_to_phys(virt);
+       pfn = hyp_phys_to_pfn(phys);
+
+       selftest_state.host = PKVM_NOPAGE;
+       selftest_state.hyp = PKVM_PAGE_OWNED;
+       assert_page_state();
+       assert_transition_res(-EPERM,   __pkvm_host_donate_hyp, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_host_share_hyp, pfn);
+       assert_transition_res(-EPERM,   __pkvm_host_unshare_hyp, pfn);
+       assert_transition_res(-EPERM,   __pkvm_host_share_ffa, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_host_unshare_ffa, pfn, 1);
+       assert_transition_res(-EPERM,   hyp_pin_shared_mem, virt, virt + size);
+
+       selftest_state.host = PKVM_PAGE_OWNED;
+       selftest_state.hyp = PKVM_NOPAGE;
+       assert_transition_res(0,        __pkvm_hyp_donate_host, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_hyp_donate_host, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_host_unshare_hyp, pfn);
+       assert_transition_res(-EPERM,   __pkvm_host_unshare_ffa, pfn, 1);
+       assert_transition_res(-EPERM,   hyp_pin_shared_mem, virt, virt + size);
+
+       selftest_state.host = PKVM_PAGE_SHARED_OWNED;
+       selftest_state.hyp = PKVM_PAGE_SHARED_BORROWED;
+       assert_transition_res(0,        __pkvm_host_share_hyp, pfn);
+       assert_transition_res(-EPERM,   __pkvm_host_share_hyp, pfn);
+       assert_transition_res(-EPERM,   __pkvm_host_donate_hyp, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_host_share_ffa, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_hyp_donate_host, pfn, 1);
+
+       assert_transition_res(0,        hyp_pin_shared_mem, virt, virt + size);
+       assert_transition_res(0,        hyp_pin_shared_mem, virt, virt + size);
+       hyp_unpin_shared_mem(virt, virt + size);
+       WARN_ON(hyp_page_count(virt) != 1);
+       assert_transition_res(-EBUSY,   __pkvm_host_unshare_hyp, pfn);
+       assert_transition_res(-EPERM,   __pkvm_host_share_hyp, pfn);
+       assert_transition_res(-EPERM,   __pkvm_host_donate_hyp, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_host_share_ffa, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_hyp_donate_host, pfn, 1);
+
+       hyp_unpin_shared_mem(virt, virt + size);
+       assert_page_state();
+       WARN_ON(hyp_page_count(virt));
+
+       selftest_state.host = PKVM_PAGE_OWNED;
+       selftest_state.hyp = PKVM_NOPAGE;
+       assert_transition_res(0,        __pkvm_host_unshare_hyp, pfn);
+
+       selftest_state.host = PKVM_PAGE_SHARED_OWNED;
+       selftest_state.hyp = PKVM_NOPAGE;
+       assert_transition_res(0,        __pkvm_host_share_ffa, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_host_share_ffa, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_host_donate_hyp, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_host_share_hyp, pfn);
+       assert_transition_res(-EPERM,   __pkvm_host_unshare_hyp, pfn);
+       assert_transition_res(-EPERM,   __pkvm_hyp_donate_host, pfn, 1);
+       assert_transition_res(-EPERM,   hyp_pin_shared_mem, virt, virt + size);
+
+       selftest_state.host = PKVM_PAGE_OWNED;
+       selftest_state.hyp = PKVM_NOPAGE;
+       assert_transition_res(0,        __pkvm_host_unshare_ffa, pfn, 1);
+       assert_transition_res(-EPERM,   __pkvm_host_unshare_ffa, pfn, 1);
+
+       selftest_state.host = PKVM_NOPAGE;
+       selftest_state.hyp = PKVM_PAGE_OWNED;
+       assert_transition_res(0,        __pkvm_host_donate_hyp, pfn, 1);
+
+       selftest_page->refcount = 1;
+       hyp_put_page(&host_s2_pool, virt);
+}
+#endif
index 46d9bd04348f48e1a3883b6108635046f4336889..54006f959e1b1e94daf14e7d4baf973cf1c08bab 100644 (file)
@@ -308,6 +308,8 @@ void __noreturn __pkvm_init_finalise(void)
                goto out;
 
        pkvm_hyp_vm_table_init(vm_table_base);
+
+       pkvm_ownership_selftest();
 out:
        /*
         * We tail-called to here from handle___pkvm_init() and will not return,