Re: [PATCH 20/20] KVM: arm64: Stage-2 huge mappings for protected VMs

[email protected] Mon, 03 Aug 2026 11:04:11 +0000
Newsgroups dev.linux.lists.kvmarm
Message-ID <[email protected]>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] A TOCTOU race and insufficient `folio_test_large()` validation in `pkvm_mem_abort()` allows malicious host userspace to trigger a DoS or map up to 2MB of arbitrary unowned physical memory into the guest.
- [High] Missing verification of pinned PFNs in `__pkvm_pgtable_stage2_split()` causes a THP refcount underflow (kernel panic) and a memory leak when raced with host userspace unmapping.
- [High] Unconditional suppression of `-EAGAIN` in `pkvm_mem_abort()` causes a deterministic livelock (infinite fault loop) when a PMD-sized mapping overlaps with a pre-existing 4K mapping.
- [High] Missing validation of the stage-2 block granule against the guest's requested fault size in `pkvm_mem_abort()` silently breaks pKVM isolation.
--

--- Patch [20]: [PATCH 20/20] KVM: arm64: Stage-2 huge mappings for protected VMs ---
commit 66c372ae561bfc5bac1a23e79526b597c8fa02fd
Author: Vincent Donnefort <[email protected]>

KVM: arm64: Stage-2 huge mappings for protected VMs

Enable PMD-sized stage-2 block mappings for protected VMs. This is
possible whenever the stage-1 mapping allows it, that is if it itself
backed by THPs.

When a THP is found, an entire PMD_SIZE mapping is donated to the guest.
This mapping can only be broken down via the HVC
__pkvm_host_split_guest() which the hypervisor can request with
PKVM_HYP_REQ_SPLIT.

> diff --git a/arch/arm64/kvm/mmu.c b/arch/arm64/kvm/mmu.c
> index 6c941aaa10c63..46820240d2337 100644
> --- a/arch/arm64/kvm/mmu.c
> +++ b/arch/arm64/kvm/mmu.c
[ ... ]
> @@ -1755,25 +1737,59 @@ static int pkvm_mem_abort(const struct kvm_s2_fault_desc *s2fd)
>  		goto unpin;
>  	}
>  
> +	pfn = page_to_pfn(page);
> +	gfn = gpa_to_gfn(s2fd->fault_ipa);
> +
> +	page_size = transparent_hugepage_adjust(kvm, s2fd->memslot, s2fd->hva, &pfn, &gfn);

[Severity: High]
Does this skip validating the stage-2 block granule against the guest's
requested fault size?

Deriving the block size solely from the host's THP state via
transparent_hugepage_adjust() without validating it against the fault
granule can install a larger block mapping than the guest requested. Is
there a missing check here to ensure the resulting page_size matches the
expected fault granule before donating host memory?

> +	if (page_size < 0) {
> +		ret = page_size;
> +		goto unpin;
> +	} else if (page_size == PMD_SIZE) {
> +		if (WARN_ON(!folio_test_large(page_folio(page)))) {
> +			ret = -EINVAL;
> +			goto unpin;
> +		}
> +	}

[Severity: Critical]
Is this folio_test_large() check sufficient to prevent a TOCTOU race
and unintended memory donation?

Since mmap_read_lock() is dropped before this check, a malicious host
userspace could unmap the faulting page and remap it as a PMD-mapped THP.
If the initially pinned page was a smaller mTHP (like a 64KB folio),
folio_test_large() will return true, bypassing the WARN_ON.

transparent_hugepage_adjust() aligns the 64KB folio's PFN down to a PMD
boundary, which is then passed to pkvm_pgtable_stage2_map(), granting
the guest RWX access to 2MB of unrelated physical memory. Could this
be prevented by stricter validation of the page size rather than just
checking if it is large?

> +
> +	ret = account_locked_vm(mm, page_size / PAGE_SIZE, true);
> +	if (ret)
> +		goto unpin;
> +
>  	write_lock(&kvm->mmu_lock);
> -	ret = pkvm_pgtable_stage2_map(pgt, s2fd->fault_ipa, PAGE_SIZE,
> -				      page_to_phys(page), KVM_PGTABLE_PROT_RWX,
> +	ret = pkvm_pgtable_stage2_map(pgt, gfn_to_gpa(gfn), page_size,
> +				      __pfn_to_phys(pfn), KVM_PGTABLE_PROT_RWX,
>  				      hyp_memcache, 0);
>  	write_unlock(&kvm->mmu_lock);
>  	if (ret) {
>  		if (ret == -EAGAIN)
>  			ret = 0;
> -		goto unpin;
> +		goto dec_account;
>  	}

[Severity: High]
Will masking -EAGAIN here cause a deterministic livelock?

If a PMD-sized mapping overlaps with a pre-existing 4K mapping,
pkvm_pgtable_stage2_map() returns -EAGAIN. Masking this error and returning
success without installing a mapping causes the guest to re-execute the
instruction and hit the exact same fault path indefinitely. Should this
fall back to a PAGE_SIZE mapping instead of suppressing the error?

[ ... ]
> diff --git a/arch/arm64/kvm/pkvm.c b/arch/arm64/kvm/pkvm.c
> index 089b77cf2f6a9..0b591c44998d0 100644
> --- a/arch/arm64/kvm/pkvm.c
> +++ b/arch/arm64/kvm/pkvm.c
[ ... ]
> @@ -541,10 +539,8 @@ int pkvm_pgtable_stage2_map(struct kvm_pgtable *pgt, u64 addr, u64 size,
>  			return ret ? -EFAULT : -EAGAIN;
>  		}
>  
> -		ret = kvm_call_hyp_nvhe(__pkvm_host_donate_guest, pfn, gfn, 1);
> +		ret = kvm_call_hyp_nvhe(__pkvm_host_donate_guest, pfn, gfn, size / PAGE_SIZE);
>  	} else {

[Severity: High]
Does __pkvm_pgtable_stage2_split() properly verify pinned PFNs when splitting
a THP?

When the hypervisor requests a split, __pkvm_pgtable_stage2_split() calls
pin_user_pages() to pin the remaining pages. However, pkvm_mapping_split()
ignores the returned array and populates the tracking mappings by
sequentially incrementing the old THP's base PFN.

If the host userspace evicts the original THP before the split,
pin_user_pages() pins new anonymous pages that are never recorded and will
leak. Later, __pkvm_pgtable_stage2_reclaim() will call
unpin_user_pages_dirty_lock() on the old THP's subpages 512 times,
causing a refcount underflow.

-- 
Sashiko AI review ยท https://sashiko.dev/#/patchset/[email protected]?part=20