/*
 * devio.c - cdev I/O for service devices
 *
 * Copyright (c) 2016 Cog Systems Pty Ltd
 *     Author: Philip Derrin <philip@cog.systems>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 */

#include <linux/version.h>
#include <linux/types.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/pagemap.h>
#include <linux/fs.h>
#include <linux/sched.h>
#include <linux/wait.h>
#include <linux/list.h>
#include <linux/atomic.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/uio.h>
#include <linux/uaccess.h>
#include <linux/poll.h>
#include <linux/security.h>
#include <linux/compat.h>

#include <vservices/types.h>
#include <vservices/buffer.h>
#include <vservices/transport.h>
#include <vservices/session.h>
#include <vservices/service.h>
#include <vservices/ioctl.h>
#include <linux/sched/signal.h>
#include "session.h"

#define VSERVICES_DEVICE_MAX (VS_MAX_SERVICES * VS_MAX_SESSIONS)

struct vs_devio_priv {
	struct kref kref;
	bool running, reset;

	/* Receive queue */
	wait_queue_head_t recv_wq;
	atomic_t notify_pending;
	struct list_head recv_queue;
};

static void
vs_devio_priv_free(struct kref *kref)
{
	struct vs_devio_priv *priv = container_of(kref, struct vs_devio_priv,
			kref);

	WARN_ON(priv->running);
	WARN_ON(!list_empty_careful(&priv->recv_queue));
	WARN_ON(waitqueue_active(&priv->recv_wq));

	kfree(priv);
}

static void vs_devio_priv_put(struct vs_devio_priv *priv)
{
	kref_put(&priv->kref, vs_devio_priv_free);
}

