You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
kernel_samsung_sm7125/drivers/misc/okl4-link-shbuf.c

667 lines
17 KiB

/*
* 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 <linux/atomic.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/ioctl.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/platform_device.h>
#include <linux/poll.h>
#include <linux/rwsem.h>
#include <linux/spinlock.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/sched.h>
#include <linux/wait.h>
#include <linux/version.h>
#include <microvisor/microvisor.h>
#include <uapi/linux/okl4-link-shbuf.h>
static const char DEVICE_NAME[] = "okl4_link_shbuf";
/* Created devices will appear as /dev/<DEV_PREFIX><name> */
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",
.owner = THIS_MODULE,
.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");