KVM: arm64: Add selftests for the pKVM heap allocator

Introduce a comprehensive runtime selftest for the pKVM hypervisor heap
allocator, executed during init when CONFIG_NVHE_EL2_DEBUG is enabled.

The selftest runs entirely at EL2 and exercises allocator's core
mechanisms:

  * over-sized allocations
  * basic allocation and alignment
  * chunk recycling, splitting, merging
  * memory reclaiming
  * memory topup

Signed-off-by: Vincent Donnefort <vdonnefort@google.com>
Reviewed-by: Fuad Tabba <fuad.tabba@linux.dev>
Tested-by: Fuad Tabba <fuad.tabba@linux.dev>
Link: https://patch.msgid.link/20261001142849.3367614-10-vdonnefort@google.com
Signed-off-by: Marc Zyngier <maz@kernel.org>
This commit is contained in:
Vincent Donnefort 2026-10-01 15:28:40 +01:00 committed by Marc Zyngier
parent 43759e3c96
commit bd93068340
8 changed files with 247 additions and 1 deletions

View File

@ -62,6 +62,7 @@ enum __kvm_host_smccc_func {
__KVM_HOST_SMCCC_FUNC___kvm_enable_ssbs, __KVM_HOST_SMCCC_FUNC___kvm_enable_ssbs,
__KVM_HOST_SMCCC_FUNC___vgic_v3_init_lrs, __KVM_HOST_SMCCC_FUNC___vgic_v3_init_lrs,
__KVM_HOST_SMCCC_FUNC___vgic_v3_get_gic_config, __KVM_HOST_SMCCC_FUNC___vgic_v3_get_gic_config,
__KVM_HOST_SMCCC_FUNC___pkvm_hyp_alloc_selftest,
MARKER(__KVM_HOST_SMCCC_FUNC_MIN_PKVM), MARKER(__KVM_HOST_SMCCC_FUNC_MIN_PKVM),

View File

@ -24,6 +24,7 @@ typedef u16 pkvm_handle_t;
enum pkvm_topup_id { enum pkvm_topup_id {
PKVM_TOPUP_HYP_ALLOC, PKVM_TOPUP_HYP_ALLOC,
PKVM_TOPUP_HYP_ALLOC_SELFTEST,
}; };
struct kvm; struct kvm;
@ -153,6 +154,7 @@ DECLARE_KVM_HOST_HCALL(int, __pkvm_cpu_set_vector,
DECLARE_KVM_HOST_HCALL0(void, __kvm_enable_ssbs) DECLARE_KVM_HOST_HCALL0(void, __kvm_enable_ssbs)
DECLARE_KVM_HOST_HCALL0(void, __vgic_v3_init_lrs) DECLARE_KVM_HOST_HCALL0(void, __vgic_v3_init_lrs)
DECLARE_KVM_HOST_HCALL0(u64, __vgic_v3_get_gic_config) DECLARE_KVM_HOST_HCALL0(u64, __vgic_v3_get_gic_config)
DECLARE_KVM_HOST_HCALL0(int, __pkvm_hyp_alloc_selftest)
DECLARE_KVM_HOST_HCALL0(int, __pkvm_prot_finalize) DECLARE_KVM_HOST_HCALL0(int, __pkvm_prot_finalize)

View File

@ -17,6 +17,7 @@
#define HYP_MEMBLOCK_REGIONS 128 #define HYP_MEMBLOCK_REGIONS 128
void pkvm_selftests(void);
int pkvm_init_host_vm(struct kvm *kvm, unsigned long type); int pkvm_init_host_vm(struct kvm *kvm, unsigned long type);
int pkvm_create_hyp_vm(struct kvm *kvm); int pkvm_create_hyp_vm(struct kvm *kvm);
bool pkvm_hyp_vm_is_created(struct kvm *kvm); bool pkvm_hyp_vm_is_created(struct kvm *kvm);
@ -208,6 +209,7 @@ struct pkvm_mapping {
enum pkvm_hyp_req_type { enum pkvm_hyp_req_type {
PKVM_HYP_NO_REQ = 0, PKVM_HYP_NO_REQ = 0,
PKVM_HYP_REQ_HYP_ALLOC, PKVM_HYP_REQ_HYP_ALLOC,
PKVM_HYP_REQ_HYP_ALLOC_SELFTEST,
__PKVM_HYP_REQ_TYPE_MAX, __PKVM_HYP_REQ_TYPE_MAX,
}; };
@ -235,6 +237,7 @@ static inline size_t pkvm_hyp_req_arg_size(u8 type)
case PKVM_HYP_NO_REQ: case PKVM_HYP_NO_REQ:
return 0; return 0;
case PKVM_HYP_REQ_HYP_ALLOC: case PKVM_HYP_REQ_HYP_ALLOC:
case PKVM_HYP_REQ_HYP_ALLOC_SELFTEST:
return sizeof(req->mem); return sizeof(req->mem);
default: default:
WARN_ON(1); WARN_ON(1);

View File

@ -2914,6 +2914,8 @@ static int __init init_hyp_mode(void)
kvm_err("Failed to init hyp memory protection\n"); kvm_err("Failed to init hyp memory protection\n");
goto out_err; goto out_err;
} }
pkvm_selftests();
} }
return 0; return 0;

