Files
2026-08-24 10:03:24 +08:00

515 lines
13 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 2023 Google LLC
* Author: Mostafa Saleh <[email protected]>
*/
#include <asm/kvm_mmu.h>
#include <asm/kvm_pkvm.h>
#include <linux/cma.h>
#include <linux/of_reserved_mem.h>
#include <kvm/iommu.h>
#include <linux/arm-smccc.h>
#include <linux/kvm_host.h>
#define kvm_call_hyp_nvhe_mc(...) \
({ \
struct arm_smccc_res __res; \
do { \
__res = kvm_call_hyp_nvhe_smccc(__VA_ARGS__); \
} while (__res.a1 && !kvm_iommu_topup_memcache(&__res, GFP_KERNEL));\
__res.a1; \
})
#ifdef CONFIG_CMA
static phys_addr_t __topup_virt_to_phys(void *virt)
{
return __pa(virt);
}
static struct page *__kvm_iommu_alloc_from_cma(gfp_t gfp)
{
bool from_spare = (gfp & GFP_ATOMIC) == GFP_ATOMIC;
static atomic64_t spare_p;
struct page *p = NULL;
again:
if (from_spare)
return (struct page *)atomic64_cmpxchg(&spare_p, atomic64_read(&spare_p), 0);
p = kvm_iommu_cma_alloc();
if (!p) {
from_spare = true;
goto again;
}
/*
* Top-up the spare block if necessary. If we failed to update spare_p
* then someone did it already and we can proceed with that page.
*/
if (!atomic64_read(&spare_p)) {
if (!atomic64_cmpxchg(&spare_p, 0, (u64)p))
goto again;
}
return p;
}
static int __kvm_iommu_topup_memcache_from_cma(size_t size, gfp_t gfp, size_t *allocated)
{
*allocated = 0;
while (*allocated < size) {
struct page *p = __kvm_iommu_alloc_from_cma(gfp);
struct kvm_hyp_memcache mc;
if (!p)
return -ENOMEM;
init_hyp_memcache(&mc);
push_hyp_memcache(&mc, page_to_virt(p), __topup_virt_to_phys,
PMD_SHIFT - PAGE_SHIFT);
if (__pkvm_topup_hyp_alloc_mgt_mc(HYP_ALLOC_MGT_IOMMU_ID, &mc)) {
kvm_iommu_cma_release(p);
return -EINVAL;
}
*allocated += PMD_SIZE;
}
return 0;
}
#else
static int __kvm_iommu_topup_memcache_from_cma(size_t size, gfp_t gfp, size_t *allocated)
{
return -ENOMEM;
}
#endif /* CONFIG_CMA */
static int kvm_iommu_topup_memcache(struct arm_smccc_res *res, gfp_t gfp)
{
struct kvm_hyp_req req;
hyp_reqs_smccc_decode(res, &req);
if ((res->a1 == -ENOMEM) && (req.type != KVM_HYP_REQ_TYPE_MEM)) {
/*
* There is no way for drivers to populate hyp_alloc requests,
* so -ENOMEM + no request indicates that.
*/
return __pkvm_topup_hyp_alloc(1);
} else if (req.type != KVM_HYP_REQ_TYPE_MEM) {
return -EBADE;
}
if (req.mem.dest == REQ_MEM_DEST_HYP_IOMMU) {
size_t nr_pages, from_cma = 0;
int ret;
nr_pages = req.mem.nr_pages;
if (req.mem.sz_alloc < PMD_SIZE) {
size_t size = req.mem.sz_alloc * nr_pages;
ret = __kvm_iommu_topup_memcache_from_cma(size, gfp, &from_cma);
if (!ret)
return 0;
nr_pages -= from_cma / req.mem.sz_alloc;
}
return __pkvm_topup_hyp_alloc_mgt_gfp(HYP_ALLOC_MGT_IOMMU_ID,
nr_pages,
req.mem.sz_alloc,
gfp);
} else if (req.mem.dest == REQ_MEM_DEST_HYP_ALLOC) {
/* Fill hyp alloc*/
return __pkvm_topup_hyp_alloc(req.mem.nr_pages);
}
pr_err("Bogus mem request");
return -EBADE;
}
struct kvm_iommu_driver *iommu_driver;
extern struct kvm_iommu_ops *kvm_nvhe_sym(kvm_iommu_ops);
#ifdef CONFIG_CMA
static struct cma *kvm_iommu_cma;
extern phys_addr_t kvm_nvhe_sym(cma_base);
extern size_t kvm_nvhe_sym(cma_size);
#endif /* CONFIG_CMA */
int kvm_iommu_register_driver(struct kvm_iommu_driver *kern_ops)
{
if (WARN_ON(!kern_ops))
return -EINVAL;
/*
* Paired with smp_load_acquire(&iommu_driver)
* Ensure memory stores happening during a driver
* init are observed before executing kvm iommu callbacks.
*/
return cmpxchg_release(&iommu_driver, NULL, kern_ops) ? -EBUSY : 0;
}
EXPORT_SYMBOL(kvm_iommu_register_driver);
int kvm_iommu_init_hyp(struct kvm_iommu_ops *hyp_ops,
struct kvm_hyp_memcache *atomic_mc)
{
if (!hyp_ops)
return -EINVAL;
return kvm_call_hyp_nvhe(__pkvm_iommu_init, hyp_ops,
atomic_mc->head, atomic_mc->nr_pages);
}
EXPORT_SYMBOL(kvm_iommu_init_hyp);
#ifdef CONFIG_CMA
static int __init pkvm_iommu_cma_setup(struct reserved_mem *rmem)
{
int err;
if (!IS_ALIGNED(rmem->base | rmem->size, PMD_SIZE))
kvm_info("pKVM IOMMU reserved memory not PMD-aligned\n");
err = cma_init_reserved_mem(rmem->base, rmem->size, 0, rmem->name,
&kvm_iommu_cma, false);
if (err) {
kvm_err("Failed to init pKVM IOMMU reserved memory\n");
kvm_iommu_cma = NULL;
return err;
}
kvm_nvhe_sym(cma_base) = cma_get_base(kvm_iommu_cma);
kvm_nvhe_sym(cma_size) = cma_get_size(kvm_iommu_cma);
return 0;
}
RESERVEDMEM_OF_DECLARE(pkvm_cma, "pkvm,cma", pkvm_iommu_cma_setup);
static const u8 pmd_order = PMD_SHIFT - PAGE_SHIFT;
struct page *kvm_iommu_cma_alloc(void)
{
if (!kvm_iommu_cma)
return NULL;
return cma_alloc(kvm_iommu_cma, (1 << pmd_order), pmd_order, true);
}
EXPORT_SYMBOL(kvm_iommu_cma_alloc);
bool kvm_iommu_cma_release(struct page *p)
{
if (!kvm_iommu_cma || !p)
return false;
return cma_release(kvm_iommu_cma, p, 1 << pmd_order);
}
EXPORT_SYMBOL(kvm_iommu_cma_release);
#else
struct page *kvm_iommu_cma_alloc(void)
{
return NULL;
}
EXPORT_SYMBOL(kvm_iommu_cma_alloc);
bool kvm_iommu_cma_release(struct page *p)
{
return false;
}
EXPORT_SYMBOL(kvm_iommu_cma_release);
#endif /* CONFIG_CMA */
int kvm_iommu_init_driver(void)
{
if (!smp_load_acquire(&iommu_driver) || !iommu_driver->get_iommu_id_by_of) {
kvm_err("pKVM enabled without an IOMMU driver, do not run confidential workloads in virtual machines\n");
return -ENODEV;
}
kvm_hyp_iommu_domains = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO,
get_order(KVM_IOMMU_DOMAINS_ROOT_SIZE));
if (!kvm_hyp_iommu_domains)
return -ENOMEM;
kvm_hyp_iommu_domains = kern_hyp_va(kvm_hyp_iommu_domains);
return iommu_driver->init_driver();
}
EXPORT_SYMBOL(kvm_iommu_init_driver);
void kvm_iommu_remove_driver(void)
{
if (smp_load_acquire(&iommu_driver))
iommu_driver->remove_driver();
}
pkvm_handle_t kvm_get_iommu_id_by_of(struct device_node *np)
{
if (!iommu_driver)
return 0;
return iommu_driver->get_iommu_id_by_of(np);
}
static pkvm_handle_t kvm_get_iommu_id(struct device *dev)
{
return kvm_get_iommu_id_by_of(dev_of_node(dev));
}
int pkvm_iommu_suspend(struct device *dev)
{
int device_id = kvm_get_iommu_id(dev);
return kvm_call_hyp_nvhe(__pkvm_host_hvc_pd, device_id, 0);
}
EXPORT_SYMBOL(pkvm_iommu_suspend);
int pkvm_iommu_resume(struct device *dev)
{
int device_id = kvm_get_iommu_id(dev);
return kvm_call_hyp_nvhe(__pkvm_host_hvc_pd, device_id, 1);
}
EXPORT_SYMBOL(pkvm_iommu_resume);
int kvm_iommu_share_hyp_sg(struct kvm_iommu_sg *sg, unsigned int nents)
{
size_t nr_pages = PAGE_ALIGN(sizeof(*sg) * nents) >> PAGE_SHIFT;
phys_addr_t sg_pfn = virt_to_phys(sg) >> PAGE_SHIFT;
int i;
int ret;
for (i = 0 ; i < nr_pages ; ++i) {
ret = kvm_call_hyp_nvhe(__pkvm_host_share_hyp, sg_pfn + i);
if (ret)
return ret;
}
return 0;
}
EXPORT_SYMBOL(kvm_iommu_share_hyp_sg);
int kvm_iommu_unshare_hyp_sg(struct kvm_iommu_sg *sg, unsigned int nents)
{
size_t nr_pages = PAGE_ALIGN(sizeof(*sg) * nents) >> PAGE_SHIFT;
phys_addr_t sg_pfn = virt_to_phys(sg) >> PAGE_SHIFT;
int i;
int ret;
for (i = 0 ; i < nr_pages ; ++i) {
ret = kvm_call_hyp_nvhe(__pkvm_host_unshare_hyp, sg_pfn + i);
if (ret)
return ret;
}
return 0;
}
EXPORT_SYMBOL(kvm_iommu_unshare_hyp_sg);
int kvm_iommu_device_num_ids(struct device *dev)
{
if (iommu_driver->get_device_iommu_num_ids)
return iommu_driver->get_device_iommu_num_ids(dev);
return 0;
}
EXPORT_SYMBOL_GPL(kvm_iommu_device_num_ids);
int kvm_iommu_device_id(struct device *dev, u32 idx,
pkvm_handle_t *out_iommu, u32 *out_sid)
{
if (iommu_driver->get_device_iommu_id)
return iommu_driver->get_device_iommu_id(dev, idx, out_iommu, out_sid);
return -ENODEV;
}
EXPORT_SYMBOL_GPL(kvm_iommu_device_id);
int kvm_iommu_guest_alloc_mc(struct kvm_hyp_memcache *mc, u32 pgsize, u32 nr_pages)
{
u8 order = get_order(pgsize);
/* Driver might have dedicated allocator especially if it needs large pages. */
if (iommu_driver && iommu_driver->guest_alloc && iommu_driver->guest_free)
return __topup_hyp_memcache(mc, nr_pages, iommu_driver->guest_alloc,
kvm_host_pa, 0, order);
return topup_hyp_memcache(mc, nr_pages, order);
}
void kvm_iommu_guest_free_mc(struct kvm_hyp_memcache *mc)
{
if (iommu_driver && iommu_driver->guest_alloc && iommu_driver->guest_free)
__free_hyp_memcache(mc, iommu_driver->guest_free,
kvm_host_va, 0);
else
free_hyp_memcache(mc);
}
/* Hypercall abstractions exposed to kernel IOMMU drivers */
int kvm_iommu_attach_dev(pkvm_handle_t iommu_id, pkvm_handle_t domain_id,
unsigned int endpoint, unsigned int pasid,
unsigned int ssid_bits, unsigned long flags)
{
return kvm_call_hyp_nvhe_mc(__pkvm_host_iommu_attach_dev, iommu_id, domain_id,
endpoint, pasid, ssid_bits, flags);
}
EXPORT_SYMBOL(kvm_iommu_attach_dev);
int kvm_iommu_attach_dev_nested(pkvm_handle_t iommu_id, pkvm_handle_t domain_id,
unsigned int endpoint, unsigned int pasid,
unsigned long flags, void *s1_desc_hva, size_t s1_desc_size)
{
return kvm_call_hyp_nvhe_mc(__pkvm_host_iommu_attach_dev_nested, iommu_id, domain_id,
endpoint, pasid, flags, s1_desc_hva, s1_desc_size);
}
EXPORT_SYMBOL(kvm_iommu_attach_dev_nested);
int kvm_iommu_detach_dev(pkvm_handle_t iommu_id, pkvm_handle_t domain_id,
unsigned int endpoint, unsigned int pasid)
{
return kvm_call_hyp_nvhe(__pkvm_host_iommu_detach_dev, iommu_id, domain_id,
endpoint, pasid);
}
EXPORT_SYMBOL(kvm_iommu_detach_dev);
int kvm_iommu_detach_dev_nested(pkvm_handle_t iommu_id, pkvm_handle_t domain_id,
unsigned int endpoint, unsigned int pasid)
{
return kvm_call_hyp_nvhe(__pkvm_host_iommu_detach_dev_nested, iommu_id, domain_id,
endpoint, pasid);
}
EXPORT_SYMBOL(kvm_iommu_detach_dev_nested);
int kvm_iommu_iotlb_inv_nested_domain(pkvm_handle_t domain_id, unsigned long iova,
size_t size, size_t granule, bool leaf)
{
return kvm_call_hyp_nvhe(__pkvm_host_iommu_iotlb_inv_nested_domain, domain_id, iova, size,
granule, leaf);
}
EXPORT_SYMBOL(kvm_iommu_iotlb_inv_nested_domain);
int kvm_iommu_nested_cfg_sync(pkvm_handle_t iommu_id, void *cmd_desc_hva, size_t cmd_desc_size)
{
return kvm_call_hyp_nvhe(__pkvm_host_iommu_nested_cfg_sync, iommu_id, cmd_desc_hva,
cmd_desc_size);
}
EXPORT_SYMBOL(kvm_iommu_nested_cfg_sync);
int kvm_iommu_alloc_domain(pkvm_handle_t domain_id, int type)
{
return kvm_call_hyp_nvhe_mc(__pkvm_host_iommu_alloc_domain,
domain_id, type);
}
EXPORT_SYMBOL(kvm_iommu_alloc_domain);
int kvm_iommu_free_domain(pkvm_handle_t domain_id)
{
return kvm_call_hyp_nvhe(__pkvm_host_iommu_free_domain, domain_id);
}
EXPORT_SYMBOL(kvm_iommu_free_domain);
int kvm_iommu_map_pages(pkvm_handle_t domain_id, unsigned long iova,
phys_addr_t paddr, size_t pgsize, size_t pgcount,
int prot, gfp_t gfp, size_t *total_mapped)
{
size_t mapped;
size_t size = pgsize * pgcount;
struct arm_smccc_res res;
do {
res = kvm_call_hyp_nvhe_smccc(__pkvm_host_iommu_map_pages, domain_id,
iova, paddr, pgsize, pgcount, prot);
mapped = res.a1;
iova += mapped;
paddr += mapped;
WARN_ON(mapped % pgsize);
WARN_ON(mapped > pgcount * pgsize);
pgcount -= mapped / pgsize;
*total_mapped += mapped;
} while (*total_mapped < size && !kvm_iommu_topup_memcache(&res, gfp));
if (*total_mapped < size)
return -EINVAL;
return 0;
}
EXPORT_SYMBOL(kvm_iommu_map_pages);
size_t kvm_iommu_unmap_pages(pkvm_handle_t domain_id, unsigned long iova,
size_t pgsize, size_t pgcount)
{
size_t unmapped;
size_t total_unmapped = 0;
size_t size = pgsize * pgcount;
struct arm_smccc_res res;
do {
res = kvm_call_hyp_nvhe_smccc(__pkvm_host_iommu_unmap_pages,
domain_id, iova, pgsize, pgcount);
unmapped = res.a1;
total_unmapped += unmapped;
iova += unmapped;
WARN_ON(unmapped % pgsize);
pgcount -= unmapped / pgsize;
/*
* The page table driver can unmap less than we asked for. If it
* didn't unmap anything at all, then it either reached the end
* of the range, or it needs a page in the memcache to break a
* block mapping.
*/
} while (total_unmapped < size &&
(unmapped || !kvm_iommu_topup_memcache(&res, GFP_ATOMIC)));
return total_unmapped;
}
EXPORT_SYMBOL(kvm_iommu_unmap_pages);
phys_addr_t kvm_iommu_iova_to_phys(pkvm_handle_t domain_id, unsigned long iova)
{
return kvm_call_hyp_nvhe(__pkvm_host_iommu_iova_to_phys, domain_id, iova);
}
EXPORT_SYMBOL(kvm_iommu_iova_to_phys);
int kvm_iommu_iotlb_sync_map(pkvm_handle_t domain_id, unsigned long iova, size_t size)
{
return kvm_call_hyp_nvhe(__pkvm_host_iommu_iotlb_sync_map, domain_id, iova, size);
}
EXPORT_SYMBOL(kvm_iommu_iotlb_sync_map);
size_t kvm_iommu_map_sg(pkvm_handle_t domain_id, struct kvm_iommu_sg *sg,
unsigned long iova, unsigned int nent,
unsigned int prot, gfp_t gfp)
{
size_t mapped, total_mapped = 0;
struct arm_smccc_res res;
do {
res = kvm_call_hyp_nvhe_smccc(__pkvm_host_iommu_map_sg,
domain_id, iova, sg, nent, prot);
mapped = res.a1;
iova += mapped;
total_mapped += mapped;
/* Skip mapped */
while (mapped) {
if (mapped < (sg->pgsize * sg->pgcount)) {
sg->phys += mapped;
sg->pgcount -= mapped / sg->pgsize;
mapped = 0;
} else {
mapped -= sg->pgsize * sg->pgcount;
sg++;
nent--;
}
}
} while (nent && !kvm_iommu_topup_memcache(&res, gfp));
return total_mapped;
}
EXPORT_SYMBOL(kvm_iommu_map_sg);