static int
vs_devio_service_probe(struct vs_service_device *service)
{
	struct vs_devio_priv *priv;

	priv = kmalloc(sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;

	kref_init(&priv->kref);
	priv->running = false;
	priv->reset = false;
	init_waitqueue_head(&priv->recv_wq);
	atomic_set(&priv->notify_pending, 0);
	INIT_LIST_HEAD(&priv->recv_queue);

	dev_set_drvdata(&service->dev, priv);

	wake_up(&service->quota_wq);

	return 0;
}

static int
vs_devio_service_remove(struct vs_service_device *service)
{
	struct vs_devio_priv *priv = dev_get_drvdata(&service->dev);

	WARN_ON(priv->running);
	WARN_ON(!list_empty_careful(&priv->recv_queue));
	WARN_ON(waitqueue_active(&priv->recv_wq));

	vs_devio_priv_put(priv);

	return 0;
}

static int
vs_devio_service_receive(struct vs_service_device *service,
		struct vs_mbuf *mbuf)
{
	struct vs_devio_priv *priv = dev_get_drvdata(&service->dev);

	WARN_ON(!priv->running);

	spin_lock(&priv->recv_wq.lock);
	list_add_tail(&mbuf->queue, &priv->recv_queue);
	wake_up_locked(&priv->recv_wq);
	spin_unlock(&priv->recv_wq.lock);

	return 0;
}

static void
vs_devio_service_notify(struct vs_service_device *service, u32 flags)
{
	struct vs_devio_priv *priv = dev_get_drvdata(&service->dev);
	int old, cur;

	WARN_ON(!priv->running);

	if (!flags)
		return;

	/* open-coded atomic_or() */
	cur = atomic_read(&priv->notify_pending);
	while ((old = atomic_cmpxchg(&priv->notify_pending,
					cur, cur | flags)) != cur)
		cur = old;

	wake_up(&priv->recv_wq);
}

static void
vs_devio_service_start(struct vs_service_device *service)
{
	struct vs_devio_priv *priv = dev_get_drvdata(&service->dev);

	if (!priv->reset) {
		WARN_ON(priv->running);
		priv->running = true;
		wake_up(&service->quota_wq);
	}
}

static void
vs_devio_service_reset(struct vs_service_device *service)
{
	struct vs_devio_priv *priv = dev_get_drvdata(&service->dev);
	struct vs_mbuf *mbuf, *tmp;

	WARN_ON(!priv->running && !priv->reset);

	/*
	 * Mark the service as being in reset. This flag can never be cleared
	 * on an open device; the user must acknowledge the reset by closing
	 * and reopening the device.
	 */
	priv->reset = true;
	priv->running = false;

	spin_lock_irq(&priv->recv_wq.lock);
	list_for_each_entry_safe(mbuf, tmp, &priv->recv_queue, queue)
		vs_service_free_mbuf(service, mbuf);
	INIT_LIST_HEAD(&priv->recv_queue);
	spin_unlock_irq(&priv->recv_wq.lock);
	wake_up_all(&priv->recv_wq);
}

/*
 * This driver will be registered by the core server module, which must also
 * set its bus and owner function pointers.
 */
struct vs_service_driver vs_devio_server_driver = {
	/* No protocol, so the normal bus match will never bind this. */
	.protocol	= NULL,
	.is_server	= true,
	.rx_atomic	= true,

	.probe		= vs_devio_service_probe,
	.remove		= vs_devio_service_remove,
	.receive	= vs_devio_service_receive,
	.notify		= vs_devio_service_notify,
	.start		= vs_devio_service_start,
	.reset		= vs_devio_service_reset,

	/*
	 * Set reasonable default quotas. These can be overridden by passing
	 * nonzero values to IOCTL_VS_BIND_SERVER, which will set the
	 * service's *_quota_set fields.
	 */
	.in_quota_min	= 1,
	.in_quota_best	= 8,
	.out_quota_min	= 1,
	.out_quota_best	= 8,

	/* Mark the notify counts as invalid; the service's will be used. */
	.in_notify_count = (unsigned)-1,
	.out_notify_count = (unsigned)-1,

	.driver		= {
		.name			= "vservices-server-devio",
		.owner			= NULL, /* set by core server */
		.bus			= NULL, /* set by core server */
		.suppress_bind_attrs	= true, /* see vs_devio_poll */
	},
};
EXPORT_SYMBOL_GPL(vs_devio_server_driver);

static int
vs_devio_bind_server(struct vs_service_device *service,
		struct vs_ioctl_bind *bind)
{
	int ret = -ENODEV;

	/* Ensure the server module is loaded and the driver is registered. */
	if (!try_module_get(vs_devio_server_driver.driver.owner))
		goto fail_module_get;

	device_lock(&service->dev);
	ret = -EBUSY;
	if (service->dev.driver != NULL)
		goto fail_device_unbound;

	/* Set up the quota and notify counts. */
	service->in_quota_set = bind->recv_quota;
	service->out_quota_set = bind->send_quota;
	service->notify_send_bits = bind->send_notify_bits;
	service->notify_recv_bits = bind->recv_notify_bits;

	/* Manually probe the driver. */
	service->dev.driver = &vs_devio_server_driver.driver;
	ret = service->dev.bus->probe(&service->dev);
	if (ret < 0)
		goto fail_probe_driver;

	ret = device_bind_driver(&service->dev);
	if (ret < 0)
		goto fail_bind_driver;

	/* Pass the allocated quotas back to the user. */
	bind->recv_quota = service->recv_quota;
	bind->send_quota = service->send_quota;
	bind->msg_size = vs_service_max_mbuf_size(service);

	device_unlock(&service->dev);
	module_put(vs_devio_server_driver.driver.owner);

	return 0;

fail_bind_driver:
	ret = service->dev.bus->remove(&service->dev);
fail_probe_driver:
	service->dev.driver = NULL;
fail_device_unbound:
	device_unlock(&service->dev);
	module_put(vs_devio_server_driver.driver.owner);
fail_module_get:
	return ret;
}

/*
 * This driver will be registered by the core client module, which must also
 * set its bus and owner pointers.
 */
struct vs_service_driver vs_devio_client_driver = {
	/* No protocol, so the normal bus match will never bind this. */
	.protocol	= NULL,
	.is_server	= false,
	.rx_atomic	= true,

	.probe		= vs_devio_service_probe,
	.remove		= vs_devio_service_remove,
	.receive	= vs_devio_service_receive,
	.notify		= vs_devio_service_notify,
	.start		= vs_devio_service_start,
	.reset		= vs_devio_service_reset,

	.driver		= {
		.name			= "vservices-client-devio",
		.owner			= NULL, /* set by core client */
		.bus			= NULL, /* set by core client */
		.suppress_bind_attrs	= true, /* see vs_devio_poll */
	},
};
EXPORT_SYMBOL_GPL(vs_devio_client_driver);

static int
vs_devio_bind_client(struct vs_service_device *service,
		struct vs_ioctl_bind *bind)
{
	int ret = -ENODEV;

	/* Ensure the client module is loaded and the driver is registered. */
	if (!try_module_get(vs_devio_client_driver.driver.owner))
		goto fail_module_get;

	device_lock(&service->dev);
	ret = -EBUSY;
	if (service->dev.driver != NULL)
		goto fail_device_unbound;

	/* Manually probe the driver. */
	service->dev.driver = &vs_devio_client_driver.driver;
	ret = service->dev.bus->probe(&service->dev);
	if (ret < 0)
		goto fail_probe_driver;

	ret = device_bind_driver(&service->dev);
	if (ret < 0)
		goto fail_bind_driver;

	/* Pass the allocated quotas back to the user. */
	bind->recv_quota = service->recv_quota;
	bind->send_quota = service->send_quota;
	bind->msg_size = vs_service_max_mbuf_size(service);
	bind->send_notify_bits = service->notify_send_bits;
	bind->recv_notify_bits = service->notify_recv_bits;

	device_unlock(&service->dev);
	module_put(vs_devio_client_driver.driver.owner);

	return 0;

fail_bind_driver:
	ret = service->dev.bus->remove(&service->dev);
fail_probe_driver:
	service->dev.driver = NULL;
fail_device_unbound:
	device_unlock(&service->dev);
	module_put(vs_devio_client_driver.driver.owner);
fail_module_get:
	return ret;
}

static struct vs_devio_priv *
vs_devio_priv_get_from_service(struct vs_service_device *service)
{
	struct vs_devio_priv *priv = NULL;
	struct device_driver *drv;

	if (!service)
		return NULL;

	device_lock(&service->dev);
	drv = service->dev.driver;

	if ((drv == &vs_devio_client_driver.driver) ||
			(drv == &vs_devio_server_driver.driver)) {
		vs_service_state_lock(service);
		priv = dev_get_drvdata(&service->dev);
		if (priv)
			kref_get(&priv->kref);
		vs_service_state_unlock(service);
	}

	device_unlock(&service->dev);

	return priv;
}

static int
vs_devio_open(struct inode *inode, struct file *file)
{
	struct vs_service_device *service;

	if (imajor(inode) != vservices_cdev_major)
		return -ENODEV;

	service = vs_service_lookup_by_devt(inode->i_rdev);
	if (!service)
		return -ENODEV;

	file->private_data = service;

	return 0;
}

static int
vs_devio_release(struct inode *inode, struct file *file)
{
	struct vs_service_device *service = file->private_data;

	if (service) {
		struct vs_devio_priv *priv =
			vs_devio_priv_get_from_service(service);

		if (priv) {
			device_release_driver(&service->dev);
			vs_devio_priv_put(priv);
		}

		file->private_data = NULL;
		vs_put_service(service);
	}

	return 0;
}

static struct iovec *
vs_devio_check_iov(struct vs_ioctl_iovec *io, bool is_send, ssize_t *total)
{
	struct iovec *iov;
	unsigned i;
	int ret;

	if (io->iovcnt > UIO_MAXIOV)
		return ERR_PTR(-EINVAL);

	iov = kmalloc(sizeof(*iov) * io->iovcnt, GFP_KERNEL);
	if (!iov)
		return ERR_PTR(-ENOMEM);

	if (copy_from_user(iov, io->iov, sizeof(*iov) * io->iovcnt)) {
		ret = -EFAULT;
		goto fail;
	}

	*total = 0;
	for (i = 0; i < io->iovcnt; i++) {
		ssize_t iov_len = (ssize_t)iov[i].iov_len;

		if (iov_len > MAX_RW_COUNT - *total) {
			ret = -EINVAL;
			goto fail;
		}

		if (!access_ok(is_send ? VERIFY_READ : VERIFY_WRITE,
					iov[i].iov_base, iov_len)) {
			ret = -EFAULT;
			goto fail;
		}

		*total += iov_len;
	}

	return iov;

fail:
	kfree(iov);
	return ERR_PTR(ret);
}

static ssize_t
vs_devio_send(struct vs_service_device *service, struct iovec *iov,
		size_t iovcnt, ssize_t to_send, bool nonblocking)
{
	struct vs_mbuf *mbuf = NULL;
	struct vs_devio_priv *priv;
	unsigned i;
	ssize_t offset = 0;
	ssize_t ret;
	DEFINE_WAIT(wait);

	priv = vs_devio_priv_get_from_service(service);
	ret = -ENODEV;
	if (!priv)
		goto fail_priv_get;

	vs_service_state_lock(service);

	/*
	 * Waiting alloc. We must open-code this because there is no real
	 * state structure or base state.
	 */
	ret = 0;
	while (!vs_service_send_mbufs_available(service)) {
		if (nonblocking) {
			ret = -EAGAIN;
			break;
		}

		if (signal_pending(current)) {
			ret = -ERESTARTSYS;
			break;
		}

		prepare_to_wait_exclusive(&service->quota_wq, &wait,
				TASK_INTERRUPTIBLE);

		vs_service_state_unlock(service);
		schedule();
		vs_service_state_lock(service);

		if (priv->reset) {
			ret = -ECONNRESET;
			break;
		}

		if (!priv->running) {
			ret = -ENOTCONN;
			break;
		}
	}
	finish_wait(&service->quota_wq, &wait);

	if (ret)
		goto fail_alloc;

	mbuf = vs_service_alloc_mbuf(service, to_send, GFP_KERNEL);
	if (IS_ERR(mbuf)) {
		ret = PTR_ERR(mbuf);
		goto fail_alloc;
	}

	/* Ready to send; copy data into the mbuf. */
	ret = -EFAULT;
	for (i = 0; i < iovcnt; i++) {
		if (copy_from_user(mbuf->data + offset, iov[i].iov_base,
					iov[i].iov_len))
			goto fail_copy;
		offset += iov[i].iov_len;
	}
	mbuf->size = to_send;

	/* Send the message. */
	ret = vs_service_send(service, mbuf);
	if (ret < 0)
		goto fail_send;

	/* Wake the next waiter, if there's more quota available. */
	if (waitqueue_active(&service->quota_wq) &&
			vs_service_send_mbufs_available(service) > 0)
		wake_up(&service->quota_wq);

	vs_service_state_unlock(service);
	vs_devio_priv_put(priv);

	return to_send;

fail_send:
fail_copy:
	vs_service_free_mbuf(service, mbuf);
	wake_up(&service->quota_wq);
fail_alloc:
	vs_service_state_unlock(service);
	vs_devio_priv_put(priv);
fail_priv_get:
	return ret;
}

static ssize_t
vs_devio_recv(struct vs_service_device *service, struct iovec *iov,
		size_t iovcnt, u32 *notify_bits, ssize_t recv_space,
		bool nonblocking)
{
	struct vs_mbuf *mbuf = NULL;
	struct vs_devio_priv *priv;
	unsigned i;
	ssize_t offset = 0;
	ssize_t ret;
	DEFINE_WAIT(wait);

	priv = vs_devio_priv_get_from_service(service);
	ret = -ENODEV;
	if (!priv)
		goto fail_priv_get;

	/* Take the recv_wq lock, which also protects recv_queue. */
	spin_lock_irq(&priv->recv_wq.lock);

	/* Wait for a message, notification, or reset. */
	ret = wait_event_interruptible_exclusive_locked_irq(priv->recv_wq,
			!list_empty(&priv->recv_queue) || priv->reset ||
			atomic_read(&priv->notify_pending) || nonblocking);

	if (priv->reset)
		ret = -ECONNRESET; /* Service reset */
	else if (!ret && list_empty(&priv->recv_queue))
		ret = -EAGAIN; /* Nonblocking, or notification */

	if (ret < 0) {
		spin_unlock_irq(&priv->recv_wq.lock);
		goto no_mbuf;
	}

	/* Take the first mbuf from the list, and check its size. */
	mbuf = list_first_entry(&priv->recv_queue, struct vs_mbuf, queue);
	if (mbuf->size > recv_space) {
		spin_unlock_irq(&priv->recv_wq.lock);
		ret = -EMSGSIZE;
		goto fail_msg_size;
	}
	list_del_init(&mbuf->queue);

	spin_unlock_irq(&priv->recv_wq.lock);

	/* Copy to user. */
	ret = -EFAULT;
	for (i = 0; (mbuf->size > offset) && (i < iovcnt); i++) {
		size_t len = min(mbuf->size - offset, iov[i].iov_len);
		if (copy_to_user(iov[i].iov_base, mbuf->data + offset, len))
			goto fail_copy;
		offset += len;
	}
	ret = offset;

no_mbuf:
	/*
	 * Read and clear the pending notification bits. If any notifications
	 * are received, don't return an error, even if we failed to receive a
	 * message.
	 */
	*notify_bits = atomic_xchg(&priv->notify_pending, 0);
	if ((ret < 0) && *notify_bits)
		ret = 0;

fail_copy:
	if (mbuf)
		vs_service_free_mbuf(service, mbuf);
fail_msg_size:
	vs_devio_priv_put(priv);
fail_priv_get:
	return ret;
}

static int
vs_devio_check_perms(struct file *file, unsigned flags)
{
	if ((flags & MAY_READ) & !(file->f_mode & FMODE_READ))
		return -EBADF;

	if ((flags & MAY_WRITE) & !(file->f_mode & FMODE_WRITE))
		return -EBADF;

	return security_file_permission(file, flags);
}

static long
vs_devio_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	void __user *ptr = (void __user *)arg;
	struct vs_service_device *service = file->private_data;
	struct vs_ioctl_bind bind;
	struct vs_ioctl_iovec io;
	u32 flags;
	long ret;
	ssize_t iov_total;
	struct iovec *iov;

	if (!service)
		return -ENODEV;

	switch (cmd) {
	case IOCTL_VS_RESET_SERVICE:
		ret = vs_devio_check_perms(file, MAY_WRITE);
		if (ret < 0)
			break;
		ret = vs_service_reset(service, service);
		break;
	case IOCTL_VS_GET_NAME:
		ret = vs_devio_check_perms(file, MAY_READ);
		if (ret < 0)
			break;
		if (service->name != NULL) {
			size_t len = strnlen(service->name,
					_IOC_SIZE(IOCTL_VS_GET_NAME) - 1);
			if (copy_to_user(ptr, service->name, len + 1))
				ret = -EFAULT;
		} else {
			ret = -EINVAL;
		}
		break;
	case IOCTL_VS_GET_PROTOCOL:
		ret = vs_devio_check_perms(file, MAY_READ);
		if (ret < 0)
			break;
		if (service->protocol != NULL) {
			size_t len = strnlen(service->protocol,
					_IOC_SIZE(IOCTL_VS_GET_PROTOCOL) - 1);
			if (copy_to_user(ptr, service->protocol, len + 1))
				ret = -EFAULT;
		} else {
			ret = -EINVAL;
		}
		break;
	case IOCTL_VS_BIND_CLIENT:
		ret = vs_devio_check_perms(file, MAY_EXEC);
		if (ret < 0)
			break;
		ret = vs_devio_bind_client(service, &bind);
		if (!ret && copy_to_user(ptr, &bind, sizeof(bind)))
			ret = -EFAULT;
		break;
	case IOCTL_VS_BIND_SERVER:
		ret = vs_devio_check_perms(file, MAY_EXEC);
		if (ret < 0)
			break;
		if (copy_from_user(&bind, ptr, sizeof(bind))) {
			ret = -EFAULT;
			break;
		}
		ret = vs_devio_bind_server(service, &bind);
		if (!ret && copy_to_user(ptr, &bind, sizeof(bind)))
			ret = -EFAULT;
		break;
	case IOCTL_VS_NOTIFY:
		ret = vs_devio_check_perms(file, MAY_WRITE);
		if (ret < 0)
			break;
		if (copy_from_user(&flags, ptr, sizeof(flags))) {
			ret = -EFAULT;
			break;
		}
		ret = vs_service_notify(service, flags);
		break;
	case IOCTL_VS_SEND:
		ret = vs_devio_check_perms(file, MAY_WRITE);
		if (ret < 0)
			break;
		if (copy_from_user(&io, ptr, sizeof(io))) {
			ret = -EFAULT;
			break;
		}

		iov = vs_devio_check_iov(&io, true, &iov_total);
		if (IS_ERR(iov)) {
			ret = PTR_ERR(iov);
			break;
		}

		ret = vs_devio_send(service, iov, io.iovcnt, iov_total,
				file->f_flags & O_NONBLOCK);
		kfree(iov);
		break;
	case IOCTL_VS_RECV:
		ret = vs_devio_check_perms(file, MAY_READ);
		if (ret < 0)
			break;
		if (copy_from_user(&io, ptr, sizeof(io))) {
			ret = -EFAULT;
			break;
		}

		iov = vs_devio_check_iov(&io, true, &iov_total);
		if (IS_ERR(iov)) {
			ret = PTR_ERR(iov);
			break;
		}

		ret = vs_devio_recv(service, iov, io.iovcnt,
			&io.notify_bits, iov_total,
			file->f_flags & O_NONBLOCK);
		kfree(iov);

		if (ret >= 0) {
			u32 __user *notify_bits_ptr = ptr + offsetof(
					struct vs_ioctl_iovec, notify_bits);
			if (copy_to_user(notify_bits_ptr, &io.notify_bits,
					sizeof(io.notify_bits)))
				ret = -EFAULT;
		}
		break;
	default:
		dev_dbg(&service->dev, "Unknown ioctl %#x, arg: %lx\n", cmd,
				arg);
		ret = -ENOSYS;
		break;
	}

	return ret;
}

