[PATCH 18/20] KVM: arm64: Add PKVM_HYP_REQ_SPLIT

Wei-Lin Chang weilin.chang at arm.com
Mon Sep 7 11:15:05 PDT 2026


On Mon, Aug 03, 2026 at 11:09:02AM +0100, Vincent Donnefort wrote:
> With the upcoming support for stage-2 huge mappings for protected VMs,
> we need a way to split blocks. Since the host has its own "copy" of the
> guest stage-2 in the pkvm_mappings rb-tree, the split must be done
> simultaneously for both that tree and the guest stage-2. Therefore the
> hypervisor can't do it on its own and must rely on the host for this
> operation.
> 
> Create a pKVM hypervisor request to ask the host to split a specified
> region of the guest. On this request, the host can synchronise the split
> of both guest stage-2 (HVC __pkvm_host_split_guest) and the
> pkvm_mappings tree. It ensures a concurrent VM teardown can't observe a
> PMD_SIZE pkvm_mapping while the guest stage-2 is PAGE_SIZE.
> 
> Signed-off-by: Vincent Donnefort <vdonnefort at google.com>
> 
> diff --git a/arch/arm64/include/asm/kvm_host.h b/arch/arm64/include/asm/kvm_host.h
> index d01e6954d363..927d9de643fa 100644
> --- a/arch/arm64/include/asm/kvm_host.h
> +++ b/arch/arm64/include/asm/kvm_host.h
> @@ -87,11 +87,18 @@ void kvm_arm_vcpu_destroy(struct kvm_vcpu *vcpu);
>  
>  enum pkvm_hyp_req_type {
>  	PKVM_HYP_NO_REQ = 0,
> +	PKVM_HYP_REQ_SPLIT,
>  	__PKVM_HYP_REQ_TYPE_MAX,
>  };
>  
>  struct pkvm_hyp_req {
>  	u8 type;
> +	union {
> +		struct {
> +			u32	nr_pages;
> +			u64	gfn;
> +		} split;
> +	};
>  };
>  
>  struct kvm_hyp_memcache {
> diff --git a/arch/arm64/include/asm/kvm_pkvm.h b/arch/arm64/include/asm/kvm_pkvm.h
> index baafbd7ca215..370eddeefbae 100644
> --- a/arch/arm64/include/asm/kvm_pkvm.h
> +++ b/arch/arm64/include/asm/kvm_pkvm.h
> @@ -193,7 +193,10 @@ static inline size_t pkvm_host_sve_state_size(void)
>  }
>  
>  struct pkvm_mapping {
> -	struct rb_node node;
> +	union {
> +		struct rb_node node;
> +		struct list_head list;
> +	};
>  	u64 gfn;
>  	u64 pfn;
>  	u64 nr_pages;
> diff --git a/arch/arm64/kvm/pkvm.c b/arch/arm64/kvm/pkvm.c
> index 379bdc2b258a..089b77cf2f6a 100644
> --- a/arch/arm64/kvm/pkvm.c
> +++ b/arch/arm64/kvm/pkvm.c
> @@ -365,6 +365,69 @@ INTERVAL_TREE_DEFINE(struct pkvm_mapping, node, u64, __subtree_last,
>  		       });									\
>  	    )
>  
> +static void pkvm_mapping_free_spares(struct list_head *spares)
> +{
> +	struct pkvm_mapping *m, *tmp;
> +
> +	list_for_each_entry_safe(m, tmp, spares, list) {
> +		list_del(&m->list);
> +		kfree(m);
> +	}
> +}
> +
> +static int pkvm_mapping_alloc_spares(struct list_head *head, u64 nr_spares)
> +{
> +	struct pkvm_mapping *m;
> +
> +	while (nr_spares--) {
> +		m = kzalloc_obj(*m);
> +		if (!m) {
> +			pkvm_mapping_free_spares(head);
> +			return -ENOMEM;
> +		}
> +
> +		list_add(&m->list, head);
> +	}
> +
> +	return 0;
> +}
> +
> +static bool pkvm_mapping_can_split(struct pkvm_mapping *mapping)
> +{
> +	return mapping && (mapping->nr_pages * PAGE_SIZE == PMD_SIZE);
> +}
> +
> +static void pkvm_mapping_split(struct pkvm_mapping *mapping, struct kvm_pgtable *pgt,
> +			       struct list_head *spares)
> +{
> +	struct kvm *kvm = kvm_s2_mmu_to_kvm(pgt->mmu);
> +	u64 nr_pages = mapping->nr_pages - 1;
> +	gfn_t gfn = mapping->gfn + 1;
> +	u64 pfn = mapping->pfn + 1;
> +
> +	lockdep_assert_held_write(&kvm->mmu_lock);
> +
> +	pkvm_mapping_remove(mapping, &pgt->pkvm_mappings);
> +	mapping->nr_pages = 1;
> +	pkvm_mapping_insert(mapping, &pgt->pkvm_mappings);
> +
> +	while (nr_pages--) {
> +		struct pkvm_mapping *m;
> +
> +		if (WARN_ON(list_empty(spares)))
> +			break;
> +
> +		m = list_first_entry(spares, struct pkvm_mapping, list);
> +		list_del(&m->list);
> +
> +		m->nr_pages = 1;
> +		m->gfn = gfn++;
> +		m->pfn = pfn++;
> +
> +		pkvm_mapping_insert(m, &pgt->pkvm_mappings);
> +	}
> +}
> +