View File

@ -14,4 +14,15 @@ int hyp_alloc_init(size_t size);
int hyp_alloc_topup(struct kvm_hyp_memcache *host_mc); int hyp_alloc_topup(struct kvm_hyp_memcache *host_mc);
unsigned long hyp_alloc_reclaimable(void); unsigned long hyp_alloc_reclaimable(void);
void hyp_alloc_reclaim(struct kvm_hyp_memcache *host_mc, unsigned long target); void hyp_alloc_reclaim(struct kvm_hyp_memcache *host_mc, unsigned long target);
#ifdef CONFIG_NVHE_EL2_DEBUG
int hyp_allocator_selftest(void);
u32 hyp_alloc_selftest_topup_needed(void);
int hyp_alloc_selftest_topup(struct kvm_hyp_memcache *host_mc);
void hyp_alloc_selftest_reclaim(struct kvm_hyp_memcache *host_mc, unsigned long target);
#else
static inline int hyp_alloc_selftest_topup(struct kvm_hyp_memcache *host_mc) { return 0; };
static inline void
hyp_alloc_selftest_reclaim(struct kvm_hyp_memcache *host_mc, unsigned long target) { };
#endif
#endif #endif

View File

@ -1001,9 +1001,17 @@ int hyp_alloc_errno(void)
return hyp_allocator_errno(&hyp_allocator); return hyp_allocator_errno(&hyp_allocator);
} }
static int selftest_init(void);
int hyp_alloc_init(size_t size) int hyp_alloc_init(size_t size)
{ {
return hyp_allocator_init(&hyp_allocator, size); int ret;
ret = hyp_allocator_init(&hyp_allocator, size);
if (ret)
return ret;
return selftest_init();
} }
void hyp_alloc_reclaim(struct kvm_hyp_memcache *mc, unsigned long target) void hyp_alloc_reclaim(struct kvm_hyp_memcache *mc, unsigned long target)
@ -1025,3 +1033,179 @@ u32 hyp_alloc_topup_needed(void)
{ {
return hyp_allocator_topup_needed(&hyp_allocator); return hyp_allocator_topup_needed(&hyp_allocator);
} }
#ifdef CONFIG_NVHE_EL2_DEBUG
#define SELFTEST_MAX_PAGES 6
#define SELFTEST_MAX_SIZE (PAGE_SIZE * SELFTEST_MAX_PAGES)
static DEFINE_PER_CPU(int, __selftest_errno);
static DEFINE_PER_CPU(u32, __selftest_topup_needed);
static struct hyp_allocator selftest_allocator = {
.errno = &__selftest_errno,
.topup_needed = &__selftest_topup_needed,
.lock = __HYP_SPIN_LOCK_UNLOCKED,
};
int hyp_alloc_selftest_topup(struct kvm_hyp_memcache *host_mc)
{
return hyp_allocator_topup(&selftest_allocator, host_mc);
}
void hyp_alloc_selftest_reclaim(struct kvm_hyp_memcache *host_mc, unsigned long target)
{
hyp_allocator_reclaim(&selftest_allocator, host_mc, target);
}
u32 hyp_alloc_selftest_topup_needed(void)
{
return hyp_allocator_topup_needed(&selftest_allocator);
}
static int selftest_init(void)
{
return hyp_allocator_init(&selftest_allocator, SELFTEST_MAX_SIZE);
}
static void *selftest_alloc(size_t size)
{
return hyp_allocator_alloc(&selftest_allocator, size);
}
static void selftest_free(void *addr)
{
hyp_allocator_free(&selftest_allocator, addr);
}
static int selftest_errno(void)
{
return hyp_allocator_errno(&selftest_allocator);
}
int hyp_allocator_selftest(void)
{
struct hyp_allocator *allocator = &selftest_allocator;
static DEFINE_HYP_SPINLOCK(selftest_lock);
struct kvm_hyp_memcache host_mc = { };
void *addr1, *addr2, *addr3, *addr4;
int ret;
guard(hyp_spinlock)(&selftest_lock);
if (allocator->mc.nr_pages < SELFTEST_MAX_PAGES) {
*this_cpu_ptr(allocator->topup_needed) = SELFTEST_MAX_PAGES -
allocator->mc.nr_pages;
return -ENOMEM;
}
selftest_alloc(SELFTEST_MAX_SIZE);
if (selftest_errno() != -E2BIG)
return -EINVAL;
selftest_alloc(SIZE_MAX);
if (selftest_errno() != -E2BIG)
return -EINVAL;
/* Test first chunk */
addr1 = selftest_alloc(0);
if (!addr1 || addr1 != (void *)allocator->start + chunk_hdr_size())
return -EINVAL;
/* Test second contiguous chunk with unaligned size */
addr2 = selftest_alloc(MIN_ALLOC_SIZE + 1);
if (!addr2)
return -EINVAL;
addr3 = selftest_alloc(0);
if (!addr3 ||
addr3 != addr2 + (2 * MIN_ALLOC_SIZE) + chunk_hdr_size())
return -EINVAL;
selftest_free(addr3);
/* Test chunk recycling */
selftest_free(addr1);
if (addr1 != selftest_alloc(0))
return -EINVAL;
/* Test chunk forward merging */
addr3 = selftest_alloc(0);
selftest_free(addr2);
selftest_free(addr1);
if (addr1 != selftest_alloc(MIN_ALLOC_SIZE * 2))
return -EINVAL;
selftest_free(addr1);
/* Test chunk splitting */
if (addr1 != selftest_alloc(0))
return -EINVAL;
if (addr2 != selftest_alloc(0))
return -EINVAL;
/* Test chunk backward merging */
selftest_free(addr1);
selftest_free(addr2);
if (addr1 != selftest_alloc(MIN_ALLOC_SIZE * 2))
return -EINVAL;
selftest_free(addr1);
/* Test chunk 3-way merging */
addr1 = selftest_alloc(0);
addr2 = selftest_alloc(0);
addr4 = selftest_alloc(0);
selftest_free(addr1);
selftest_free(addr3);
selftest_free(addr2);
if (addr1 != selftest_alloc(MIN_ALLOC_SIZE * 3))
return -EINVAL;
selftest_free(addr4);
selftest_free(addr1);
/* Test reclaiming */
if (addr1 != selftest_alloc(0))
return -EINVAL;
if (addr2 != selftest_alloc(PAGE_SIZE * 2))
return -EINVAL;
addr3 = selftest_alloc(0);
addr4 = selftest_alloc(PAGE_SIZE);
/* Test reclaiming the last chunk of the list */
selftest_free(addr4);
hyp_allocator_reclaim(allocator, &host_mc, SELFTEST_MAX_PAGES);
if (host_mc.nr_pages != SELFTEST_MAX_PAGES - 3)
return -EINVAL;
/* Test punching a hole in the middle of a free chunk ... */
selftest_free(addr2);
hyp_allocator_reclaim(allocator, &host_mc, SELFTEST_MAX_PAGES);
if (host_mc.nr_pages != SELFTEST_MAX_PAGES - 2)
return -EINVAL;
if (selftest_alloc(PAGE_SIZE))
return -EINVAL;
if (selftest_errno() != -ENOMEM)
return -EINVAL;
/* ... and to refill this hole */
ret = hyp_allocator_topup(allocator, &host_mc);
if (ret)
return ret;
/* Chunk at addr2 was made smaller by the reclaim */
if (addr2 != selftest_alloc(PAGE_SIZE))
return -EINVAL;
/* Test reclaiming the entire allocator from the host */
selftest_free(addr3);
selftest_free(addr2);
selftest_free(addr1);
if (addr1 != selftest_alloc(SELFTEST_MAX_PAGES * PAGE_SIZE - chunk_hdr_size()))
return -EINVAL;
selftest_free(addr1);
return 0;
}
#else
static int selftest_init(void) { return 0; }
#endif