#ifdef CONFIG_COMPAT

struct vs_compat_ioctl_bind {
	__u32 send_quota;
	__u32 recv_quota;
	__u32 send_notify_bits;
	__u32 recv_notify_bits;
	compat_size_t msg_size;
};

#define compat_ioctl_bind_conv(dest, src) ({ \
	dest.send_quota = src.send_quota;		\
	dest.recv_quota = src.recv_quota;		\
	dest.send_notify_bits = src.send_notify_bits;	\
	dest.recv_notify_bits = src.recv_notify_bits;	\
	dest.msg_size = (compat_size_t)src.msg_size;	\
})

#define COMPAT_IOCTL_VS_BIND_CLIENT _IOR('4', 3, struct vs_compat_ioctl_bind)
#define COMPAT_IOCTL_VS_BIND_SERVER _IOWR('4', 4, struct vs_compat_ioctl_bind)

struct vs_compat_ioctl_iovec {
	union {
		__u32 iovcnt; /* input */
		__u32 notify_bits; /* output (recv only) */
	};
	compat_uptr_t iov;
};

#define COMPAT_IOCTL_VS_SEND \
    _IOW('4', 6, struct vs_compat_ioctl_iovec)
#define COMPAT_IOCTL_VS_RECV \
    _IOWR('4', 7, struct vs_compat_ioctl_iovec)

