| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (C) 2023 Google LLC |
| * Author: Mostafa Saleh <smostafa@google.com> |
| */ |
| #include <linux/of_platform.h> |
| #include <linux/arm-smccc.h> |
| #include <linux/iommu.h> |
| #include <linux/maple_tree.h> |
| #include <linux/pci.h> |
| |
| |
| #define ASSERT(cond) \ |
| do { \ |
| if (!(cond)) { \ |
| pr_err("line %d: assertion failed: %s\n", \ |
| __LINE__, #cond); \ |
| return -1; \ |
| } \ |
| } while (0) |
| |
| #define FEAUTRE_PGSIZE_BITMAP 0x1 |
| #define DRIVER_VERSION 0x1000ULL |
| |
| struct pviommu { |
| struct iommu_device iommu; |
| u32 id; |
| u32 pgsize_bitmap; |
| }; |
| |
| struct pviommu_domain { |
| struct iommu_domain domain; |
| unsigned long id; /* pKVM domain ID. */ |
| struct maple_tree mappings; /* IOVA -> IPA */ |
| }; |
| |
| struct pviommu_master { |
| struct device *dev; |
| struct pviommu *iommu; |
| u32 ssid_bits; |
| struct pviommu_domain *domain; |
| }; |
| |
| /* Ranges are inclusive for all functions. */ |
| static void pviommu_domain_insert_map(struct pviommu_domain *pv_domain, |
| u64 start, u64 end, u64 val) |
| { |
| if (end < start) |
| return; |
| |
| mtree_store_range(&pv_domain->mappings, start, end, val, GFP_KERNEL); |
| } |
| |
| static void pviommu_domain_remove_map(struct pviommu_domain *pv_domain, |
| u64 start, u64 end) |
| { |
| /* Range can cover multiple entries. */ |
| while (start < end) { |
| MA_STATE(mas, &pv_domain->mappings, start, ULONG_MAX); |
| u64 entry = (u64)mas_find(&mas, start); |
| u64 old_start, old_end; |
| |
| old_start = mas.index; |
| old_end = mas.last; |
| mas_erase(&mas); |
| |
| /* Insert the rest if no removed*/ |
| if (start > old_start) |
| mtree_store_range(&pv_domain->mappings, old_start, start - 1, entry, GFP_KERNEL); |
| |
| if (old_end > end) |
| mtree_store_range(&pv_domain->mappings, end + 1, old_end, entry + (end - old_start + 1), GFP_KERNEL); |
| |
| start = old_end + 1; |
| } |
| } |
| |
| static u64 pviommu_domain_find(struct pviommu_domain *pv_domain, u64 key) |
| { |
| MA_STATE(mas, &pv_domain->mappings, key, ULONG_MAX); |
| u64 entry = (u64)mas_find(&mas, key); |
| |
| /* No entry. */ |
| if (mas.index == mas.last) |
| return 0; |
| |
| return (key - mas.index) + entry; |
| } |
| |
| static int pviommu_map_pages(struct iommu_domain *domain, unsigned long iova, |
| phys_addr_t paddr, size_t pgsize, size_t pgcount, |
| int prot, gfp_t gfp, size_t *mapped) |
| { |
| int ret; |
| struct pviommu_domain *pv_domain = container_of(domain, struct pviommu_domain, domain); |
| struct arm_smccc_res res; |
| size_t requested_size = pgsize * pgcount, cur_mapped; |
| |
| *mapped = 0; |
| while (*mapped < requested_size) { |
| arm_smccc_1_1_hvc(ARM_SMCCC_VENDOR_HYP_KVM_IOMMU_MAP_FUNC_ID, |
| pv_domain->id, iova, paddr, pgsize, pgcount, prot, &res); |
| cur_mapped = res.a1; |
| ret = res.a0; |
| *mapped += cur_mapped; |
| iova += cur_mapped; |
| paddr += cur_mapped; |
| pgcount -= cur_mapped / pgsize; |
| if (ret && (ret != -EAGAIN)) |
| break; |
| } |
| |
| if (*mapped) |
| pviommu_domain_insert_map(pv_domain, iova, iova + *mapped - 1, paddr); |
| |
| return ret; |
| } |
| |
| static size_t pviommu_unmap_pages(struct iommu_domain *domain, unsigned long iova, |
| size_t pgsize, size_t pgcount, |
| struct iommu_iotlb_gather *gather) |
| { |
| int ret; |
| struct pviommu_domain *pv_domain = container_of(domain, struct pviommu_domain, domain); |
| struct arm_smccc_res res; |
| size_t total_unmapped = 0, unmapped, requested_size = pgsize * pgcount; |
| |
| while (total_unmapped < requested_size) { |
| arm_smccc_1_1_hvc(ARM_SMCCC_VENDOR_HYP_KVM_IOMMU_UNMAP_FUNC_ID, |
| pv_domain->id, iova, pgsize, pgcount, &res); |
| ret = res.a0; |
| unmapped = res.a1; |
| total_unmapped += unmapped; |
| iova += unmapped; |
| pgcount -= unmapped / pgsize; |
| if (ret && (ret != -EAGAIN)) |
| break; |
| } |
| |
| if (total_unmapped) |
| pviommu_domain_remove_map(pv_domain, iova, iova + total_unmapped - 1); |
| |
| return total_unmapped; |
| } |
| |
| static phys_addr_t pviommu_iova_to_phys(struct iommu_domain *domain, dma_addr_t iova) |
| { |
| struct pviommu_domain *pv_domain = container_of(domain, struct pviommu_domain, domain); |
| |
| return pviommu_domain_find(pv_domain, iova); |
| } |
| |
| static void pviommu_domain_free(struct iommu_domain *domain) |
| { |
| struct pviommu_domain *pv_domain = container_of(domain, struct pviommu_domain, domain); |
| struct arm_smccc_res res; |
| |
| arm_smccc_1_1_hvc(ARM_SMCCC_VENDOR_HYP_KVM_IOMMU_FREE_DOMAIN_FUNC_ID, |
| pv_domain->id, &res); |
| if (res.a0 != SMCCC_RET_SUCCESS) |
| pr_err("Failed to free domain %d\n", res.a0); |
| |
| mtree_destroy(&pv_domain->mappings); |
| kfree(pv_domain); |
| } |
| |
| static int smccc_to_linux_ret(u64 smccc_ret) |
| { |
| switch(smccc_ret) |
| { |
| case SMCCC_RET_SUCCESS: |
| return 0; |
| case SMCCC_RET_NOT_SUPPORTED: |
| return -EOPNOTSUPP; |
| case SMCCC_RET_NOT_REQUIRED: |
| return -ENOENT; |
| case SMCCC_RET_INVALID_PARAMETER: |
| return -EINVAL; |
| }; |
| |
| return -ENODEV; |
| } |
| |
| static int pviommu_set_dev_pasid(struct iommu_domain *domain, |
| struct device *dev, ioasid_t pasid) |
| { |
| int ret = 0, i; |
| struct iommu_fwspec *fwspec = dev_iommu_fwspec_get(dev); |
| struct pviommu *pv; |
| struct pviommu_domain *pv_domain = container_of(domain, struct pviommu_domain, domain); |
| struct pviommu_master *master; |
| struct arm_smccc_res res; |
| u32 sid; |
| |
| if (!fwspec) |
| return -ENOENT; |
| |
| master = dev_iommu_priv_get(dev); |
| pv = master->iommu; |
| master->domain = pv_domain; |
| |
| for (i = 0; i < fwspec->num_ids; i++) { |
| sid = fwspec->ids[i]; |
| arm_smccc_1_1_hvc(ARM_SMCCC_VENDOR_HYP_KVM_IOMMU_ATTACH_DEV_FUNC_ID, |
| pv->id, sid, pasid, |
| pv_domain->id, master->ssid_bits, &res); |
| if (res.a0) { |
| ret = smccc_to_linux_ret(res.a0); |
| break; |
| } |
| } |
| |
| if (ret) { |
| while(i--) { |
| arm_smccc_1_1_hvc(ARM_SMCCC_VENDOR_HYP_KVM_IOMMU_DETACH_DEV_FUNC_ID, |
| pv->id, sid, 0 /* PASID */, |
| pv_domain->id, &res); |
| } |
| } |
| |
| return ret; |
| } |
| |
| static int pviommu_attach_dev(struct iommu_domain *domain, struct device *dev) |
| { |
| return pviommu_set_dev_pasid(domain, dev, 0); |
| } |
| |
| static void pviommu_remove_dev_pasid(struct device *dev, ioasid_t pasid) |
| { |
| int i; |
| struct pviommu_master *master = dev_iommu_priv_get(dev); |
| struct iommu_fwspec *fwspec = dev_iommu_fwspec_get(dev); |
| struct pviommu *pv = master->iommu; |
| struct pviommu_domain *pv_domain = master->domain; |
| struct arm_smccc_res res; |
| u32 sid; |
| |
| if (!fwspec) |
| return; |
| |
| for (i = 0; i < fwspec->num_ids; i++) { |
| sid = fwspec->ids[i]; |
| arm_smccc_1_1_hvc(ARM_SMCCC_VENDOR_HYP_KVM_IOMMU_DETACH_DEV_FUNC_ID, |
| pv->id, sid, pasid, pv_domain->id, &res); |
| if (res.a0 != SMCCC_RET_SUCCESS) |
| dev_err(dev, "Failed to detach_dev sid %d, err %d\n", sid, res.a0); |
| } |
| } |
| |
| static void pviommu_detach_dev(struct device *dev) |
| { |
| pviommu_remove_dev_pasid(dev, 0); |
| } |
| |
| static struct iommu_domain *pviommu_domain_alloc(unsigned int type) |
| { |
| struct pviommu_domain *pv_domain; |
| struct arm_smccc_res res; |
| |
| if (type != IOMMU_DOMAIN_UNMANAGED && |
| type != IOMMU_DOMAIN_DMA) |
| return NULL; |
| |
| |
| pv_domain = kzalloc(sizeof(*pv_domain), GFP_KERNEL); |
| if (!pv_domain) |
| return NULL; |
| |
| mt_init(&pv_domain->mappings); |
| |
| arm_smccc_1_1_hvc(ARM_SMCCC_VENDOR_HYP_KVM_IOMMU_ALLOC_DOMAIN_FUNC_ID, &res); |
| |
| if (res.a0 != SMCCC_RET_SUCCESS) { |
| kfree(pv_domain); |
| return NULL; |
| } |
| |
| pv_domain->id = res.a1; |
| |
| return &pv_domain->domain; |
| } |
| |
| static struct platform_driver pkvm_pviommu_driver; |
| |
| static struct pviommu *pviommu_get_by_fwnode(struct fwnode_handle *fwnode) |
| { |
| struct device *dev = driver_find_device_by_fwnode(&pkvm_pviommu_driver.driver, fwnode); |
| |
| put_device(dev); |
| return dev ? dev_get_drvdata(dev) : NULL; |
| } |
| |
| static struct iommu_ops pviommu_ops; |
| |
| static struct iommu_device *pviommu_probe_device(struct device *dev) |
| { |
| struct pviommu_master *master; |
| struct pviommu *pv = NULL; |
| struct iommu_fwspec *fwspec = dev_iommu_fwspec_get(dev); |
| |
| if (!fwspec || fwspec->ops != &pviommu_ops) |
| return ERR_PTR(-ENODEV); |
| |
| pv = pviommu_get_by_fwnode(fwspec->iommu_fwnode); |
| if (!pv) |
| return ERR_PTR(-ENODEV); |
| |
| master = kzalloc(sizeof(*master), GFP_KERNEL); |
| if (!master) |
| return ERR_PTR(-ENOMEM); |
| |
| master->dev = dev; |
| master->iommu = pv; |
| device_property_read_u32(dev, "pasid-num-bits", &master->ssid_bits); |
| dev_iommu_priv_set(dev, master); |
| |
| return &pv->iommu; |
| } |
| |
| static void pviommu_release_device(struct device *dev) |
| { |
| pviommu_detach_dev(dev); |
| } |
| |
| static int pviommu_of_xlate(struct device *dev, struct of_phandle_args *args) |
| { |
| return iommu_fwspec_add_ids(dev, args->args, 1); |
| } |
| |
| static struct iommu_group *pviommu_device_group(struct device *dev) |
| { |
| if (dev_is_pci(dev)) |
| return pci_device_group(dev); |
| else |
| return generic_device_group(dev); |
| } |
| |
| static struct iommu_ops pviommu_ops = { |
| .device_group = pviommu_device_group, |
| .of_xlate = pviommu_of_xlate, |
| .probe_device = pviommu_probe_device, |
| .release_device = pviommu_release_device, |
| .domain_alloc = pviommu_domain_alloc, |
| .remove_dev_pasid = pviommu_remove_dev_pasid, |
| .owner = THIS_MODULE, |
| .default_domain_ops = &(const struct iommu_domain_ops) { |
| .attach_dev = pviommu_attach_dev, |
| .map_pages = pviommu_map_pages, |
| .unmap_pages = pviommu_unmap_pages, |
| .iova_to_phys = pviommu_iova_to_phys, |
| .set_dev_pasid = pviommu_set_dev_pasid, |
| .free = pviommu_domain_free, |
| } |
| }; |
| |
| static int pviommu_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct pviommu *pv = devm_kmalloc(dev, sizeof(*pv), GFP_KERNEL); |
| struct device_node *np = pdev->dev.of_node; |
| int ret; |
| struct arm_smccc_res res; |
| u64 version; |
| |
| ret = of_property_read_u32_index(np, "id", 0, &pv->id); |
| if (ret) { |
| dev_err(dev, "Failed to read id from device tree node %d\n", ret); |
| return ret; |
| } |
| |
| arm_smccc_1_1_hvc(ARM_SMCCC_VENDOR_HYP_KVM_IOMMU_VERSION_FUNC_ID, &res); |
| if (res.a0 != SMCCC_RET_SUCCESS) |
| return -ENODEV; |
| version = res.a1; |
| if (version != DRIVER_VERSION) |
| pr_warn("pviommu driver expects version %llx but found %llx\n", |
| DRIVER_VERSION, version); |
| |
| arm_smccc_1_1_hvc(ARM_SMCCC_VENDOR_HYP_KVM_IOMMU_GET_FEATURE_FUNC_ID, |
| pv->id, FEAUTRE_PGSIZE_BITMAP, &res); |
| if (res.a0 != SMCCC_RET_SUCCESS) |
| return -ENODEV; |
| |
| pv->pgsize_bitmap = pviommu_ops.pgsize_bitmap = res.a1; |
| |
| ret = iommu_device_sysfs_add(&pv->iommu, dev, NULL, |
| "pviommu.%pa", &pv->id); |
| |
| ret = iommu_device_register(&pv->iommu, &pviommu_ops, dev); |
| if (ret) { |
| dev_err(dev, "Couldn't register %d\n", ret); |
| iommu_device_sysfs_remove(&pv->iommu); |
| } |
| |
| platform_set_drvdata(pdev, pv); |
| |
| return ret; |
| } |
| |
| static const struct of_device_id pviommu_of_match[] = { |
| { .compatible = "pkvm,pviommu", }, |
| { }, |
| }; |
| |
| static struct platform_driver pkvm_pviommu_driver = { |
| .probe = pviommu_probe, |
| .driver = { |
| .name = "pkvm-pviommu", |
| .of_match_table = pviommu_of_match, |
| }, |
| }; |
| |
| #if IS_ENABLED(CONFIG_PKVM_PVIOMMU_SELFTEST) |
| /* Mainly test iova_to_phys and not hypervisor interface. */ |
| int __init __pviommu_selftest(void) |
| { |
| struct pviommu_domain domain; |
| |
| pr_info("pviommu selftest starting\n"); |
| |
| mt_init(&domain.mappings); |
| |
| pviommu_domain_insert_map(&domain, 0x10000 , 0xFEFFF, 0xE0000); |
| pviommu_domain_insert_map(&domain, 0xFFF0000, 0x1EDBFFFF, 0xDEAD0000); |
| ASSERT(pviommu_domain_find(&domain, 0x10000) == 0xE0000); |
| ASSERT(pviommu_domain_find(&domain, 0x10F00) == 0xE0F00); |
| ASSERT(pviommu_domain_find(&domain, 0x1EDBFFFF) == 0xED89FFFF); |
| ASSERT(pviommu_domain_find(&domain, 0x10000000) == 0xDEAE0000); |
| ASSERT(pviommu_domain_find(&domain, 0x1FF000) == 0); |
| pviommu_domain_remove_map(&domain, 0x12000, 0x19FFF); |
| ASSERT(pviommu_domain_find(&domain, 0x11000) == 0xE1000); |
| ASSERT(pviommu_domain_find(&domain, 0x1B000) == 0xEB000); |
| ASSERT(pviommu_domain_find(&domain, 0x14000) == 0); |
| |
| pviommu_domain_insert_map(&domain, 0xC00000 , 0xCFFFFF, 0xABCD000); |
| pviommu_domain_insert_map(&domain, 0xD00000 , 0xDFFFFF, 0x1000); |
| pviommu_domain_insert_map(&domain, 0xE00000, 0xEFFFFF, 0xC0FE00000); |
| ASSERT(pviommu_domain_find(&domain, 0xD00000) == 0x1000); |
| pviommu_domain_remove_map(&domain, 0xC50000, 0xE5FFFF); |
| ASSERT(pviommu_domain_find(&domain, 0xC50000) == 0); |
| ASSERT(pviommu_domain_find(&domain, 0xD10000) == 0); |
| ASSERT(pviommu_domain_find(&domain, 0xE60000) == 0xC0FE60000); |
| ASSERT(pviommu_domain_find(&domain, 0xC10000) == 0xABDD000); |
| |
| mtree_destroy(&domain.mappings); |
| return 0; |
| } |
| |
| subsys_initcall(__pviommu_selftest); |
| #endif |
| |
| module_platform_driver(pkvm_pviommu_driver); |
| |
| MODULE_DESCRIPTION("IOMMU API for pKVM paravirtualized IOMMU"); |
| MODULE_AUTHOR("Mostafa Saleh <smostafa@google.com>"); |
| MODULE_LICENSE("GPL v2"); |