| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (C) 2022-2023, Advanced Micro Devices, Inc. |
| */ |
| |
| #include <linux/vfio.h> |
| #include <linux/cdx/cdx_bus.h> |
| |
| #include "private.h" |
| |
| static int vfio_cdx_open_device(struct vfio_device *core_vdev) |
| { |
| struct vfio_cdx_device *vdev = |
| container_of(core_vdev, struct vfio_cdx_device, vdev); |
| struct cdx_device *cdx_dev = to_cdx_device(core_vdev->dev); |
| int count = cdx_dev->res_count; |
| int i, ret; |
| |
| vdev->regions = kcalloc(count, sizeof(struct vfio_cdx_region), |
| GFP_KERNEL_ACCOUNT); |
| if (!vdev->regions) |
| return -ENOMEM; |
| |
| for (i = 0; i < count; i++) { |
| struct resource *res = &cdx_dev->res[i]; |
| |
| vdev->regions[i].addr = res->start; |
| vdev->regions[i].size = resource_size(res); |
| vdev->regions[i].type = res->flags; |
| /* |
| * Only regions addressed with PAGE granularity may be |
| * MMAP'ed securely. |
| */ |
| if (!(vdev->regions[i].addr & ~PAGE_MASK) && |
| !(vdev->regions[i].size & ~PAGE_MASK)) |
| vdev->regions[i].flags |= |
| VFIO_REGION_INFO_FLAG_MMAP; |
| vdev->regions[i].flags |= VFIO_REGION_INFO_FLAG_READ; |
| if (!(cdx_dev->res[i].flags & IORESOURCE_READONLY)) |
| vdev->regions[i].flags |= VFIO_REGION_INFO_FLAG_WRITE; |
| } |
| ret = cdx_dev_reset(core_vdev->dev); |
| if (ret) { |
| kfree(vdev->regions); |
| vdev->regions = NULL; |
| return ret; |
| } |
| ret = cdx_clear_master(cdx_dev); |
| if (ret) |
| vdev->flags &= ~BME_SUPPORT; |
| else |
| vdev->flags |= BME_SUPPORT; |
| |
| return 0; |
| } |
| |
| static void vfio_cdx_close_device(struct vfio_device *core_vdev) |
| { |
| struct vfio_cdx_device *vdev = |
| container_of(core_vdev, struct vfio_cdx_device, vdev); |
| |
| kfree(vdev->regions); |
| cdx_dev_reset(core_vdev->dev); |
| vfio_cdx_irqs_cleanup(vdev); |
| } |
| |
| static int vfio_cdx_bm_ctrl(struct vfio_device *core_vdev, u32 flags, |
| void __user *arg, size_t argsz) |
| { |
| size_t minsz = |
| offsetofend(struct vfio_device_feature_bus_master, op); |
| struct vfio_cdx_device *vdev = |
| container_of(core_vdev, struct vfio_cdx_device, vdev); |
| struct cdx_device *cdx_dev = to_cdx_device(core_vdev->dev); |
| struct vfio_device_feature_bus_master ops; |
| int ret; |
| |
| if (!(vdev->flags & BME_SUPPORT)) |
| return -ENOTTY; |
| |
| ret = vfio_check_feature(flags, argsz, VFIO_DEVICE_FEATURE_SET, |
| sizeof(ops)); |
| if (ret != 1) |
| return ret; |
| |
| if (copy_from_user(&ops, arg, minsz)) |
| return -EFAULT; |
| |
| switch (ops.op) { |
| case VFIO_DEVICE_FEATURE_CLEAR_MASTER: |
| return cdx_clear_master(cdx_dev); |
| case VFIO_DEVICE_FEATURE_SET_MASTER: |
| return cdx_set_master(cdx_dev); |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| static int vfio_cdx_ioctl_feature(struct vfio_device *device, u32 flags, |
| void __user *arg, size_t argsz) |
| { |
| switch (flags & VFIO_DEVICE_FEATURE_MASK) { |
| case VFIO_DEVICE_FEATURE_BUS_MASTER: |
| return vfio_cdx_bm_ctrl(device, flags, arg, argsz); |
| default: |
| return -ENOTTY; |
| } |
| } |
| |
| static int vfio_cdx_ioctl_get_info(struct vfio_cdx_device *vdev, |
| struct vfio_device_info __user *arg) |
| { |
| unsigned long minsz = offsetofend(struct vfio_device_info, num_irqs); |
| struct cdx_device *cdx_dev = to_cdx_device(vdev->vdev.dev); |
| struct vfio_device_info info; |
| |
| if (copy_from_user(&info, arg, minsz)) |
| return -EFAULT; |
| |
| if (info.argsz < minsz) |
| return -EINVAL; |
| |
| info.flags = VFIO_DEVICE_FLAGS_CDX; |
| info.flags |= VFIO_DEVICE_FLAGS_RESET; |
| |
| info.num_regions = cdx_dev->res_count; |
| info.num_irqs = cdx_dev->num_msi ? 1 : 0; |
| |
| return copy_to_user(arg, &info, minsz) ? -EFAULT : 0; |
| } |
| |
| static int vfio_cdx_ioctl_get_region_info(struct vfio_cdx_device *vdev, |
| struct vfio_region_info __user *arg) |
| { |
| unsigned long minsz = offsetofend(struct vfio_region_info, offset); |
| struct cdx_device *cdx_dev = to_cdx_device(vdev->vdev.dev); |
| struct vfio_region_info info; |
| |
| if (copy_from_user(&info, arg, minsz)) |
| return -EFAULT; |
| |
| if (info.argsz < minsz) |
| return -EINVAL; |
| |
| if (info.index >= cdx_dev->res_count) |
| return -EINVAL; |
| |
| /* map offset to the physical address */ |
| info.offset = vfio_cdx_index_to_offset(info.index); |
| info.size = vdev->regions[info.index].size; |
| info.flags = vdev->regions[info.index].flags; |
| |
| return copy_to_user(arg, &info, minsz) ? -EFAULT : 0; |
| } |
| |
| static int vfio_cdx_ioctl_get_irq_info(struct vfio_cdx_device *vdev, |
| struct vfio_irq_info __user *arg) |
| { |
| unsigned long minsz = offsetofend(struct vfio_irq_info, count); |
| struct cdx_device *cdx_dev = to_cdx_device(vdev->vdev.dev); |
| struct vfio_irq_info info; |
| |
| if (copy_from_user(&info, arg, minsz)) |
| return -EFAULT; |
| |
| if (info.argsz < minsz) |
| return -EINVAL; |
| |
| if (info.index >= 1) |
| return -EINVAL; |
| |
| if (!cdx_dev->num_msi) |
| return -EINVAL; |
| |
| info.flags = VFIO_IRQ_INFO_EVENTFD | VFIO_IRQ_INFO_NORESIZE; |
| info.count = cdx_dev->num_msi; |
| |
| return copy_to_user(arg, &info, minsz) ? -EFAULT : 0; |
| } |
| |
| static int vfio_cdx_ioctl_set_irqs(struct vfio_cdx_device *vdev, |
| struct vfio_irq_set __user *arg) |
| { |
| unsigned long minsz = offsetofend(struct vfio_irq_set, count); |
| struct cdx_device *cdx_dev = to_cdx_device(vdev->vdev.dev); |
| struct vfio_irq_set hdr; |
| size_t data_size = 0; |
| u8 *data = NULL; |
| int ret = 0; |
| |
| if (copy_from_user(&hdr, arg, minsz)) |
| return -EFAULT; |
| |
| ret = vfio_set_irqs_validate_and_prepare(&hdr, cdx_dev->num_msi, |
| 1, &data_size); |
| if (ret) |
| return ret; |
| |
| if (data_size) { |
| data = memdup_user(arg->data, data_size); |
| if (IS_ERR(data)) |
| return PTR_ERR(data); |
| } |
| |
| ret = vfio_cdx_set_irqs_ioctl(vdev, hdr.flags, hdr.index, |
| hdr.start, hdr.count, data); |
| kfree(data); |
| |
| return ret; |
| } |
| |
| static long vfio_cdx_ioctl(struct vfio_device *core_vdev, |
| unsigned int cmd, unsigned long arg) |
| { |
| struct vfio_cdx_device *vdev = |
| container_of(core_vdev, struct vfio_cdx_device, vdev); |
| void __user *uarg = (void __user *)arg; |
| |
| switch (cmd) { |
| case VFIO_DEVICE_GET_INFO: |
| return vfio_cdx_ioctl_get_info(vdev, uarg); |
| case VFIO_DEVICE_GET_REGION_INFO: |
| return vfio_cdx_ioctl_get_region_info(vdev, uarg); |
| case VFIO_DEVICE_GET_IRQ_INFO: |
| return vfio_cdx_ioctl_get_irq_info(vdev, uarg); |
| case VFIO_DEVICE_SET_IRQS: |
| return vfio_cdx_ioctl_set_irqs(vdev, uarg); |
| case VFIO_DEVICE_RESET: |
| return cdx_dev_reset(core_vdev->dev); |
| default: |
| return -ENOTTY; |
| } |
| } |
| |
| static int vfio_cdx_mmap_mmio(struct vfio_cdx_region region, |
| struct vm_area_struct *vma) |
| { |
| u64 size = vma->vm_end - vma->vm_start; |
| u64 pgoff, base; |
| |
| pgoff = vma->vm_pgoff & |
| ((1U << (VFIO_CDX_OFFSET_SHIFT - PAGE_SHIFT)) - 1); |
| base = pgoff << PAGE_SHIFT; |
| |
| if (base + size > region.size) |
| return -EINVAL; |
| |
| vma->vm_pgoff = (region.addr >> PAGE_SHIFT) + pgoff; |
| vma->vm_page_prot = pgprot_device(vma->vm_page_prot); |
| |
| return io_remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, |
| size, vma->vm_page_prot); |
| } |
| |
| static int vfio_cdx_mmap(struct vfio_device *core_vdev, |
| struct vm_area_struct *vma) |
| { |
| struct vfio_cdx_device *vdev = |
| container_of(core_vdev, struct vfio_cdx_device, vdev); |
| struct cdx_device *cdx_dev = to_cdx_device(core_vdev->dev); |
| unsigned int index; |
| |
| index = vma->vm_pgoff >> (VFIO_CDX_OFFSET_SHIFT - PAGE_SHIFT); |
| |
| if (index >= cdx_dev->res_count) |
| return -EINVAL; |
| |
| if (!(vdev->regions[index].flags & VFIO_REGION_INFO_FLAG_MMAP)) |
| return -EINVAL; |
| |
| if (!(vdev->regions[index].flags & VFIO_REGION_INFO_FLAG_READ) && |
| (vma->vm_flags & VM_READ)) |
| return -EPERM; |
| |
| if (!(vdev->regions[index].flags & VFIO_REGION_INFO_FLAG_WRITE) && |
| (vma->vm_flags & VM_WRITE)) |
| return -EPERM; |
| |
| return vfio_cdx_mmap_mmio(vdev->regions[index], vma); |
| } |
| |
| static const struct vfio_device_ops vfio_cdx_ops = { |
| .name = "vfio-cdx", |
| .open_device = vfio_cdx_open_device, |
| .close_device = vfio_cdx_close_device, |
| .ioctl = vfio_cdx_ioctl, |
| .device_feature = vfio_cdx_ioctl_feature, |
| .mmap = vfio_cdx_mmap, |
| .bind_iommufd = vfio_iommufd_physical_bind, |
| .unbind_iommufd = vfio_iommufd_physical_unbind, |
| .attach_ioas = vfio_iommufd_physical_attach_ioas, |
| }; |
| |
| static int vfio_cdx_probe(struct cdx_device *cdx_dev) |
| { |
| struct vfio_cdx_device *vdev; |
| struct device *dev = &cdx_dev->dev; |
| int ret; |
| |
| vdev = vfio_alloc_device(vfio_cdx_device, vdev, dev, |
| &vfio_cdx_ops); |
| if (IS_ERR(vdev)) |
| return PTR_ERR(vdev); |
| |
| ret = vfio_register_group_dev(&vdev->vdev); |
| if (ret) |
| goto out_uninit; |
| |
| dev_set_drvdata(dev, vdev); |
| return 0; |
| |
| out_uninit: |
| vfio_put_device(&vdev->vdev); |
| return ret; |
| } |
| |
| static int vfio_cdx_remove(struct cdx_device *cdx_dev) |
| { |
| struct device *dev = &cdx_dev->dev; |
| struct vfio_cdx_device *vdev = dev_get_drvdata(dev); |
| |
| vfio_unregister_group_dev(&vdev->vdev); |
| vfio_put_device(&vdev->vdev); |
| |
| return 0; |
| } |
| |
| static const struct cdx_device_id vfio_cdx_table[] = { |
| { CDX_DEVICE_DRIVER_OVERRIDE(CDX_ANY_ID, CDX_ANY_ID, |
| CDX_ID_F_VFIO_DRIVER_OVERRIDE) }, /* match all by default */ |
| {} |
| }; |
| |
| MODULE_DEVICE_TABLE(cdx, vfio_cdx_table); |
| |
| static struct cdx_driver vfio_cdx_driver = { |
| .probe = vfio_cdx_probe, |
| .remove = vfio_cdx_remove, |
| .match_id_table = vfio_cdx_table, |
| .driver = { |
| .name = "vfio-cdx", |
| }, |
| .driver_managed_dma = true, |
| }; |
| |
| module_driver(vfio_cdx_driver, cdx_driver_register, cdx_driver_unregister); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_DESCRIPTION("VFIO for CDX devices - User Level meta-driver"); |
| MODULE_IMPORT_NS("CDX_BUS"); |