I don't see anything wrong with this linked list approach, but have you
considered just allocating an array of pointers for the allocated
pkvm_mappings? The union in pkvm_mappings can be avoided.

>  int pkvm_pgtable_stage2_init(struct kvm_pgtable *pgt, struct kvm_s2_mmu *mmu,
>  			     struct kvm_pgtable_mm_ops *mm_ops)
>  {
> @@ -619,6 +682,91 @@ int pkvm_pgtable_stage2_split(struct kvm_pgtable *pgt, u64 addr, u64 size, void
>  	return -EINVAL;
>  }
>  
> +/*
> + * Splitting is only expected on the back of a guest HVC, while
> + * pkvm_pgtable_stage2_split() can be called with dirty logging.
> + */
> +static int __pkvm_pgtable_stage2_split(struct kvm_vcpu *vcpu, phys_addr_t ipa, u64 size)
> +{
> +	struct kvm_hyp_memcache *mc = &vcpu->arch.pkvm_memcache;
> +	struct kvm_pgtable *pgt = vcpu->arch.hw_mmu->pgt;
> +	struct page **pages __free(kfree) = NULL;
> +	struct mm_struct *mm = current->mm;
> +	struct kvm_memory_slot *memslot;
> +	struct pkvm_mapping *mapping;
> +	struct kvm *kvm = vcpu->kvm;
> +	struct list_head spares;
> +	unsigned long hva;
> +	bool writable;
> +	u64 nr_pages;
> +	int ret, idx;
> +	gfn_t gfn;
> +
> +	if (WARN_ON(!kvm_vm_is_protected(kvm)))
> +		return -EINVAL;
> +
> +	if (!IS_ALIGNED(ipa, PMD_SIZE) || size != PMD_SIZE)
> +		return -EINVAL;
> +
> +	ret = topup_hyp_memcache(mc, 1);
> +	if (ret)
> +		return ret;
> +
> +	/* We already have 1 pin on the huge-page */
> +	gfn = gpa_to_gfn(ipa) + 1;
> +	nr_pages = (size / PAGE_SIZE) - 1;
> +	pages = kmalloc_objs(struct page *, nr_pages);
> +	if (!pages)
> +		return -ENOMEM;
> +
> +	INIT_LIST_HEAD(&spares);
> +	ret = pkvm_mapping_alloc_spares(&spares, nr_pages);
> +	if (ret)
> +		return ret;
> +
> +	idx = srcu_read_lock(&kvm->srcu);
> +	memslot = gfn_to_memslot(kvm, gfn);
> +	hva = gfn_to_hva_memslot_prot(memslot, gfn, &writable);
> +	if (kvm_is_error_hva(hva) || !writable) {
> +		ret = -EFAULT;
> +		goto unlock_srcu;
> +	}
> +
> +	mmap_read_lock(mm);
> +	ret = pin_user_pages(hva, nr_pages, FOLL_HWPOISON | FOLL_LONGTERM | FOLL_WRITE, pages);

If I am not mistaken this is for getting additional pins only. hva walk
and pages[] are just by-product.

Can we use folio_add_pins() here instead? Then I think we won't need
pages[], the hva walk, and unpin_user_pages().

Thanks,
Wei-Lin Chang

> +	mmap_read_unlock(mm);
> +	if (ret != nr_pages) {
> +		if (ret > 0)
> +			unpin_user_pages(pages, ret);
> +		ret = -EFAULT;
> +		goto unlock_srcu;
> +	}
> +
> +	write_lock(&kvm->mmu_lock);
> +	mapping = pkvm_mapping_iter_first(&pgt->pkvm_mappings, ipa, ipa + size - 1);
> +	if (!pkvm_mapping_can_split(mapping)) {
> +		ret = -EINVAL;
> +		goto unlock_mmu;
> +	}
> +
> +	ret = kvm_call_hyp_nvhe(__pkvm_host_split_guest, gpa_to_gfn(ipa), size / PAGE_SIZE);
> +	if (ret)
> +		goto unlock_mmu;
> +
> +	pkvm_mapping_split(mapping, pgt, &spares);
> +
> +unlock_mmu:
> +	write_unlock(&kvm->mmu_lock);
> +	if (ret)
> +		unpin_user_pages(pages, nr_pages);
> +
> +unlock_srcu:
> +	srcu_read_unlock(&kvm->srcu, idx);
> +	pkvm_mapping_free_spares(&spares);
> +
> +	return ret;
> +}
> +

[...]




More information about the linux-arm-kernel mailing list