root/drivers/vfio/pci/ism/main.c
// SPDX-License-Identifier: GPL-2.0
/*
 * vfio-ISM driver for s390
 *
 * Copyright IBM Corp.
 */

#include <linux/slab.h>
#include "../vfio_pci_priv.h"

#define ISM_VFIO_PCI_OFFSET_SHIFT   48
#define ISM_VFIO_PCI_OFFSET_TO_INDEX(off) ((off) >> ISM_VFIO_PCI_OFFSET_SHIFT)
#define ISM_VFIO_PCI_INDEX_TO_OFFSET(index) ((u64)(index) << ISM_VFIO_PCI_OFFSET_SHIFT)
#define ISM_VFIO_PCI_OFFSET_MASK (((u64)(1) << ISM_VFIO_PCI_OFFSET_SHIFT) - 1)

/*
 * Use __zpci_load() to bypass automatic use of
 * PCI MIO instructions which are not supported on ISM devices
 */
#define ISM_READ(size)                                                        \
        static int ism_read##size(struct zpci_dev *zdev, int bar,             \
                                  size_t *filled, char __user *buf,           \
                                  loff_t off)                                 \
        {                                                                     \
                u64 req, tmp;                                                 \
                u##size val;                                                  \
                int ret;                                                      \
                                                                              \
                req = ZPCI_CREATE_REQ(READ_ONCE(zdev->fh), bar, sizeof(val)); \
                ret = __zpci_load(&tmp, req, off);                            \
                if (ret)                                                      \
                        return ret;                                           \
                val = (u##size)tmp;                                           \
                if (copy_to_user(buf, &val, sizeof(val)))                     \
                        return -EFAULT;                                       \
                *filled = sizeof(val);                                        \
                return 0;                                                     \
        }

ISM_READ(64);
ISM_READ(32);
ISM_READ(16);
ISM_READ(8);

struct ism_vfio_pci_core_device {
        struct vfio_pci_core_device core_device;
        struct kmem_cache *store_block_cache;
};

static int ism_vfio_pci_open_device(struct vfio_device *core_vdev)
{
        struct ism_vfio_pci_core_device *ivpcd;
        struct vfio_pci_core_device *vdev;
        int ret;

        ivpcd = container_of(core_vdev, struct ism_vfio_pci_core_device,
                             core_device.vdev);
        vdev = &ivpcd->core_device;

        ret = vfio_pci_core_enable(vdev);
        if (ret)
                return ret;

        vfio_pci_core_finish_enable(vdev);
        return 0;
}

/*
 * ism_vfio_pci_do_io_r()
 *
 * On s390, kernel primitives such as ioread() and iowrite() are switched over
 * from function-handle-based PCI load/stores instructions to PCI memory-I/O (MIO)
 * loads/stores when these are available and not explicitly disabled. Since these
 * instructions cannot be used with ISM devices, ensure that classic
 * function-handle-based PCI instructions are used instead.
 */
static ssize_t ism_vfio_pci_do_io_r(struct vfio_pci_core_device *vdev,
                                    char __user *buf, loff_t off, size_t count,
                                    int bar)
{
        struct zpci_dev *zdev = to_zpci(vdev->pdev);
        ssize_t done = 0;
        int ret;

        while (count) {
                size_t filled;

                if (count >= 8 && IS_ALIGNED(off, 8)) {
                        ret = ism_read64(zdev, bar, &filled, buf, off);
                        if (ret)
                                return ret;
                } else if (count >= 4 && IS_ALIGNED(off, 4)) {
                        ret = ism_read32(zdev, bar, &filled, buf, off);
                        if (ret)
                                return ret;
                } else if (count >= 2 && IS_ALIGNED(off, 2)) {
                        ret = ism_read16(zdev, bar, &filled, buf, off);
                        if (ret)
                                return ret;
                } else {
                        ret = ism_read8(zdev, bar, &filled, buf, off);
                        if (ret)
                                return ret;
                }

                count -= filled;
                done += filled;
                off += filled;
                buf += filled;
        }

        return done;
}

/*
 * ism_vfio_pci_do_io_w()
 *
 * Ensure that the PCI store block (PCISTB) instruction is used as required by the
 * ISM device. The ISM device also uses a 256 TiB BAR 0 for write operations,
 * which requires a 48bit region address space (ISM_VFIO_PCI_OFFSET_SHIFT).
 */
static ssize_t ism_vfio_pci_do_io_w(struct vfio_pci_core_device *vdev,
                                    char __user *buf, loff_t off, size_t count,
                                    int bar)
{
        struct zpci_dev *zdev = to_zpci(vdev->pdev);
        struct ism_vfio_pci_core_device *ivpcd;
        ssize_t ret;
        void *data;
        u64 req;

        if (count > zdev->maxstbl)
                return -EINVAL;
        if (((off % PAGE_SIZE) + count) > PAGE_SIZE)
                return -EINVAL;

        ivpcd = container_of(vdev, struct ism_vfio_pci_core_device,
                             core_device);
        data = kmem_cache_alloc(ivpcd->store_block_cache, GFP_KERNEL);
        if (!data)
                return -ENOMEM;

        if (copy_from_user(data, buf, count)) {
                ret = -EFAULT;
                goto out_free;
        }

        req = ZPCI_CREATE_REQ(READ_ONCE(zdev->fh), bar, count);
        ret = __zpci_store_block(data, req, off);
        if (ret)
                goto out_free;

        ret = count;

out_free:
        kmem_cache_free(ivpcd->store_block_cache, data);
        return ret;
}

static ssize_t ism_vfio_pci_bar_rw(struct vfio_pci_core_device *vdev,
                                   char __user *buf, size_t count, loff_t *ppos,
                                   bool iswrite)
{
        int bar = ISM_VFIO_PCI_OFFSET_TO_INDEX(*ppos);
        loff_t pos = *ppos & ISM_VFIO_PCI_OFFSET_MASK;
        resource_size_t end;
        ssize_t done = 0;

        if (pci_resource_start(vdev->pdev, bar))
                end = pci_resource_len(vdev->pdev, bar);
        else
                return -EINVAL;

        if (pos >= end)
                return -EINVAL;

        count = min(count, (size_t)(end - pos));

        if (iswrite)
                done = ism_vfio_pci_do_io_w(vdev, buf, pos, count, bar);
        else
                done = ism_vfio_pci_do_io_r(vdev, buf, pos, count, bar);

        if (done >= 0)
                *ppos += done;

        return done;
}

static ssize_t ism_vfio_pci_config_rw(struct vfio_pci_core_device *vdev,
                                      char __user *buf, size_t count,
                                      loff_t *ppos, bool iswrite)
{
        loff_t pos = *ppos;
        size_t done = 0;
        int ret = 0;

        pos &= ISM_VFIO_PCI_OFFSET_MASK;

        while (count) {
                /*
                 * zPCI must not use MIO instructions for config space access,
                 * so we can use common code path here.
                 */
                ret = vfio_pci_config_rw_single(vdev, buf, count, &pos, iswrite);
                if (ret < 0)
                        return ret;

                count -= ret;
                done += ret;
                buf += ret;
                pos += ret;
        }

        *ppos += done;

        return done;
}

static ssize_t ism_vfio_pci_rw(struct vfio_device *core_vdev, char __user *buf,
                               size_t count, loff_t *ppos, bool iswrite)
{
        unsigned int index = ISM_VFIO_PCI_OFFSET_TO_INDEX(*ppos);
        struct vfio_pci_core_device *vdev;
        int ret;

        vdev = container_of(core_vdev, struct vfio_pci_core_device, vdev);

        if (!count)
                return 0;

        switch (index) {
        case VFIO_PCI_CONFIG_REGION_INDEX:
                ret = ism_vfio_pci_config_rw(vdev, buf, count, ppos, iswrite);
                break;

        case VFIO_PCI_BAR0_REGION_INDEX ... VFIO_PCI_BAR5_REGION_INDEX:
                ret = ism_vfio_pci_bar_rw(vdev, buf, count, ppos, iswrite);
                break;

        default:
                return -EINVAL;
        }

        return ret;
}

static ssize_t ism_vfio_pci_read(struct vfio_device *core_vdev,
                                 char __user *buf, size_t count, loff_t *ppos)
{
        return ism_vfio_pci_rw(core_vdev, buf, count, ppos, false);
}

static ssize_t ism_vfio_pci_write(struct vfio_device *core_vdev,
                                  const char __user *buf, size_t count,
                                  loff_t *ppos)
{
        return ism_vfio_pci_rw(core_vdev, (char __user *)buf, count, ppos,
                               true);
}

static int ism_vfio_pci_ioctl_get_region_info(struct vfio_device *core_vdev,
                                              struct vfio_region_info *info,
                                              struct vfio_info_cap *caps)
{
        struct vfio_pci_core_device *vdev =
                container_of(core_vdev, struct vfio_pci_core_device, vdev);
        struct pci_dev *pdev = vdev->pdev;

        switch (info->index) {
        case VFIO_PCI_CONFIG_REGION_INDEX:
                info->offset = ISM_VFIO_PCI_INDEX_TO_OFFSET(info->index);
                info->size = pdev->cfg_size;
                info->flags = VFIO_REGION_INFO_FLAG_READ |
                              VFIO_REGION_INFO_FLAG_WRITE;
                break;
        case VFIO_PCI_BAR0_REGION_INDEX ... VFIO_PCI_BAR5_REGION_INDEX:
                info->offset = ISM_VFIO_PCI_INDEX_TO_OFFSET(info->index);
                info->size = pci_resource_len(pdev, info->index);
                if (!info->size) {
                        info->flags = 0;
                        break;
                }
                info->flags = VFIO_REGION_INFO_FLAG_READ |
                              VFIO_REGION_INFO_FLAG_WRITE;
                break;
        default:
                info->offset = 0;
                info->size = 0;
                info->flags = 0;
                return -EINVAL;
        }
        return 0;
}

static int ism_vfio_pci_init_dev(struct vfio_device *core_vdev)
{
        struct zpci_dev *zdev = to_zpci(to_pci_dev(core_vdev->dev));
        struct ism_vfio_pci_core_device *ivpcd;
        char cache_name[20];
        int ret;

        ivpcd = container_of(core_vdev, struct ism_vfio_pci_core_device,
                             core_device.vdev);

        snprintf(cache_name, sizeof(cache_name), "ism_sb_fid_%08x", zdev->fid);

        ivpcd->store_block_cache =
                kmem_cache_create(cache_name, zdev->maxstbl,
                                  (&(struct kmem_cache_args){
                                          .align = PAGE_SIZE,
                                          .useroffset = 0,
                                          .usersize = zdev->maxstbl,
                                  }),
                                  (SLAB_RECLAIM_ACCOUNT | SLAB_ACCOUNT));
        if (!ivpcd->store_block_cache)
                return -ENOMEM;

        ret = vfio_pci_core_init_dev(core_vdev);
        if (ret)
                kmem_cache_destroy(ivpcd->store_block_cache);

        return ret;
}

static void ism_vfio_pci_release_dev(struct vfio_device *core_vdev)
{
        struct ism_vfio_pci_core_device *ivpcd = container_of(
                core_vdev, struct ism_vfio_pci_core_device, core_device.vdev);

        kmem_cache_destroy(ivpcd->store_block_cache);
        vfio_pci_core_release_dev(core_vdev);
}

static const struct vfio_device_ops ism_pci_ops = {
        .name = "ism-vfio-pci",
        .init = ism_vfio_pci_init_dev,
        .release = ism_vfio_pci_release_dev,
        .open_device = ism_vfio_pci_open_device,
        .close_device = vfio_pci_core_close_device,
        .ioctl = vfio_pci_core_ioctl,
        .get_region_info_caps = ism_vfio_pci_ioctl_get_region_info,
        .device_feature = vfio_pci_core_ioctl_feature,
        .read = ism_vfio_pci_read,
        .write = ism_vfio_pci_write,
        .request = vfio_pci_core_request,
        .match = vfio_pci_core_match,
        .match_token_uuid = vfio_pci_core_match_token_uuid,
        .bind_iommufd = vfio_iommufd_physical_bind,
        .unbind_iommufd = vfio_iommufd_physical_unbind,
        .attach_ioas = vfio_iommufd_physical_attach_ioas,
        .detach_ioas = vfio_iommufd_physical_detach_ioas,
};

static int ism_vfio_pci_probe(struct pci_dev *pdev,
                              const struct pci_device_id *id)
{
        struct ism_vfio_pci_core_device *ivpcd;
        int ret;

        ivpcd = vfio_alloc_device(ism_vfio_pci_core_device, core_device.vdev,
                                  &pdev->dev, &ism_pci_ops);
        if (IS_ERR(ivpcd))
                return PTR_ERR(ivpcd);

        dev_set_drvdata(&pdev->dev, &ivpcd->core_device);

        ret = vfio_pci_core_register_device(&ivpcd->core_device);
        if (ret)
                vfio_put_device(&ivpcd->core_device.vdev);

        return ret;
}

static void ism_vfio_pci_remove(struct pci_dev *pdev)
{
        struct vfio_pci_core_device *core_device;
        struct ism_vfio_pci_core_device *ivpcd;

        core_device = dev_get_drvdata(&pdev->dev);
        ivpcd = container_of(core_device, struct ism_vfio_pci_core_device,
                             core_device);

        vfio_pci_core_unregister_device(&ivpcd->core_device);
        vfio_put_device(&ivpcd->core_device.vdev);
}

static const struct pci_device_id ism_device_table[] = {
        { PCI_DRIVER_OVERRIDE_DEVICE_VFIO(PCI_VENDOR_ID_IBM,
                                          PCI_DEVICE_ID_IBM_ISM) },
        {}
};
MODULE_DEVICE_TABLE(pci, ism_device_table);

static struct pci_driver ism_vfio_pci_driver = {
        .name = KBUILD_MODNAME,
        .id_table = ism_device_table,
        .probe = ism_vfio_pci_probe,
        .remove = ism_vfio_pci_remove,
        .err_handler = &vfio_pci_core_err_handlers,
        .driver_managed_dma = true,
};

module_pci_driver(ism_vfio_pci_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("vfio-pci variant driver for the IBM Internal Shared Memory (ISM) device");
MODULE_AUTHOR("IBM Corporation");