| /* |
| * 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 "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); |
| } |