/* * Simple driver for userspace access to shared memory buffers with * associated inter-VM interrupts under the OKL4 Microvisor. * * Copyright (c) 2016 Cog Systems Pty Ltd. * Copyright (c) 2017 General Dynamics. * * 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 /* Linux version compatibility */ #if LINUX_VERSION_CODE < KERNEL_VERSION(3,11,0) #define DEVICE_ATTR_RO(_name) \ struct device_attribute dev_attr_##_name = __ATTR_RO(_name) #define OLD_ATTRS #endif #define OKL4_IPC_MINORBITS 9 #define OKL4_IPC_MINORS (1 << OKL4_IPC_MINORBITS) #define OKL4_IPC_MINOR_MAX (OKL4_IPC_MINORS - 1) #define OKL4_IPC_MAX_BUFFERS 10 struct virq_data { wait_queue_head_t wq; int irqno; bool raised; unsigned long payload; }; struct source_data { okl4_kcap_t kcap; }; struct vbuf_data { u64 addr; u64 size; u32 flags; }; struct okl4_vipc_device { struct device *dev; struct cdev cdev; int type; struct virq_data virq; struct source_data source; struct vbuf_data vbuf[OKL4_IPC_MAX_BUFFERS]; unsigned int num_bufs; int minor; const char *label; }; static irqreturn_t okl4_virq_handler(int irq, void *dev_id) { struct okl4_vipc_device *dev = dev_id; unsigned long payload; struct _okl4_sys_interrupt_get_payload_return _payload = _okl4_sys_interrupt_get_payload(irq); payload = _payload.payload; dev->virq.payload |= payload; smp_wmb(); dev->virq.raised = true; wake_up_interruptible(&dev->virq.wq); return IRQ_HANDLED; } static int okl4_vipc_dev_open(struct inode *inode, struct file *file) { struct okl4_vipc_device *dev; dev = container_of(inode->i_cdev, struct okl4_vipc_device, cdev); if (!dev) return -ENODEV; file->private_data = dev; return nonseekable_open(inode, file); } static int okl4_vipc_dev_release(struct inode *inode, struct file *file) { return 0; } static ssize_t okl4_vipc_read(struct file *file, char __user *buf, size_t length, loff_t *off) { struct okl4_vipc_device *dev = file->private_data; unsigned long payload; int ret; if (!dev->virq.raised && (file->f_flags & O_NONBLOCK)) { ret = -EAGAIN; goto err_wait; } do { ret = wait_event_interruptible(dev->virq.wq, dev->virq.raised); if (ret < 0) goto err_wait; } while(!xchg(&dev->virq.raised, 0)); smp_rmb(); payload = xchg(&dev->virq.payload, 0); if (length >= sizeof(payload)) { if (copy_to_user(buf, &payload, sizeof(payload))) { dev_dbg(dev->dev, "failed to copy data to userland\n"); return -EFAULT; } length = sizeof(payload); } else if (length != 0) { return -EIO; } return length; err_wait: return ret; } static unsigned int okl4_vipc_poll(struct file *file, poll_table *table) { int mask = 0; struct okl4_vipc_device *dev = file->private_data; poll_wait(file, &dev->virq.wq, table); if (dev->virq.raised) mask |= POLLIN | POLLRDNORM; return mask; } static ssize_t assert_virq(struct file *file, const char __user *ubuf, size_t length, loff_t *offset) { struct okl4_vipc_device *dev = file->private_data; okl4_error_t err; unsigned long payload; if (length == 0) { payload = 0; } else if (length == sizeof(payload)) { if (copy_from_user(&payload, ubuf, length)) { dev_dbg(dev->dev, "failed to copy data from userland\n"); return -EFAULT; } } else { dev_dbg(dev->dev, "length != sizeof(payload\n"); return -EIO; } err =_okl4_sys_vinterrupt_raise(dev->source.kcap, payload); if (err != OKL4_OK) { dev_dbg(dev->dev, "failed to raise virtual interrupt\n"); return -EIO; } return length; } static long okl4_vipc_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct okl4_vipc_device *dev = file->private_data; struct okl4_vipc_data *data = (struct okl4_vipc_data *)arg; int status = 0; /* Check user supplied address valid */ if (_IOC_DIR(cmd) & _IOC_READ) { status = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd)); } else if (_IOC_DIR(cmd) & _IOC_WRITE) { status = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd)); } if (status != 0) return -EFAULT; switch (cmd) { case OKL4_VIPC_GET_BUFFER_COUNT: *(unsigned long *)arg = dev->num_bufs; break; case OKL4_VIPC_GET_BUFFER_INFO: if (data->index >= 0 && data->index < dev->num_bufs) { status = copy_to_user(&data->size, &dev->vbuf[data->index].size, sizeof(u64)); if (status != 0) return -EFAULT; status = copy_to_user(&data->flags, &dev->vbuf[data->index].flags, sizeof(u32)); if (status != 0) return -EFAULT; } else { return -EINVAL; } break; default: return -ENOTTY; break; } return 0L; } static ssize_t is_virq_show(struct device *dev, struct device_attribute *attr, char *buf) { struct okl4_vipc_device *priv = dev_get_drvdata(dev); return scnprintf(buf, PAGE_SIZE, "%d\n", priv->virq.irqno >= 0); } static ssize_t is_source_show(struct device *dev, struct device_attribute *attr, char *buf) { struct okl4_vipc_device *priv = dev_get_drvdata(dev); return scnprintf(buf, PAGE_SIZE, "%d\n", priv->source.kcap != OKL4_KCAP_INVALID); } static ssize_t label_show(struct device *dev, struct device_attribute *attr, char *buf) { struct okl4_vipc_device *priv = dev_get_drvdata(dev); return scnprintf(buf, PAGE_SIZE, "%s\n", priv->label); } static int okl4_vipc_mmap(struct file *file, struct vm_area_struct *vma) { struct okl4_vipc_device *dev = file->private_data; unsigned long current_index = 0; /* * We are hijacking vm_pgoff(virtual memeory page offset) * When it is passed in from user space it is the byte offset * and must be a multiple of page size * By the time it is stored in vm_pgoff it is divided by page size * For example lets say we want to get index 2 in user space it will * call mmap with the offset set to 2 * PAGE_SIZE * Now once we are in kernel space the kernel will do the division * of (2*PAGE_SIZE)/PAGE_SIZE for us thus we just need to store * vm_pgoff as the buffer offset. Lastly, we need to reset the * vm_pgoff to 0 to ensure the vm_iomap_memory method maps the * entire shared buffer (and only the shared buffer) into user space */ current_index = vma->vm_pgoff; vma->vm_pgoff = 0; if (current_index >= dev->num_bufs) return -EINVAL; if (dev->vbuf[current_index].addr & (PAGE_SIZE - 1)) return -ENXIO; return vm_iomap_memory(vma, dev->vbuf[current_index].addr, dev->vbuf[current_index].size); } /* Device registration */ /* * This file operations structure will be used for a virq device. */ static const struct file_operations okl4_vipc_r_fops = { .owner = THIS_MODULE, .open = okl4_vipc_dev_open, .release = okl4_vipc_dev_release, .read = okl4_vipc_read, .poll = okl4_vipc_poll, .unlocked_ioctl = okl4_vipc_ioctl, .mmap = okl4_vipc_mmap, }; /* * This file operations structure will be used for a * virtual_interrupt_line device. */ static const struct file_operations okl4_vipc_w_fops = { .owner = THIS_MODULE, .open = okl4_vipc_dev_open, .release = okl4_vipc_dev_release, .write = assert_virq, .unlocked_ioctl = okl4_vipc_ioctl, .mmap = okl4_vipc_mmap, }; /* * This file operations structure will be used for a combined * virq+source device. */ static const struct file_operations okl4_vipc_rw_fops = { .owner = THIS_MODULE, .open = okl4_vipc_dev_open, .release = okl4_vipc_dev_release, .read = okl4_vipc_read, .poll = okl4_vipc_poll, .write = assert_virq, .unlocked_ioctl = okl4_vipc_ioctl, .mmap = okl4_vipc_mmap, }; #ifndef OLD_ATTRS static DEVICE_ATTR_RO(is_virq); static DEVICE_ATTR_RO(is_source); static DEVICE_ATTR_RO(label); static struct attribute *virq_attrs[] = { &dev_attr_is_virq.attr, &dev_attr_is_source.attr, &dev_attr_label.attr, NULL }; ATTRIBUTE_GROUPS(virq); #else static struct device_attribute virq_attrs[] = { __ATTR(is_virq, S_IRUGO, is_virq_show, NULL), __ATTR(is_source, S_IRUGO, is_source_show, NULL), __ATTR(label, S_IRUGO, label_show, NULL), __ATTR_NULL, }; #endif static struct class *okl4_vipc_class; static dev_t okl4_vipc_devt; static DEFINE_MUTEX(okl4_vipc_minor_lock); static unsigned long okl4_vipc_ids[BITS_TO_LONGS(OKL4_IPC_MINORBITS)]; /** * okl4_vipc_minor_get - obtain next free device minor number * * @dev: device pointer * * Return: allocated minor, or -ENOSPC if no free minor left */ static int okl4_vipc_minor_get(struct okl4_vipc_device *dev) { int ret; mutex_lock(&okl4_vipc_minor_lock); ret = find_first_zero_bit(okl4_vipc_ids, OKL4_IPC_MINORS); if (ret >= OKL4_IPC_MINORS) { dev_err(dev->dev, "too many okl4 ipc devices\n"); ret = -ENODEV; } else { dev->minor = ret; set_bit(ret, okl4_vipc_ids); ret = 0; } mutex_unlock(&okl4_vipc_minor_lock); return ret; } /** * okl4_vipc_minor_free - mark device minor number as free * * @dev: device pointer */ static void okl4_vipc_minor_free(struct okl4_vipc_device *dev) { mutex_lock(&okl4_vipc_minor_lock); clear_bit(dev->minor, okl4_vipc_ids); mutex_unlock(&okl4_vipc_minor_lock); } static void okl4_vipc_dev_deregister(struct okl4_vipc_device *dev) { int devno; devno = dev->cdev.dev; cdev_del(&dev->cdev); device_destroy(okl4_vipc_class, devno); okl4_vipc_minor_free(dev); } static int okl4_vipc_probe(struct platform_device *pdev) { int ret, devno, irq; okl4_kcap_t kcap; struct okl4_vipc_device *dev; struct device *parent; struct device *clsdev; /* class device */ const struct file_operations *fops; struct device_node *node; const char *devname = "vipc%d"; u32 all_flags[OKL4_IPC_MAX_BUFFERS]; parent = &pdev->dev; node = parent->of_node; dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev->virq.raised = false; dev->virq.payload = 0; irq = platform_get_irq(pdev, 0); dev->virq.irqno = irq; init_waitqueue_head(&dev->virq.wq); ret = okl4_vipc_minor_get(dev); if (ret < 0) goto err; if (irq >= 0) { ret = devm_request_irq(parent, irq, okl4_virq_handler, IRQF_TRIGGER_RISING, dev_name(parent), dev); if (ret < 0) goto err_free_minor; } /* Set number of buffers to 0 */ dev->num_bufs = 0; kcap = OKL4_KCAP_INVALID; if (of_device_is_compatible(node, "okl,microvisor-shared-memory-ipc")) { struct device_node *virqline_node = NULL; struct resource res; /* * loop through all avaible indexes and make sure not to go * above 9 (10 with 0 based index) */ int reg_index = 0; while(of_address_to_resource(node, reg_index, &res) >=0 && reg_index <= (OKL4_IPC_MAX_BUFFERS-1)) { /* * Get the start address of the shmem and calculate its * size */ dev->vbuf[reg_index].addr = res.start; dev->vbuf[reg_index].size = (res.end - res.start) + 1; dev->num_bufs++; reg_index++; } /* * We didnt find any registers on any index * this is bad, set errno and cleanup */ if (dev->num_bufs == 0) { ret = -ENODEV; goto err_free_minor; } /* * Next, get the access flags * On older kernels we cant use of_property_read_u32_index * since its not there. So we have to wait until we * know the number of buffers. Once we know that we * can get an array of the number of buffers size * Then we itterate through that array and store them * in their correct spot */ ret = of_property_read_u32_array(node, "okl,rwx", all_flags, dev->num_bufs); if (ret < 0) goto err_free_minor; for (reg_index=0; reg_index < dev->num_bufs; reg_index++) { dev->vbuf[reg_index].flags = all_flags[reg_index]; } /* Find the virqline node that corresponds to this node */ virqline_node = of_parse_phandle(node, "virqline", 0); if (!virqline_node) { ret = -ENODEV; goto err_free_minor; } ret = of_property_read_u32(virqline_node, "reg", &kcap); if (ret < 0) goto err_free_minor; } dev->source.kcap = kcap; ret = of_property_read_string(node, "label", &dev->label); if (ret != 0) { ret = -ENODEV; goto err_free_minor; } /* Fill in the data structures */ devno = MKDEV(MAJOR(okl4_vipc_devt), dev->minor); ret = -ENODEV; if (irq >= 0 && kcap != OKL4_KCAP_INVALID) fops = &okl4_vipc_rw_fops; else if (irq >= 0) fops = &okl4_vipc_r_fops; else if (kcap != OKL4_KCAP_INVALID) fops = &okl4_vipc_w_fops; else goto err_free_minor; cdev_init(&dev->cdev, fops); dev->cdev.owner = fops->owner; /* Add the device */ ret = cdev_add(&dev->cdev, devno, 1); if (ret) { dev_err(parent, "unable to add device %d:%d\n", MAJOR(okl4_vipc_devt), dev->minor); goto err_free_minor; } #ifndef OLD_ATTRS clsdev = device_create_with_groups(okl4_vipc_class, parent, devno, dev, virq_groups, devname, dev->minor); #else clsdev = device_create(okl4_vipc_class, parent, devno, dev, devname, dev->minor); #endif if (IS_ERR(clsdev)) { dev_err(parent, "unable to create device %d:%d\n", MAJOR(okl4_vipc_devt), dev->minor); ret = PTR_ERR(clsdev); goto err_dev_create; } dev_info(parent, "using /dev/vipc%d\n", dev->minor); dev_set_drvdata(parent, dev); return 0; err_dev_create: cdev_del(&dev->cdev); err_free_minor: okl4_vipc_minor_free(dev); err: devm_kfree(&pdev->dev, dev); return ret; } static int okl4_vipc_remove(struct platform_device *pdev) { struct okl4_vipc_device *priv = dev_get_drvdata(&pdev->dev); okl4_vipc_dev_deregister(priv); return 0; } /* Driver registration */ static const struct of_device_id of_plat_okl4_vipc_table[] = { { .compatible = "okl,microvisor-shared-memory-ipc" }, { /* end */ }, }; static struct platform_driver of_plat_okl4_vipc_driver = { .driver = { .name = "okl4_vipc", .owner = THIS_MODULE, .of_match_table = of_plat_okl4_vipc_table, }, .probe = okl4_vipc_probe, .remove = okl4_vipc_remove, }; static int __init okl4_vipc_init(void) { int ret; memset(okl4_vipc_ids, 0, sizeof(okl4_vipc_ids)); okl4_vipc_class = class_create(THIS_MODULE, "okl4_vipc"); if (IS_ERR(okl4_vipc_class)) { pr_err("failed to create class\n"); ret = PTR_ERR(okl4_vipc_class); goto err; } #ifdef OLD_ATTRS okl4_vipc_class->dev_attrs = virq_attrs; #endif ret = alloc_chrdev_region(&okl4_vipc_devt, 0, MINORMASK, "okl4_vipc"); if (ret < 0) { pr_err("failed to allocate char dev region\n"); goto err_class; } ret = platform_driver_register(&of_plat_okl4_vipc_driver); if (ret) goto err_vipc_driver; return 0; err_vipc_driver: unregister_chrdev_region(okl4_vipc_devt, MINORMASK); err_class: class_destroy(okl4_vipc_class); err: return ret; } static void __exit okl4_vipc_exit(void) { platform_driver_unregister(&of_plat_okl4_vipc_driver); unregister_chrdev_region(okl4_vipc_devt, MINORMASK); class_destroy(okl4_vipc_class); } module_init(okl4_vipc_init); module_exit(okl4_vipc_exit); MODULE_AUTHOR("General Dynamics"); MODULE_DESCRIPTION("OKL4 userspace VIPC interface");