static struct iovec *
vs_devio_check_compat_iov(struct vs_compat_ioctl_iovec *c_io,
	bool is_send, ssize_t *total)
{
	struct iovec *iov;
	struct compat_iovec *c_iov;

	unsigned i;
	int ret;

	if (c_io->iovcnt > UIO_MAXIOV)
		return ERR_PTR(-EINVAL);

	c_iov = kzalloc(sizeof(*c_iov) * c_io->iovcnt, GFP_KERNEL);
	if (!c_iov)
		return ERR_PTR(-ENOMEM);

	iov = kzalloc(sizeof(*iov) * c_io->iovcnt, GFP_KERNEL);
	if (!iov) {
		kfree(c_iov);
		return ERR_PTR(-ENOMEM);
	}

	if (copy_from_user(c_iov, (struct compat_iovec __user *)
		compat_ptr(c_io->iov), sizeof(*c_iov) * c_io->iovcnt)) {
		ret = -EFAULT;
		goto fail;
	}

	*total = 0;
	for (i = 0; i < c_io->iovcnt; i++) {
		ssize_t iov_len;
		iov[i].iov_base = compat_ptr (c_iov[i].iov_base);
		iov[i].iov_len = (compat_size_t) c_iov[i].iov_len;

		iov_len = (ssize_t)iov[i].iov_len;

		if (iov_len > MAX_RW_COUNT - *total) {
			ret = -EINVAL;
			goto fail;
		}

		if (!access_ok(is_send ? VERIFY_READ : VERIFY_WRITE,
					iov[i].iov_base, iov_len)) {
			ret = -EFAULT;
			goto fail;
		}

		*total += iov_len;
	}

	kfree (c_iov);
	return iov;

fail:
	kfree(c_iov);
	kfree(iov);
	return ERR_PTR(ret);
}

