From 8ceb0ea6d889c6fa982ac33c63fce4d4ca8e6330 Mon Sep 17 00:00:00 2001 From: Carl van Schaik Date: Tue, 12 Jun 2018 01:46:33 +1000 Subject: [PATCH] drivers misc: add okl4-link-shbuf driver A simple driver for using a shared-memory bridge between two VMs. Change-Id: I3dac6dd88d1f658c95e799262a9720a44af00291 Signed-off-by: Carl van Schaik Git-commit: 18b0192bb16d90974af0401f1a46f20f0f5b9502 Git-repo: https://github.com/CogSystems/linux-msm/commits/msm-4.9-hyp [mnalajal@codeaurora: Resolve trivial merge conflicts] Signed-off-by: Murali Nalajala Signed-off-by: Prakruthi Deepak Heragu --- drivers/misc/Kconfig | 10 + drivers/misc/Makefile | 1 + drivers/misc/okl4-link-shbuf.c | 666 +++++++++++++++++++++++++++ include/uapi/linux/Kbuild | 1 + include/uapi/linux/okl4-link-shbuf.h | 40 ++ 5 files changed, 718 insertions(+) create mode 100644 drivers/misc/okl4-link-shbuf.c create mode 100644 include/uapi/linux/okl4-link-shbuf.h diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig index 3242af01f8fa..a605eab809ad 100644 --- a/drivers/misc/Kconfig +++ b/drivers/misc/Kconfig @@ -549,3 +549,13 @@ source "drivers/misc/cxl/Kconfig" source "drivers/misc/ocxl/Kconfig" source "drivers/misc/cardreader/Kconfig" endmenu + +config OKL4_LINK_SHBUF + tristate "OKL4 link with shared buffer transport" + default y + depends on OKL4_GUEST + help + Enable driver for OKL4 inter-cell links using the "shared-buffer" + transport. This driver presents the link to Linux as a character device + which can be written to or read from to access the shared memory. An ioctl + on the device is used to send a virtual interrupt to the partner cell. diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile index f4d0fd9afcb8..2c2357bc0c75 100644 --- a/drivers/misc/Makefile +++ b/drivers/misc/Makefile @@ -60,3 +60,4 @@ obj-$(CONFIG_OCXL) += ocxl/ obj-$(CONFIG_MISC_RTSX) += cardreader/ obj-$(CONFIG_UID_SYS_STATS) += uid_sys_stats.o obj-$(CONFIG_MEMORY_STATE_TIME) += memory_state_time.o +obj-$(CONFIG_OKL4_LINK_SHBUF) += okl4-link-shbuf.o diff --git a/drivers/misc/okl4-link-shbuf.c b/drivers/misc/okl4-link-shbuf.c new file mode 100644 index 000000000000..88f13e3cb7b9 --- /dev/null +++ b/drivers/misc/okl4-link-shbuf.c @@ -0,0 +1,666 @@ +/* + * Driver for inter-cell links using the shared-buffer transport. + * + * Copyright (c) 2016 Cog Systems Pty Ltd. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published by + * the Free Software Foundation. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char DEVICE_NAME[] = "okl4_link_shbuf"; + +/* Created devices will appear as /dev/ */ +static const char DEV_PREFIX[] = "okl4-"; + +static const struct of_device_id okl4_link_shbuf_match[] = { + { + .compatible = "okl,microvisor-link-shbuf", + }, + {}, +}; +MODULE_DEVICE_TABLE(of, okl4_link_shbuf_match); + +static struct class *link_shbuf_class; +static dev_t link_shbuf_dev; + +/* A lock used to protect access to link_shbuf_dev */ +static spinlock_t device_number_allocate; + +/* Sentinel values for indicating missing communication channels */ +static const u32 NO_OUTGOING_IRQ = 0; +static const int NO_INCOMING_IRQ = -1; + +/* Private data for this driver */ +struct link_shbuf_data { + + /* Outgoing vIRQ */ + u32 virqline; + + /* Incoming vIRQ */ + int virq; + atomic64_t virq_payload; + bool virq_pending; + wait_queue_head_t virq_wq; + + /* Shared memory region */ + void *base; + fmode_t permissions; + struct resource buffer; + + /* Device data */ + dev_t devt; + struct device *dev; + struct cdev cdev; + +}; + +static bool link_shbuf_data_invariant(const struct link_shbuf_data *priv) +{ + if (!priv) + return false; + + if (!priv->base || (uintptr_t)priv->base % PAGE_SIZE != 0) + return false; + + if (resource_size(&priv->buffer) == 0) + return false; + + if (!priv->dev) + return false; + + return true; +} + +static bool link_shbuf_valid_access(size_t size, loff_t pos, size_t count) +{ + return pos < size && count <= size && size - count >= pos; +} + +static ssize_t link_shbuf_read(struct file *file, char __user *buffer, + size_t count, loff_t *ppos) +{ + long remaining; + const struct link_shbuf_data *priv; + + /* The file should have been opened with read access to reach here */ + if (WARN_ON(!(file->f_mode & FMODE_READ))) + return -EINVAL; + + priv = file->private_data; + if (WARN_ON(!link_shbuf_data_invariant(priv))) + return -EINVAL; + + if (!link_shbuf_valid_access(resource_size(&priv->buffer), *ppos, count)) + return -EINVAL; + + remaining = copy_to_user(buffer, priv->base + *ppos, count); + *ppos += count - remaining; + return count - remaining; +} + +static ssize_t link_shbuf_write(struct file *file, const char __user *buffer, + size_t count, loff_t *ppos) +{ + long remaining; + const struct link_shbuf_data *priv; + + /* The file should have been opened with write access to reach here */ + if (WARN_ON(!(file->f_mode & FMODE_WRITE))) + return -EINVAL; + + priv = file->private_data; + if (WARN_ON(!link_shbuf_data_invariant(priv))) + return -EINVAL; + + if (!link_shbuf_valid_access(resource_size(&priv->buffer), *ppos, count)) + return -EINVAL; + + remaining = copy_from_user(priv->base + *ppos, buffer, count); + *ppos += count - remaining; + return count - remaining; +} + +static unsigned int link_shbuf_poll(struct file *file, poll_table *table) +{ + struct link_shbuf_data *priv; + unsigned int mask; + + priv = file->private_data; + if (WARN_ON(!link_shbuf_data_invariant(priv))) + return POLLERR; + + poll_wait(file, &priv->virq_wq, table); + + /* The shared memory is always considered ready for reading and writing. */ + mask = POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM; + + if (priv->virq_pending) + mask |= POLLPRI; + + return mask; +} + +static long link_shbuf_ioctl_irq_tx(const struct link_shbuf_data *priv, + unsigned long arg) +{ + okl4_error_t err; + u64 payload; + const u64 __user *user_arg = (const u64 __user*)arg; + + if (priv->virqline == NO_OUTGOING_IRQ) + return -EINVAL; + +#if defined(CONFIG_ARM) && (LINUX_VERSION_CODE < KERNEL_VERSION(3, 17, 0)) + if (copy_from_user(&payload, user_arg, sizeof(payload))) + return -EFAULT; +#else + if (get_user(payload, user_arg)) + return -EFAULT; +#endif + + err = _okl4_sys_vinterrupt_raise(priv->virqline, payload); + if (WARN_ON(err != OKL4_OK)) + return -EINVAL; + + return 0; +} + +static long link_shbuf_ioctl_irq_clr(struct link_shbuf_data *priv, + unsigned long arg) +{ + u64 payload; + u64 __user *user_arg = (u64 __user*)arg; + + /* + * Check validity of the user pointer before clearing the interrupt to avoid + * races involved with having to undo the latter. + */ + if (!access_ok(VERIFY_WRITE, user_arg, sizeof(*user_arg))) + return -EFAULT; + + /* + * Note that the clearing of the pending flag can race with the setting of + * this flag in the IRQ handler. It is up to the user to coordinate these + * actions. + */ + priv->virq_pending = false; + smp_rmb(); + payload = atomic64_xchg(&priv->virq_payload, 0); + + /* We've already checked that this access is OK, so no need for put_user. */ + if (__put_user(payload, user_arg)) + return -EFAULT; + + return 0; +} + +static long link_shbuf_ioctl(struct file *file, unsigned int request, + unsigned long arg) +{ + struct link_shbuf_data *priv; + + priv = file->private_data; + if (WARN_ON(!link_shbuf_data_invariant(priv))) + return -EINVAL; + + /* We only support two ioctls */ + switch (request) { + + case OKL4_LINK_SHBUF_IOCTL_IRQ_TX: + return link_shbuf_ioctl_irq_tx(priv, arg); + + case OKL4_LINK_SHBUF_IOCTL_IRQ_CLR: + return link_shbuf_ioctl_irq_clr(priv, arg); + + } + + /* + * Handy for debugging when userspace is linking against ioctl headers from + * a different kernel revision. + */ + dev_dbg(priv->dev, "ioctl request 0x%x received which did not match either " + "OKL4_LINK_SHBUF_IOCTL_IRQ_TX (0x%x) or OKL4_LINK_SHBUF_IOCTL_IRQ_CLR " + "(0x%x)\n", request, (unsigned)OKL4_LINK_SHBUF_IOCTL_IRQ_TX, + (unsigned)OKL4_LINK_SHBUF_IOCTL_IRQ_CLR); + + return -EINVAL; +} + +static int link_shbuf_mmap(struct file *file, struct vm_area_struct *vma) +{ + const struct link_shbuf_data *priv; + unsigned long offset, pfn, flags; + size_t size; + pgprot_t prot; + + /* Our caller should have taken the MM semaphore. */ + if (WARN_ON(!rwsem_is_locked(&vma->vm_mm->mmap_sem))) + return -EINVAL; + + /* + * The file should have been opened with a superset of the mmap requested + * permissions. + */ + flags = vma->vm_flags; + if (WARN_ON((flags & VM_READ) && !(file->f_mode & FMODE_READ))) + return -EINVAL; + if (WARN_ON((flags & VM_WRITE) && !(file->f_mode & FMODE_WRITE))) + return -EINVAL; + if (WARN_ON((flags & VM_EXEC) && !(file->f_mode & FMODE_EXEC))) + return -EINVAL; + + /* Retrieve our private data. */ + priv = file->private_data; + if (WARN_ON(!link_shbuf_data_invariant(priv))) + return -EINVAL; + + /* Check the mmap request is within bounds. */ + size = vma->vm_end - vma->vm_start; + offset = vma->vm_pgoff << PAGE_SHIFT; + if (!link_shbuf_valid_access(resource_size(&priv->buffer), offset, size)) + return -EINVAL; + + pfn = (priv->buffer.start + offset) >> PAGE_SHIFT; + prot = vm_get_page_prot(flags); + + return remap_pfn_range(vma, vma->vm_start, pfn, size, prot); +} + +static bool link_shbuf_access_ok(fmode_t allowed, fmode_t request) +{ + static const fmode_t ACCESS_MASK = FMODE_READ|FMODE_WRITE|FMODE_EXEC; + fmode_t relevant = request & ACCESS_MASK; + return (relevant & allowed) == relevant; +} + +static int link_shbuf_open(struct inode *inode, struct file *file) +{ + struct cdev *cdev; + struct link_shbuf_data *priv; + + /* Retrieve a pointer to our private data */ + cdev = inode->i_cdev; + priv = container_of(cdev, struct link_shbuf_data, cdev); + if (WARN_ON(!link_shbuf_data_invariant(priv))) + return -EINVAL; + + if (!link_shbuf_access_ok(priv->permissions, file->f_mode)) + return -EACCES; + + file->private_data = priv; + + return 0; +} + +static const struct file_operations link_shbuf_ops = { + .owner = THIS_MODULE, + .read = link_shbuf_read, + .write = link_shbuf_write, + .poll = link_shbuf_poll, + .unlocked_ioctl = link_shbuf_ioctl, +#ifdef CONFIG_COMPAT + .compat_ioctl = link_shbuf_ioctl, +#endif +#ifdef CONFIG_MMU + .mmap = link_shbuf_mmap, +#endif + .open = link_shbuf_open, +}; + +/* + * Interrupt handler. + * + * This function will be called when our link partner uses the ioctl on their + * shared memory device to send an outgoing interrupt. + */ +static irqreturn_t link_shbuf_irq_handler(int irq, void *data) +{ + u64 payload, old, new; + struct _okl4_sys_interrupt_get_payload_return _payload; + + /* Retrieve a pointer to our private data. */ + struct link_shbuf_data *priv = data; + if (WARN_ON(!link_shbuf_data_invariant(priv))) + return IRQ_NONE; + + /* + * We should only ever be handling a single interrupt, and only if there + * was an incoming interrupt in the configuration. + */ + if (WARN_ON(priv->virq < 0 || priv->virq != irq)) + return IRQ_NONE; + + _payload = _okl4_sys_interrupt_get_payload(irq); + payload = (u64)_payload.payload; + + /* + * At this point, it is possible the pending flag is already set. It is up to + * the user to synchronise their transmission and acknowledgement of + * interrupts. + */ + + /* We open code atomic64_or which is not universally available. */ + do { + old = atomic64_read(&priv->virq_payload); + new = old | payload; + } while (atomic64_cmpxchg(&priv->virq_payload, old, new) != old); + smp_wmb(); + priv->virq_pending = true; + + wake_up_interruptible(&priv->virq_wq); + + return IRQ_HANDLED; +} + +/* + * Allocate a unique device number for this device. + * + * Note that this function needs to lock its access to link_shbuf_dev as there + * may be multiple threads attempting to acquire a new device number. + */ +static int link_shbuf_allocate_device(dev_t *devt) +{ + int ret = 0; + dev_t next; + + spin_lock(&device_number_allocate); + + *devt = link_shbuf_dev; + next = MKDEV(MAJOR(link_shbuf_dev), MINOR(link_shbuf_dev) + 1); + /* Check for overflow */ + if (MINOR(next) != MINOR(link_shbuf_dev) + 1) + ret = -ENOSPC; + else + link_shbuf_dev = next; + + spin_unlock(&device_number_allocate); + + return ret; +} + +/* + * Discover and add a new shared-buffer link. + * + * In the following function, we are expecting to parse device tree entries + * looking like the following: + * + * hypervisor { + * ... + * interrupt-line@1d { + * compatible = "okl,microvisor-interrupt-line", + * "okl,microvisor-capability"; + * phandle = <0x7>; + * reg = <0x1d>; + * label = "foo_virqline"; + * }; + * ; + * + * foo@41003000 { + * compatible = "okl,microvisor-link-shbuf", + * "okl,microvisor-shared-memory"; + * phandle = <0xd>; + * reg = <0x0 0x41003000 0x2000>; + * label = "foo"; + * okl,rwx = <0x6>; + * okl,interrupt-line = <0x7>; + * interrupts = <0x0 0x4 0x1>; + * interrupt-parent = <0x1>; + * }; + */ +static int link_shbuf_probe(struct platform_device *pdev) +{ + int ret; + struct device_node *node, *virqline; + struct link_shbuf_data *priv; + const char *name; + u32 permissions; + + node = pdev->dev.of_node; + + if (!node) + return -ENODEV; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + /* + * Retrieve the outgoing vIRQ cap. Note, this is configurable and we + * anticipate that it may not exist. + */ + virqline = of_parse_phandle(node, "okl,interrupt-line", 0); + if (!virqline) { + priv->virqline = NO_OUTGOING_IRQ; + } else { + ret = of_property_read_u32(virqline, "reg", &priv->virqline); + if (ret < 0 || priv->virqline == OKL4_KCAP_INVALID) { + of_node_put(virqline); + ret = -ENODEV; + goto err_free_dev; + } + } + of_node_put(virqline); + + /* Retrieve the incoming vIRQ number. Again, this is configurable and we + * anticipate that it may not exist. + */ + priv->virq = platform_get_irq(pdev, 0); + if (priv->virq < 0) + priv->virq = NO_INCOMING_IRQ; + + /* If we have a valid incoming vIRQ, register to handle it. */ + if (priv->virq >= 0) { + ret = devm_request_irq(&pdev->dev, priv->virq, link_shbuf_irq_handler, + 0, dev_name(&pdev->dev), priv); + if (ret < 0) { + dev_err(&pdev->dev, "failed request for IRQ\n"); + goto err_free_dev; + } + } + + init_waitqueue_head(&priv->virq_wq); + priv->virq_pending = false; + + /* Retrieve information about the shared memory region. */ + ret = of_address_to_resource(node, 0, &priv->buffer); + if (ret < 0) + goto err_free_irq; + /* + * We expect the Elfweaver to have validated that we have a non-NULL, + * page-aligned region. + */ + if (WARN_ON(priv->buffer.start == 0) || + WARN_ON(resource_size(&priv->buffer) % PAGE_SIZE != 0)) + goto err_free_irq; + if (!devm_request_mem_region(&pdev->dev, priv->buffer.start, + resource_size(&priv->buffer), dev_name(&pdev->dev))) { + ret = -ENODEV; + goto err_free_irq; + } + priv->base = devm_ioremap(&pdev->dev, priv->buffer.start, + resource_size(&priv->buffer)); + if (!priv->base) + goto err_release_region; + + /* Read the permissions of the shared memory region. */ + ret = of_property_read_u32(node, "okl,rwx", &permissions); + if (ret < 0) { + dev_err(&pdev->dev, "failed to read shared memory permissions\n"); + goto err_unmap_dev; + } + if (permissions & ~S_IRWXO) { + ret = -EINVAL; + goto err_unmap_dev; + } + priv->permissions = ((permissions & S_IROTH) ? FMODE_READ : 0) | + ((permissions & S_IWOTH) ? FMODE_WRITE : 0) | + ((permissions & S_IXOTH) ? FMODE_EXEC : 0); + if (WARN_ON(priv->permissions == 0)) { + ret = -EINVAL; + goto err_unmap_dev; + } + + /* Retrieve the label of this device. This will be the "name" attribute of + * the corresponding "link" tag in the system's XML specification. + */ + ret = of_property_read_string(node, "label", &name); + if (ret < 0) { + dev_err(&pdev->dev, "failed to read label\n"); + goto err_unmap_dev; + } + + cdev_init(&priv->cdev, &link_shbuf_ops); + ret = cdev_add(&priv->cdev, link_shbuf_dev, 1); + if (ret < 0) { + dev_err(&pdev->dev, "failed to add char dev region\n"); + goto err_unmap_dev; + } + + ret = link_shbuf_allocate_device(&priv->devt); + if (ret < 0) { + dev_err(&pdev->dev, "failed to allocate new device number\n"); + goto err_unmap_dev; + } + + /* We're now ready to create the device itself. */ + BUG_ON(name == NULL); + priv->dev = device_create(link_shbuf_class, &pdev->dev, priv->devt, + priv, "%s%s", DEV_PREFIX, name); + if (IS_ERR(priv->dev)) { + dev_err(&pdev->dev, "failed to create device\n"); + ret = PTR_ERR(priv->dev); + goto err_del_dev; + } + + dev_set_drvdata(&pdev->dev, priv); + + return 0; + +err_del_dev: + cdev_del(&priv->cdev); +err_unmap_dev: + devm_iounmap(&pdev->dev, priv->base); +err_release_region: + devm_release_mem_region(&pdev->dev, priv->buffer.start, + resource_size(&priv->buffer)); +err_free_irq: + if (priv->virq != NO_INCOMING_IRQ) + devm_free_irq(&pdev->dev, priv->virq, priv); +err_free_dev: + devm_kfree(&pdev->dev, priv); + return ret; +} + +static int link_shbuf_remove(struct platform_device *pdev) +{ + struct link_shbuf_data *priv; + + priv = dev_get_drvdata(&pdev->dev); + WARN_ON(!link_shbuf_data_invariant(priv)); + + device_destroy(link_shbuf_class, priv->devt); + + cdev_del(&priv->cdev); + + /* + * None of the following is strictly required, as these are all managed + * resources, but we clean it up anyway for clarity. + */ + + devm_iounmap(&pdev->dev, priv->base); + + devm_release_mem_region(&pdev->dev, priv->buffer.start, + resource_size(&priv->buffer)); + + if (priv->virq != NO_INCOMING_IRQ) + devm_free_irq(&pdev->dev, priv->virq, priv); + + devm_kfree(&pdev->dev, priv); + + return 0; +} + +static struct platform_driver of_plat_link_shbuf_driver = { + .driver = { + .name = "okl4-shbuf", + .of_match_table = okl4_link_shbuf_match, + }, + .probe = link_shbuf_probe, + .remove = link_shbuf_remove, +}; + +/* Maximum number of minor device numbers */ +enum { + MAX_MINOR = 1 << MINORBITS, +}; + +static int __init okl4_link_shbuf_init(void) +{ + int ret; + + link_shbuf_class = class_create(THIS_MODULE, DEVICE_NAME); + if (IS_ERR(link_shbuf_class)) { + pr_err("failed to create class\n"); + ret = PTR_ERR(link_shbuf_class); + return ret; + } + + ret = alloc_chrdev_region(&link_shbuf_dev, 0, MAX_MINOR, DEVICE_NAME); + if (ret < 0) { + pr_err("failed to allocate char dev region\n"); + goto err_destroy_class; + } + + ret = platform_driver_register(&of_plat_link_shbuf_driver); + if (ret < 0) { + pr_err("failed to register driver\n"); + goto err_unregister_dev_region; + } + + spin_lock_init(&device_number_allocate); + + return 0; + +err_unregister_dev_region: + unregister_chrdev_region(link_shbuf_dev, MAX_MINOR); +err_destroy_class: + class_destroy(link_shbuf_class); + return ret; +} +module_init(okl4_link_shbuf_init); + +static void __exit okl4_link_shbuf_exit(void) +{ + platform_driver_unregister(&of_plat_link_shbuf_driver); + unregister_chrdev_region(link_shbuf_dev, MAX_MINOR); + class_destroy(link_shbuf_class); +} +module_exit(okl4_link_shbuf_exit); + +MODULE_DESCRIPTION("OKL4 shared buffer link driver"); +MODULE_AUTHOR("Cog Systems Pty Ltd"); diff --git a/include/uapi/linux/Kbuild b/include/uapi/linux/Kbuild index 6527884402d5..e81b23a9f73c 100644 --- a/include/uapi/linux/Kbuild +++ b/include/uapi/linux/Kbuild @@ -15,3 +15,4 @@ endif ifneq ($(VSERVICES_SUPPORT), "") include include/linux/Kbuild.vservices endif +header-y += okl4-link-shbuf.h diff --git a/include/uapi/linux/okl4-link-shbuf.h b/include/uapi/linux/okl4-link-shbuf.h new file mode 100644 index 000000000000..69561bc3ec10 --- /dev/null +++ b/include/uapi/linux/okl4-link-shbuf.h @@ -0,0 +1,40 @@ +/* + * User-visible interface to driver for inter-cell links using the + * shared-buffer transport. + * + * Copyright (c) 2016 Cog Systems Pty Ltd. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version + * 2 of the License, or (at your option) any later version. + * + */ +#ifndef _LINUX_OKL4_LINK_SHBUF_H +#define _LINUX_OKL4_LINK_SHBUF_H + +#include +#include + +/* + * Ioctl that indicates a request to raise the outgoing vIRQ. This value is + * chosen to avoid conflict with the numbers documented in Linux 4.1's + * ioctl-numbers.txt. The argument is a payload to transmit to the receiver. + * Note that consecutive transmissions without an interleaved clear of the + * interrupt results in the payloads being ORed together. + */ +#define OKL4_LINK_SHBUF_IOCTL_IRQ_TX _IOW(0x8d, 1, __u64) + +/* + * Ioctl that indicates a request to clear any pending incoming vIRQ. The value + * returned through the argument to the ioctl is the payload, which is also + * cleared. + * + * The caller cannot distinguish between the cases of no pending interrupt and + * a pending interrupt with payload 0. It is expected that the caller is + * communicating with a cooperative sender and has polled their file descriptor + * to determine there is a pending interrupt before using this ioctl. + */ +#define OKL4_LINK_SHBUF_IOCTL_IRQ_CLR _IOR(0x8d, 2, __u64) + +#endif /* _LINUX_OKL4_LINK_SHBUF_H */