Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2f4ca293 authored by Carl van Schaik's avatar Carl van Schaik Committed by Gerrit - the friendly Code Review server
Browse files

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: default avatarCarl van Schaik <carl@cog.systems>
Git-commit: 18b0192bb16d90974af0401f1a46f20f0f5b9502
Git-repo: https://github.com/CogSystems/linux-msm.git


Signed-off-by: default avatarMinming Qi <mqi@codeaurora.org>
parent 402932a0
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -827,3 +827,13 @@ source "drivers/misc/echo/Kconfig"
source "drivers/misc/cxl/Kconfig"
source "drivers/misc/fpr_FingerprintCard/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.
+2 −0
Original line number Diff line number Diff line
@@ -80,3 +80,5 @@ OBJCOPYFLAGS_lkdtm_rodata_objcopy.o := \
targets += lkdtm_rodata.o lkdtm_rodata_objcopy.o
$(obj)/lkdtm_rodata_objcopy.o: $(obj)/lkdtm_rodata.o FORCE
	$(call if_changed,objcopy)

obj-$(CONFIG_OKL4_LINK_SHBUF)    += okl4-link-shbuf.o
+667 −0
Original line number Diff line number Diff line
/*
 * 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");
+1 −0
Original line number Diff line number Diff line
@@ -519,3 +519,4 @@ header-y += nfc/
ifneq ($(VSERVICES_SUPPORT), "")
include include/linux/Kbuild.vservices
endif
header-y += okl4-link-shbuf.h
+40 −0
Original line number Diff line number Diff line
/*
 *  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 <linux/ioctl.h>
#include <linux/types.h>

/*
 * 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 */