static long
vs_devio_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	void __user *ptr = (void __user *)arg;
	struct vs_service_device *service = file->private_data;
	struct vs_ioctl_bind bind;
	struct vs_compat_ioctl_bind compat_bind;
	struct vs_compat_ioctl_iovec compat_io;
	long ret;
	ssize_t iov_total;
	struct iovec *iov;

	if (!service)
		return -ENODEV;

	switch (cmd) {
	case IOCTL_VS_RESET_SERVICE:
	case IOCTL_VS_GET_NAME:
	case IOCTL_VS_GET_PROTOCOL:
		return vs_devio_ioctl (file, cmd, arg);
	case COMPAT_IOCTL_VS_SEND:
		ret = vs_devio_check_perms(file, MAY_WRITE);
		if (ret < 0)
			break;
		if (copy_from_user(&compat_io, ptr, sizeof(compat_io))) {
			ret = -EFAULT;
			break;
		}

		iov = vs_devio_check_compat_iov(&compat_io, true, &iov_total);
		if (IS_ERR(iov)) {
			ret = PTR_ERR(iov);
			break;
		}

		ret = vs_devio_send(service, iov, compat_io.iovcnt, iov_total,
				file->f_flags & O_NONBLOCK);
		kfree(iov);

		break;
	case COMPAT_IOCTL_VS_RECV:
		ret = vs_devio_check_perms(file, MAY_READ);
		if (ret < 0)
			break;
		if (copy_from_user(&compat_io, ptr, sizeof(compat_io))) {
			ret = -EFAULT;
			break;
		}

		iov = vs_devio_check_compat_iov(&compat_io, true, &iov_total);
		if (IS_ERR(iov)) {
			ret = PTR_ERR(iov);
			break;
		}

		ret = vs_devio_recv(service, iov, compat_io.iovcnt,
			&compat_io.notify_bits, iov_total,
			file->f_flags & O_NONBLOCK);
		kfree(iov);

		if (ret >= 0) {
			u32 __user *notify_bits_ptr = ptr + offsetof(
					struct vs_compat_ioctl_iovec, notify_bits);
			if (copy_to_user(notify_bits_ptr, &compat_io.notify_bits,
					sizeof(compat_io.notify_bits)))
				ret = -EFAULT;
		}
		break;
	case COMPAT_IOCTL_VS_BIND_CLIENT:
		ret = vs_devio_check_perms(file, MAY_EXEC);
		if (ret < 0)
			break;
		ret = vs_devio_bind_client(service, &bind);
		compat_ioctl_bind_conv(compat_bind, bind);
		if (!ret && copy_to_user(ptr, &compat_bind,
					sizeof(compat_bind)))
			ret = -EFAULT;
		break;
	case COMPAT_IOCTL_VS_BIND_SERVER:
		ret = vs_devio_check_perms(file, MAY_EXEC);
		if (ret < 0)
			break;
		if (copy_from_user(&compat_bind, ptr, sizeof(compat_bind))) {
			ret = -EFAULT;
			break;
		}
		compat_ioctl_bind_conv(bind, compat_bind);
		ret = vs_devio_bind_server(service, &bind);
		compat_ioctl_bind_conv(compat_bind, bind);
		if (!ret && copy_to_user(ptr, &compat_bind,
					sizeof(compat_bind)))
			ret = -EFAULT;
		break;
	default:
		dev_dbg(&service->dev, "Unknown ioctl %#x, arg: %lx\n", cmd,
				arg);
		ret = -ENOSYS;
		break;
	}

	return ret;
}