View File

@ -800,6 +800,23 @@ DEFINE_KVM_HOST_HCALL(int, __pkvm_finalize_teardown_vm,
return __pkvm_finalize_teardown_vm(handle); return __pkvm_finalize_teardown_vm(handle);
} }
DEFINE_KVM_HOST_HCALL0(int, __pkvm_hyp_alloc_selftest)
{
struct pkvm_hyp_req req = { .type = PKVM_HYP_NO_REQ };
int ret = -EPERM;
#ifdef CONFIG_NVHE_EL2_DEBUG
ret = hyp_allocator_selftest();
if (ret == -ENOMEM) {
req.type = PKVM_HYP_REQ_HYP_ALLOC_SELFTEST;
req.mem.nr_pages = hyp_alloc_selftest_topup_needed();
}
#endif
pkvm_hyp_req_to_smccc(host_data_ptr(host_ctxt), &req);
return ret;
}
DEFINE_KVM_HOST_HCALL(int, __pkvm_hyp_topup, DEFINE_KVM_HOST_HCALL(int, __pkvm_hyp_topup,
enum pkvm_topup_id, id, phys_addr_t, head, unsigned long, nr_pages) enum pkvm_topup_id, id, phys_addr_t, head, unsigned long, nr_pages)
{ {
@ -814,6 +831,9 @@ DEFINE_KVM_HOST_HCALL(int, __pkvm_hyp_topup,
case PKVM_TOPUP_HYP_ALLOC: case PKVM_TOPUP_HYP_ALLOC:
ret = hyp_alloc_topup(&host_mc); ret = hyp_alloc_topup(&host_mc);
break; break;
case PKVM_TOPUP_HYP_ALLOC_SELFTEST:
ret = hyp_alloc_selftest_topup(&host_mc);
break;
default: default:
ret = -EINVAL; ret = -EINVAL;
} }
@ -835,6 +855,9 @@ DEFINE_KVM_HOST_HCALL(int, __pkvm_hyp_reclaim,
case PKVM_TOPUP_HYP_ALLOC: case PKVM_TOPUP_HYP_ALLOC:
hyp_alloc_reclaim(&host_mc, target); hyp_alloc_reclaim(&host_mc, target);
break; break;
case PKVM_TOPUP_HYP_ALLOC_SELFTEST:
hyp_alloc_selftest_reclaim(&host_mc, target);
break;
default: default:
ret = -EINVAL; ret = -EINVAL;
} }
@ -927,6 +950,7 @@ static const hcall_t host_hcall[] = {
HANDLE_FUNC(__kvm_enable_ssbs), HANDLE_FUNC(__kvm_enable_ssbs),
HANDLE_FUNC(__vgic_v3_init_lrs), HANDLE_FUNC(__vgic_v3_init_lrs),
HANDLE_FUNC(__vgic_v3_get_gic_config), HANDLE_FUNC(__vgic_v3_get_gic_config),
HANDLE_FUNC(__pkvm_hyp_alloc_selftest),
HANDLE_FUNC(__pkvm_prot_finalize), HANDLE_FUNC(__pkvm_prot_finalize),
HANDLE_FUNC(__kvm_adjust_pc), HANDLE_FUNC(__kvm_adjust_pc),

View File

@ -344,6 +344,22 @@ static int __init pkvm_drop_host_privileges(void)
return ret; return ret;
} }
void __init pkvm_selftests(void)
{
#ifdef CONFIG_NVHE_EL2_DEBUG
int ret = pkvm_call_hyp_req(__pkvm_hyp_alloc_selftest);
unsigned long reclaimed;
reclaimed = pkvm_hyp_reclaim(PKVM_TOPUP_HYP_ALLOC_SELFTEST, ULONG_MAX);
/* On failure, not all the pages may be reclaimable */
if (!ret)
WARN_ON(reclaimed != 6 /* SELFTEST_MAX_PAGES */);
else
kvm_err("pKVM hyp allocator selftest failed (%d)\n", ret);
#endif
}
static int __init finalize_pkvm(void) static int __init finalize_pkvm(void)
{ {
int ret; int ret;
@ -677,6 +693,9 @@ static int pkvm_handle_hyp_req(struct pkvm_hyp_req *req)
case PKVM_HYP_REQ_HYP_ALLOC: case PKVM_HYP_REQ_HYP_ALLOC:
ret = pkvm_hyp_topup(PKVM_TOPUP_HYP_ALLOC, req->mem.nr_pages); ret = pkvm_hyp_topup(PKVM_TOPUP_HYP_ALLOC, req->mem.nr_pages);
break; break;
case PKVM_HYP_REQ_HYP_ALLOC_SELFTEST:
ret = pkvm_hyp_topup(PKVM_TOPUP_HYP_ALLOC_SELFTEST, req->mem.nr_pages);
break;
} }
trace_kvm_handle_pkvm_hyp_req(req, ret); trace_kvm_handle_pkvm_hyp_req(req, ret);