#endif /* CONFIG_COMPAT */

static unsigned int
vs_devio_poll(struct file *file, struct poll_table_struct *wait)
{
	struct vs_service_device *service = file->private_data;
	struct vs_devio_priv *priv = vs_devio_priv_get_from_service(service);
	unsigned int flags = 0;

	poll_wait(file, &service->quota_wq, wait);

	if (priv) {
		/*
		 * Note: there is no way for us to ensure that all poll
		 * waiters on a given workqueue have gone away, other than to
		 * actually close the file. So, this poll_wait() is only safe
		 * if we never release our claim on the service before the
		 * file is closed.
		 *
		 * We try to guarantee this by only unbinding the devio driver
		 * on close, and setting suppress_bind_attrs in the driver so
		 * root can't unbind us with sysfs.
		 */
		poll_wait(file, &priv->recv_wq, wait);

		if (priv->reset) {
			/* Service reset; raise poll error. */
			flags |= POLLERR | POLLHUP;
		} else if (priv->running) {
			if (!list_empty_careful(&priv->recv_queue))
				flags |= POLLRDNORM | POLLIN;
			if (atomic_read(&priv->notify_pending))
				flags |= POLLRDNORM | POLLIN;
			if (vs_service_send_mbufs_available(service) > 0)
				flags |= POLLWRNORM | POLLOUT;
		}

		vs_devio_priv_put(priv);
	} else {
		/* No driver attached. Return error flags. */
		flags |= POLLERR | POLLHUP;
	}

	return flags;
}

static const struct file_operations vs_fops = {
	.owner		= THIS_MODULE,
	.open		= vs_devio_open,
	.release	= vs_devio_release,
	.unlocked_ioctl	= vs_devio_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl	= vs_devio_compat_ioctl,
#endif
	.poll		= vs_devio_poll,
};

int vservices_cdev_major;
static struct cdev vs_cdev;

int __init
vs_devio_init(void)
{
	dev_t dev;
	int r;

	r = alloc_chrdev_region(&dev, 0, VSERVICES_DEVICE_MAX,
			"vs_service");
	if (r < 0)
		goto fail_alloc_chrdev;
	vservices_cdev_major = MAJOR(dev);

	cdev_init(&vs_cdev, &vs_fops);
	r = cdev_add(&vs_cdev, dev, VSERVICES_DEVICE_MAX);
	if (r < 0)
		goto fail_cdev_add;

	return 0;

fail_cdev_add:
	unregister_chrdev_region(dev, VSERVICES_DEVICE_MAX);
fail_alloc_chrdev:
	return r;
}

void __exit
vs_devio_exit(void)
{
	cdev_del(&vs_cdev);
	unregister_chrdev_region(MKDEV(vservices_cdev_major, 0),
			VSERVICES_DEVICE_MAX);
}
