| // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause | |
| /* | |
| * Copyright (c) 2014 Raspberry Pi (Trading) Ltd. All rights reserved. | |
| * Copyright (c) 2010-2012 Broadcom. All rights reserved. | |
| */ | |
| #include <linux/cdev.h> | |
| #include <linux/fs.h> | |
| #include <linux/device.h> | |
| #include <linux/slab.h> | |
| #include <linux/compat.h> | |
| #include <linux/miscdevice.h> | |
| #include <linux/raspberrypi/vchiq_core.h> | |
| #include <linux/raspberrypi/vchiq_arm.h> | |
| #include <linux/raspberrypi/vchiq_debugfs.h> | |
| #include "vchiq_ioctl.h" | |
| static const char *const ioctl_names[] = { | |
| "CONNECT", | |
| "SHUTDOWN", | |
| "CREATE_SERVICE", | |
| "REMOVE_SERVICE", | |
| "QUEUE_MESSAGE", | |
| "QUEUE_BULK_TRANSMIT", | |
| "QUEUE_BULK_RECEIVE", | |
| "AWAIT_COMPLETION", | |
| "DEQUEUE_MESSAGE", | |
| "GET_CLIENT_ID", | |
| "GET_CONFIG", | |
| "CLOSE_SERVICE", | |
| "USE_SERVICE", | |
| "RELEASE_SERVICE", | |
| "SET_SERVICE_OPTION", | |
| "DUMP_PHYS_MEM", | |
| "LIB_VERSION", | |
| "CLOSE_DELIVERED" | |
| }; | |
| static_assert(ARRAY_SIZE(ioctl_names) == (VCHIQ_IOC_MAX + 1)); | |
| static void | |
| user_service_free(void *userdata) | |
| { | |
| kfree(userdata); | |
| } | |
| static void close_delivered(struct user_service *user_service) | |
| { | |
| dev_dbg(user_service->service->state->dev, | |
| "arm: (handle=%x)\n", user_service->service->handle); | |
| if (user_service->close_pending) { | |
| /* Allow the underlying service to be culled */ | |
| vchiq_service_put(user_service->service); | |
| /* Wake the user-thread blocked in close_ or remove_service */ | |
| complete(&user_service->close_event); | |
| user_service->close_pending = 0; | |
| } | |
| } | |
| struct vchiq_io_copy_callback_context { | |
| struct vchiq_element *element; | |
| size_t element_offset; | |
| unsigned long elements_to_go; | |
| }; | |
| static ssize_t vchiq_ioc_copy_element_data(void *context, void *dest, | |
| size_t offset, size_t maxsize) | |
| { | |
| struct vchiq_io_copy_callback_context *cc = context; | |
| size_t total_bytes_copied = 0; | |
| size_t bytes_this_round; | |
| while (total_bytes_copied < maxsize) { | |
| if (!cc->elements_to_go) | |
| return total_bytes_copied; | |
| if (!cc->element->size) { | |
| cc->elements_to_go--; | |
| cc->element++; | |
| cc->element_offset = 0; | |
| continue; | |
| } | |
| bytes_this_round = min(cc->element->size - cc->element_offset, | |
| maxsize - total_bytes_copied); | |
| if (copy_from_user(dest + total_bytes_copied, | |
| cc->element->data + cc->element_offset, | |
| bytes_this_round)) | |
| return -EFAULT; | |
| cc->element_offset += bytes_this_round; | |
| total_bytes_copied += bytes_this_round; | |
| if (cc->element_offset == cc->element->size) { | |
| cc->elements_to_go--; | |
| cc->element++; | |
| cc->element_offset = 0; | |
| } | |
| } | |
| return maxsize; | |
| } | |
| static int | |
| vchiq_ioc_queue_message(struct vchiq_instance *instance, unsigned int handle, | |
| struct vchiq_element *elements, unsigned long count) | |
| { | |
| struct vchiq_io_copy_callback_context context; | |
| int status = 0; | |
| unsigned long i; | |
| size_t total_size = 0; | |
| context.element = elements; | |
| context.element_offset = 0; | |
| context.elements_to_go = count; | |
| for (i = 0; i < count; i++) { | |
| if (!elements[i].data && elements[i].size != 0) | |
| return -EFAULT; | |
| total_size += elements[i].size; | |
| } | |
| status = vchiq_queue_message(instance, handle, vchiq_ioc_copy_element_data, | |
| &context, total_size); | |
| if (status == -EINVAL) | |
| return -EIO; | |
| else if (status == -EAGAIN) | |
| return -EINTR; | |
| return 0; | |
| } | |
| static int vchiq_ioc_create_service(struct vchiq_instance *instance, | |
| struct vchiq_create_service *args) | |
| { | |
| struct user_service *user_service = NULL; | |
| struct vchiq_service *service; | |
| int status = 0; | |
| struct vchiq_service_params_kernel params; | |
| int srvstate; | |
| if (args->is_open && !instance->connected) | |
| return -ENOTCONN; | |
| user_service = kmalloc_obj(*user_service); | |
| if (!user_service) | |
| return -ENOMEM; | |
| if (args->is_open) { | |
| srvstate = VCHIQ_SRVSTATE_OPENING; | |
| } else { | |
| srvstate = instance->connected ? | |
| VCHIQ_SRVSTATE_LISTENING : VCHIQ_SRVSTATE_HIDDEN; | |
| } | |
| params = (struct vchiq_service_params_kernel) { | |
| .fourcc = args->params.fourcc, | |
| .callback = service_callback, | |
| .userdata = user_service, | |
| .version = args->params.version, | |
| .version_min = args->params.version_min, | |
| }; | |
| service = vchiq_add_service_internal(instance->state, ¶ms, | |
| srvstate, instance, | |
| user_service_free); | |
| if (!service) { | |
| kfree(user_service); | |
| return -EEXIST; | |
| } | |
| user_service->service = service; | |
| user_service->userdata = args->params.userdata; | |
| user_service->instance = instance; | |
| user_service->is_vchi = (args->is_vchi != 0); | |
| user_service->dequeue_pending = 0; | |
| user_service->close_pending = 0; | |
| user_service->message_available_pos = instance->completion_remove - 1; | |
| user_service->msg_insert = 0; | |
| user_service->msg_remove = 0; | |
| init_completion(&user_service->insert_event); | |
| init_completion(&user_service->remove_event); | |
| init_completion(&user_service->close_event); | |
| if (args->is_open) { | |
| status = vchiq_open_service_internal(service, instance->pid); | |
| if (status) { | |
| vchiq_remove_service(instance, service->handle); | |
| return (status == -EAGAIN) ? | |
| -EINTR : -EIO; | |
| } | |
| } | |
| args->handle = service->handle; | |
| return 0; | |
| } | |
| static int vchiq_ioc_dequeue_message(struct vchiq_instance *instance, | |
| struct vchiq_dequeue_message *args) | |
| { | |
| struct user_service *user_service; | |
| struct vchiq_service *service; | |
| struct vchiq_header *header; | |
| int ret; | |
| DEBUG_INITIALISE(instance->state->local); | |
| DEBUG_TRACE(DEQUEUE_MESSAGE_LINE); | |
| service = find_service_for_instance(instance, args->handle); | |
| if (!service) | |
| return -EINVAL; | |
| user_service = (struct user_service *)service->base.userdata; | |
| if (user_service->is_vchi == 0) { | |
| ret = -EINVAL; | |
| goto out; | |
| } | |
| spin_lock(&service->state->msg_queue_spinlock); | |
| if (user_service->msg_remove == user_service->msg_insert) { | |
| if (!args->blocking) { | |
| spin_unlock(&service->state->msg_queue_spinlock); | |
| DEBUG_TRACE(DEQUEUE_MESSAGE_LINE); | |
| ret = -EWOULDBLOCK; | |
| goto out; | |
| } | |
| user_service->dequeue_pending = 1; | |
| ret = 0; | |
| do { | |
| spin_unlock(&service->state->msg_queue_spinlock); | |
| DEBUG_TRACE(DEQUEUE_MESSAGE_LINE); | |
| if (wait_for_completion_interruptible(&user_service->insert_event)) { | |
| dev_dbg(service->state->dev, "arm: DEQUEUE_MESSAGE interrupted\n"); | |
| ret = -EINTR; | |
| break; | |
| } | |
| spin_lock(&service->state->msg_queue_spinlock); | |
| } while (user_service->msg_remove == user_service->msg_insert); | |
| if (ret) | |
| goto out; | |
| } | |
| if (WARN_ON_ONCE((int)(user_service->msg_insert - | |
| user_service->msg_remove) < 0)) { | |
| spin_unlock(&service->state->msg_queue_spinlock); | |
| ret = -EINVAL; | |
| goto out; | |
| } | |
| header = user_service->msg_queue[user_service->msg_remove & | |
| (MSG_QUEUE_SIZE - 1)]; | |
| user_service->msg_remove++; | |
| spin_unlock(&service->state->msg_queue_spinlock); | |
| complete(&user_service->remove_event); | |
| if (!header) { | |
| ret = -ENOTCONN; | |
| } else if (header->size <= args->bufsize) { | |
| /* Copy to user space if msgbuf is not NULL */ | |
| if (!args->buf || (copy_to_user(args->buf, header->data, header->size) == 0)) { | |
| ret = header->size; | |
| vchiq_release_message(instance, service->handle, header); | |
| } else { | |
| ret = -EFAULT; | |
| } | |
| } else { | |
| dev_err(service->state->dev, | |
| "arm: header %p: bufsize %x < size %x\n", | |
| header, args->bufsize, header->size); | |
| WARN(1, "invalid size\n"); | |
| ret = -EMSGSIZE; | |
| } | |
| DEBUG_TRACE(DEQUEUE_MESSAGE_LINE); | |
| out: | |
| vchiq_service_put(service); | |
| return ret; | |
| } | |
| static int vchiq_irq_queue_bulk_tx_rx(struct vchiq_instance *instance, | |
| struct vchiq_queue_bulk_transfer *args, | |
| enum vchiq_bulk_dir dir, | |
| enum vchiq_bulk_mode __user *mode) | |
| { | |
| struct vchiq_service *service; | |
| struct bulk_waiter_node *waiter = NULL, *iter; | |
| struct vchiq_bulk bulk_params = {}; | |
| int status = 0; | |
| int ret; | |
| service = find_service_for_instance(instance, args->handle); | |
| if (!service) | |
| return -EINVAL; | |
| if (args->mode == VCHIQ_BULK_MODE_BLOCKING) { | |
| waiter = kzalloc_obj(*waiter); | |
| if (!waiter) { | |
| ret = -ENOMEM; | |
| goto out; | |
| } | |
| bulk_params.uoffset = args->data; | |
| bulk_params.mode = args->mode; | |
| bulk_params.size = args->size; | |
| bulk_params.dir = dir; | |
| bulk_params.waiter = &waiter->bulk_waiter; | |
| status = vchiq_bulk_xfer_blocking(instance, args->handle, | |
| &bulk_params); | |
| } else if (args->mode == VCHIQ_BULK_MODE_WAITING) { | |
| mutex_lock(&instance->bulk_waiter_list_mutex); | |
| list_for_each_entry(iter, &instance->bulk_waiter_list, | |
| list) { | |
| if (iter->pid == current->pid) { | |
| list_del(&iter->list); | |
| waiter = iter; | |
| break; | |
| } | |
| } | |
| mutex_unlock(&instance->bulk_waiter_list_mutex); | |
| if (!waiter) { | |
| dev_err(service->state->dev, | |
| "arm: no bulk_waiter found for pid %d\n", current->pid); | |
| ret = -ESRCH; | |
| goto out; | |
| } | |
| dev_dbg(service->state->dev, "arm: found bulk_waiter %p for pid %d\n", | |
| waiter, current->pid); | |
| status = vchiq_bulk_xfer_waiting(instance, args->handle, | |
| &waiter->bulk_waiter); | |
| } else { | |
| bulk_params.uoffset = args->data; | |
| bulk_params.mode = args->mode; | |
| bulk_params.size = args->size; | |
| bulk_params.dir = dir; | |
| bulk_params.cb_userdata = args->userdata; | |
| status = vchiq_bulk_xfer_callback(instance, args->handle, | |
| &bulk_params); | |
| } | |
| if (!waiter) { | |
| ret = 0; | |
| goto out; | |
| } | |
| if ((status != -EAGAIN) || fatal_signal_pending(current) || | |
| !waiter->bulk_waiter.bulk) { | |
| if (waiter->bulk_waiter.bulk) { | |
| /* Cancel the signal when the transfer completes. */ | |
| spin_lock(&service->state->bulk_waiter_spinlock); | |
| waiter->bulk_waiter.bulk->waiter = NULL; | |
| spin_unlock(&service->state->bulk_waiter_spinlock); | |
| } | |
| kfree(waiter); | |
| ret = 0; | |
| } else { | |
| const enum vchiq_bulk_mode mode_waiting = | |
| VCHIQ_BULK_MODE_WAITING; | |
| waiter->pid = current->pid; | |
| mutex_lock(&instance->bulk_waiter_list_mutex); | |
| list_add(&waiter->list, &instance->bulk_waiter_list); | |
| mutex_unlock(&instance->bulk_waiter_list_mutex); | |
| dev_dbg(service->state->dev, "arm: saved bulk_waiter %p for pid %d\n", | |
| waiter, current->pid); | |
| ret = put_user(mode_waiting, mode); | |
| } | |
| out: | |
| vchiq_service_put(service); | |
| if (ret) | |
| return ret; | |
| else if (status == -EINVAL) | |
| return -EIO; | |
| else if (status == -EAGAIN) | |
| return -EINTR; | |
| return 0; | |
| } | |
| /* read a user pointer value from an array pointers in user space */ | |
| static inline int vchiq_get_user_ptr(void __user **buf, void __user *ubuf, int index) | |
| { | |
| int ret; | |
| if (in_compat_syscall()) { | |
| compat_uptr_t ptr32; | |
| compat_uptr_t __user *uptr = ubuf; | |
| ret = get_user(ptr32, uptr + index); | |
| if (ret) | |
| return ret; | |
| *buf = compat_ptr(ptr32); | |
| } else { | |
| uintptr_t ptr, __user *uptr = ubuf; | |
| ret = get_user(ptr, uptr + index); | |
| if (ret) | |
| return ret; | |
| *buf = (void __user *)ptr; | |
| } | |
| return 0; | |
| } | |
| struct vchiq_completion_data32 { | |
| enum vchiq_reason reason; | |
| compat_uptr_t header; | |
| compat_uptr_t service_userdata; | |
| compat_uptr_t cb_data; | |
| }; | |
| static int vchiq_put_completion(struct vchiq_completion_data __user *buf, | |
| struct vchiq_completion_data *completion, | |
| int index) | |
| { | |
| struct vchiq_completion_data32 __user *buf32 = (void __user *)buf; | |
| if (in_compat_syscall()) { | |
| struct vchiq_completion_data32 tmp = { | |
| .reason = completion->reason, | |
| .header = ptr_to_compat(completion->header), | |
| .service_userdata = ptr_to_compat(completion->service_userdata), | |
| .cb_data = ptr_to_compat(completion->cb_userdata), | |
| }; | |
| if (copy_to_user(&buf32[index], &tmp, sizeof(tmp))) | |
| return -EFAULT; | |
| } else { | |
| if (copy_to_user(&buf[index], completion, sizeof(*completion))) | |
| return -EFAULT; | |
| } | |
| return 0; | |
| } | |
| static int vchiq_ioc_await_completion(struct vchiq_instance *instance, | |
| struct vchiq_await_completion *args, | |
| int __user *msgbufcountp) | |
| { | |
| int msgbufcount; | |
| int remove; | |
| int ret; | |
| DEBUG_INITIALISE(instance->state->local); | |
| DEBUG_TRACE(AWAIT_COMPLETION_LINE); | |
| if (!instance->connected) | |
| return -ENOTCONN; | |
| mutex_lock(&instance->completion_mutex); | |
| DEBUG_TRACE(AWAIT_COMPLETION_LINE); | |
| while ((instance->completion_remove == instance->completion_insert) && !instance->closing) { | |
| int rc; | |
| DEBUG_TRACE(AWAIT_COMPLETION_LINE); | |
| mutex_unlock(&instance->completion_mutex); | |
| rc = wait_for_completion_interruptible(&instance->insert_event); | |
| mutex_lock(&instance->completion_mutex); | |
| if (rc) { | |
| DEBUG_TRACE(AWAIT_COMPLETION_LINE); | |
| dev_dbg(instance->state->dev, "arm: AWAIT_COMPLETION interrupted\n"); | |
| ret = -EINTR; | |
| goto out; | |
| } | |
| } | |
| DEBUG_TRACE(AWAIT_COMPLETION_LINE); | |
| msgbufcount = args->msgbufcount; | |
| remove = instance->completion_remove; | |
| for (ret = 0; ret < args->count; ret++) { | |
| struct vchiq_completion_data_kernel *completion; | |
| struct vchiq_completion_data user_completion; | |
| struct vchiq_service *service; | |
| struct user_service *user_service; | |
| struct vchiq_header *header; | |
| if (remove == instance->completion_insert) | |
| break; | |
| completion = &instance->completions[remove & (MAX_COMPLETIONS - 1)]; | |
| /* | |
| * A read memory barrier is needed to stop | |
| * prefetch of a stale completion record | |
| */ | |
| rmb(); | |
| service = completion->service_userdata; | |
| user_service = service->base.userdata; | |
| memset(&user_completion, 0, sizeof(user_completion)); | |
| user_completion = (struct vchiq_completion_data) { | |
| .reason = completion->reason, | |
| .service_userdata = user_service->userdata, | |
| }; | |
| header = completion->header; | |
| if (header) { | |
| void __user *msgbuf; | |
| int msglen; | |
| msglen = header->size + sizeof(struct vchiq_header); | |
| /* This must be a VCHIQ-style service */ | |
| if (args->msgbufsize < msglen) { | |
| dev_err(service->state->dev, | |
| "arm: header %p: msgbufsize %x < msglen %x\n", | |
| header, args->msgbufsize, msglen); | |
| WARN(1, "invalid message size\n"); | |
| if (ret == 0) | |
| ret = -EMSGSIZE; | |
| break; | |
| } | |
| if (msgbufcount <= 0) | |
| /* Stall here for lack of a buffer for the message. */ | |
| break; | |
| /* Get the pointer from user space */ | |
| msgbufcount--; | |
| if (vchiq_get_user_ptr(&msgbuf, args->msgbufs, | |
| msgbufcount)) { | |
| if (ret == 0) | |
| ret = -EFAULT; | |
| break; | |
| } | |
| /* Copy the message to user space */ | |
| if (copy_to_user(msgbuf, header, msglen)) { | |
| if (ret == 0) | |
| ret = -EFAULT; | |
| break; | |
| } | |
| /* Now it has been copied, the message can be released. */ | |
| vchiq_release_message(instance, service->handle, header); | |
| /* The completion must point to the msgbuf. */ | |
| user_completion.header = msgbuf; | |
| } | |
| if ((completion->reason == VCHIQ_SERVICE_CLOSED) && | |
| !instance->use_close_delivered) | |
| vchiq_service_put(service); | |
| user_completion.cb_userdata = completion->cb_userdata; | |
| if (vchiq_put_completion(args->buf, &user_completion, ret)) { | |
| if (ret == 0) | |
| ret = -EFAULT; | |
| break; | |
| } | |
| /* | |
| * Ensure that the above copy has completed | |
| * before advancing the remove pointer. | |
| */ | |
| mb(); | |
| remove++; | |
| instance->completion_remove = remove; | |
| } | |
| if (msgbufcount != args->msgbufcount) { | |
| if (put_user(msgbufcount, msgbufcountp)) | |
| ret = -EFAULT; | |
| } | |
| out: | |
| if (ret) | |
| complete(&instance->remove_event); | |
| mutex_unlock(&instance->completion_mutex); | |
| DEBUG_TRACE(AWAIT_COMPLETION_LINE); | |
| return ret; | |
| } | |
| static long | |
| vchiq_ioctl(struct file *file, unsigned int cmd, unsigned long arg) | |
| { | |
| struct vchiq_instance *instance = file->private_data; | |
| int status = 0; | |
| struct vchiq_service *service = NULL; | |
| long ret = 0; | |
| int i, rc; | |
| dev_dbg(instance->state->dev, "arm: instance %p, cmd %s, arg %lx\n", instance, | |
| ((_IOC_TYPE(cmd) == VCHIQ_IOC_MAGIC) && (_IOC_NR(cmd) <= VCHIQ_IOC_MAX)) ? | |
| ioctl_names[_IOC_NR(cmd)] : "<invalid>", arg); | |
| switch (cmd) { | |
| case VCHIQ_IOC_SHUTDOWN: | |
| if (!instance->connected) | |
| break; | |
| /* Remove all services */ | |
| i = 0; | |
| while ((service = next_service_by_instance(instance->state, | |
| instance, &i))) { | |
| status = vchiq_remove_service(instance, service->handle); | |
| vchiq_service_put(service); | |
| if (status) | |
| break; | |
| } | |
| service = NULL; | |
| if (!status) { | |
| /* Wake the completion thread and ask it to exit */ | |
| instance->closing = 1; | |
| complete(&instance->insert_event); | |
| } | |
| break; | |
| case VCHIQ_IOC_CONNECT: | |
| if (instance->connected) { | |
| ret = -EINVAL; | |
| break; | |
| } | |
| rc = mutex_lock_killable(&instance->state->mutex); | |
| if (rc) { | |
| dev_err(instance->state->dev, | |
| "arm: vchiq: connect: could not lock mutex for state %d: %d\n", | |
| instance->state->id, rc); | |
| ret = -EINTR; | |
| break; | |
| } | |
| status = vchiq_connect_internal(instance->state, instance); | |
| mutex_unlock(&instance->state->mutex); | |
| if (!status) | |
| instance->connected = 1; | |
| else | |
| dev_err(instance->state->dev, | |
| "arm: vchiq: could not connect: %d\n", status); | |
| break; | |
| case VCHIQ_IOC_CREATE_SERVICE: { | |
| struct vchiq_create_service __user *argp; | |
| struct vchiq_create_service args; | |
| argp = (void __user *)arg; | |
| if (copy_from_user(&args, argp, sizeof(args))) { | |
| ret = -EFAULT; | |
| break; | |
| } | |
| ret = vchiq_ioc_create_service(instance, &args); | |
| if (ret < 0) | |
| break; | |
| if (put_user(args.handle, &argp->handle)) { | |
| vchiq_remove_service(instance, args.handle); | |
| ret = -EFAULT; | |
| } | |
| } break; | |
| case VCHIQ_IOC_CLOSE_SERVICE: | |
| case VCHIQ_IOC_REMOVE_SERVICE: { | |
| unsigned int handle = (unsigned int)arg; | |
| struct user_service *user_service; | |
| service = find_service_for_instance(instance, handle); | |
| if (!service) { | |
| ret = -EINVAL; | |
| break; | |
| } | |
| user_service = service->base.userdata; | |
| /* | |
| * close_pending is false on first entry, and when the | |
| * wait in vchiq_close_service has been interrupted. | |
| */ | |
| if (!user_service->close_pending) { | |
| status = (cmd == VCHIQ_IOC_CLOSE_SERVICE) ? | |
| vchiq_close_service(instance, service->handle) : | |
| vchiq_remove_service(instance, service->handle); | |
| if (status) | |
| break; | |
| } | |
| /* | |
| * close_pending is true once the underlying service | |
| * has been closed until the client library calls the | |
| * CLOSE_DELIVERED ioctl, signalling close_event. | |
| */ | |
| if (user_service->close_pending && | |
| wait_for_completion_interruptible(&user_service->close_event)) | |
| status = -EAGAIN; | |
| break; | |
| } | |
| case VCHIQ_IOC_USE_SERVICE: | |
| case VCHIQ_IOC_RELEASE_SERVICE: { | |
| unsigned int handle = (unsigned int)arg; | |
| service = find_service_for_instance(instance, handle); | |
| if (service) { | |
| ret = (cmd == VCHIQ_IOC_USE_SERVICE) ? | |
| vchiq_use_service_internal(service) : | |
| vchiq_release_service_internal(service); | |
| if (ret) { | |
| dev_err(instance->state->dev, | |
| "suspend: cmd %s returned error %ld for service %p4cc:%03d\n", | |
| (cmd == VCHIQ_IOC_USE_SERVICE) ? | |
| "VCHIQ_IOC_USE_SERVICE" : | |
| "VCHIQ_IOC_RELEASE_SERVICE", | |
| ret, &service->base.fourcc, | |
| service->client_id); | |
| } | |
| } else { | |
| ret = -EINVAL; | |
| } | |
| } break; | |
| case VCHIQ_IOC_QUEUE_MESSAGE: { | |
| struct vchiq_queue_message args; | |
| if (copy_from_user(&args, (const void __user *)arg, | |
| sizeof(args))) { | |
| ret = -EFAULT; | |
| break; | |
| } | |
| service = find_service_for_instance(instance, args.handle); | |
| if (service && (args.count <= MAX_ELEMENTS)) { | |
| /* Copy elements into kernel space */ | |
| struct vchiq_element elements[MAX_ELEMENTS]; | |
| if (copy_from_user(elements, args.elements, | |
| args.count * sizeof(struct vchiq_element)) == 0) | |
| ret = vchiq_ioc_queue_message(instance, args.handle, elements, | |
| args.count); | |
| else | |
| ret = -EFAULT; | |
| } else { | |
| ret = -EINVAL; | |
| } | |
| } break; | |
| case VCHIQ_IOC_QUEUE_BULK_TRANSMIT: | |
| case VCHIQ_IOC_QUEUE_BULK_RECEIVE: { | |
| struct vchiq_queue_bulk_transfer args; | |
| struct vchiq_queue_bulk_transfer __user *argp; | |
| enum vchiq_bulk_dir dir = | |
| (cmd == VCHIQ_IOC_QUEUE_BULK_TRANSMIT) ? | |
| VCHIQ_BULK_TRANSMIT : VCHIQ_BULK_RECEIVE; | |
| argp = (void __user *)arg; | |
| if (copy_from_user(&args, argp, sizeof(args))) { | |
| ret = -EFAULT; | |
| break; | |
| } | |
| ret = vchiq_irq_queue_bulk_tx_rx(instance, &args, | |
| dir, &argp->mode); | |
| } break; | |
| case VCHIQ_IOC_AWAIT_COMPLETION: { | |
| struct vchiq_await_completion args; | |
| struct vchiq_await_completion __user *argp; | |
| argp = (void __user *)arg; | |
| if (copy_from_user(&args, argp, sizeof(args))) { | |
| ret = -EFAULT; | |
| break; | |
| } | |
| ret = vchiq_ioc_await_completion(instance, &args, | |
| &argp->msgbufcount); | |
| } break; | |
| case VCHIQ_IOC_DEQUEUE_MESSAGE: { | |
| struct vchiq_dequeue_message args; | |
| if (copy_from_user(&args, (const void __user *)arg, | |
| sizeof(args))) { | |
| ret = -EFAULT; | |
| break; | |
| } | |
| ret = vchiq_ioc_dequeue_message(instance, &args); | |
| } break; | |
| case VCHIQ_IOC_GET_CLIENT_ID: { | |
| unsigned int handle = (unsigned int)arg; | |
| ret = vchiq_get_client_id(instance, handle); | |
| } break; | |
| case VCHIQ_IOC_GET_CONFIG: { | |
| struct vchiq_get_config args; | |
| struct vchiq_config config; | |
| if (copy_from_user(&args, (const void __user *)arg, | |
| sizeof(args))) { | |
| ret = -EFAULT; | |
| break; | |
| } | |
| if (args.config_size > sizeof(config)) { | |
| ret = -EINVAL; | |
| break; | |
| } | |
| vchiq_get_config(&config); | |
| if (copy_to_user(args.pconfig, &config, args.config_size)) { | |
| ret = -EFAULT; | |
| break; | |
| } | |
| } break; | |
| case VCHIQ_IOC_SET_SERVICE_OPTION: { | |
| struct vchiq_set_service_option args; | |
| if (copy_from_user(&args, (const void __user *)arg, | |
| sizeof(args))) { | |
| ret = -EFAULT; | |
| break; | |
| } | |
| service = find_service_for_instance(instance, args.handle); | |
| if (!service) { | |
| ret = -EINVAL; | |
| break; | |
| } | |
| ret = vchiq_set_service_option(instance, args.handle, args.option, | |
| args.value); | |
| } break; | |
| case VCHIQ_IOC_LIB_VERSION: { | |
| unsigned int lib_version = (unsigned int)arg; | |
| if (lib_version < VCHIQ_VERSION_MIN) | |
| ret = -EINVAL; | |
| else if (lib_version >= VCHIQ_VERSION_CLOSE_DELIVERED) | |
| instance->use_close_delivered = 1; | |
| } break; | |
| case VCHIQ_IOC_CLOSE_DELIVERED: { | |
| unsigned int handle = (unsigned int)arg; | |
| service = find_closed_service_for_instance(instance, handle); | |
| if (service) { | |
| struct user_service *user_service = | |
| (struct user_service *)service->base.userdata; | |
| close_delivered(user_service); | |
| } else { | |
| ret = -EINVAL; | |
| } | |
| } break; | |
| default: | |
| ret = -ENOTTY; | |
| break; | |
| } | |
| if (service) | |
| vchiq_service_put(service); | |
| if (ret == 0) { | |
| if (status == -EINVAL) | |
| ret = -EIO; | |
| else if (status == -EAGAIN) | |
| ret = -EINTR; | |
| } | |
| if (!status && (ret < 0) && (ret != -EINTR) && (ret != -EWOULDBLOCK)) { | |
| dev_dbg(instance->state->dev, | |
| "arm: ioctl instance %p, cmd %s -> status %d, %ld\n", | |
| instance, (_IOC_NR(cmd) <= VCHIQ_IOC_MAX) ? | |
| ioctl_names[_IOC_NR(cmd)] : "<invalid>", status, ret); | |
| } else { | |
| dev_dbg(instance->state->dev, | |
| "arm: ioctl instance %p, cmd %s -> status %d\n, %ld\n", | |
| instance, (_IOC_NR(cmd) <= VCHIQ_IOC_MAX) ? | |
| ioctl_names[_IOC_NR(cmd)] : "<invalid>", status, ret); | |
| } | |
| return ret; | |
| } | |
| #if defined(CONFIG_COMPAT) | |
| struct vchiq_service_params32 { | |
| int fourcc; | |
| compat_uptr_t callback; | |
| compat_uptr_t userdata; | |
| short version; /* Increment for non-trivial changes */ | |
| short version_min; /* Update for incompatible changes */ | |
| }; | |
| struct vchiq_create_service32 { | |
| struct vchiq_service_params32 params; | |
| int is_open; | |
| int is_vchi; | |
| unsigned int handle; /* OUT */ | |
| }; | |
| #define VCHIQ_IOC_CREATE_SERVICE32 \ | |
| _IOWR(VCHIQ_IOC_MAGIC, 2, struct vchiq_create_service32) | |
| static long | |
| vchiq_compat_ioctl_create_service(struct file *file, unsigned int cmd, | |
| struct vchiq_create_service32 __user *ptrargs32) | |
| { | |
| struct vchiq_create_service args; | |
| struct vchiq_create_service32 args32; | |
| struct vchiq_instance *instance = file->private_data; | |
| long ret; | |
| if (copy_from_user(&args32, ptrargs32, sizeof(args32))) | |
| return -EFAULT; | |
| args = (struct vchiq_create_service) { | |
| .params = { | |
| .fourcc = args32.params.fourcc, | |
| .callback = compat_ptr(args32.params.callback), | |
| .userdata = compat_ptr(args32.params.userdata), | |
| .version = args32.params.version, | |
| .version_min = args32.params.version_min, | |
| }, | |
| .is_open = args32.is_open, | |
| .is_vchi = args32.is_vchi, | |
| .handle = args32.handle, | |
| }; | |
| ret = vchiq_ioc_create_service(instance, &args); | |
| if (ret < 0) | |
| return ret; | |
| if (put_user(args.handle, &ptrargs32->handle)) { | |
| vchiq_remove_service(instance, args.handle); | |
| return -EFAULT; | |
| } | |
| return 0; | |
| } | |
| struct vchiq_element32 { | |
| compat_uptr_t data; | |
| unsigned int size; | |
| }; | |
| struct vchiq_queue_message32 { | |
| unsigned int handle; | |
| unsigned int count; | |
| compat_uptr_t elements; | |
| }; | |
| #define VCHIQ_IOC_QUEUE_MESSAGE32 \ | |
| _IOW(VCHIQ_IOC_MAGIC, 4, struct vchiq_queue_message32) | |
| static long | |
| vchiq_compat_ioctl_queue_message(struct file *file, | |
| unsigned int cmd, | |
| struct vchiq_queue_message32 __user *arg) | |
| { | |
| struct vchiq_queue_message args; | |
| struct vchiq_queue_message32 args32; | |
| struct vchiq_service *service; | |
| struct vchiq_instance *instance = file->private_data; | |
| int ret; | |
| if (copy_from_user(&args32, arg, sizeof(args32))) | |
| return -EFAULT; | |
| args = (struct vchiq_queue_message) { | |
| .handle = args32.handle, | |
| .count = args32.count, | |
| .elements = compat_ptr(args32.elements), | |
| }; | |
| if (args32.count > MAX_ELEMENTS) | |
| return -EINVAL; | |
| service = find_service_for_instance(instance, args.handle); | |
| if (!service) | |
| return -EINVAL; | |
| if (args32.elements && args32.count) { | |
| struct vchiq_element32 element32[MAX_ELEMENTS]; | |
| struct vchiq_element elements[MAX_ELEMENTS]; | |
| unsigned int count; | |
| if (copy_from_user(&element32, args.elements, | |
| sizeof(element32))) { | |
| vchiq_service_put(service); | |
| return -EFAULT; | |
| } | |
| for (count = 0; count < args32.count; count++) { | |
| elements[count].data = | |
| compat_ptr(element32[count].data); | |
| elements[count].size = element32[count].size; | |
| } | |
| ret = vchiq_ioc_queue_message(instance, args.handle, elements, | |
| args.count); | |
| } else { | |
| ret = -EINVAL; | |
| } | |
| vchiq_service_put(service); | |
| return ret; | |
| } | |
| struct vchiq_queue_bulk_transfer32 { | |
| unsigned int handle; | |
| compat_uptr_t data; | |
| unsigned int size; | |
| compat_uptr_t userdata; | |
| enum vchiq_bulk_mode mode; | |
| }; | |
| #define VCHIQ_IOC_QUEUE_BULK_TRANSMIT32 \ | |
| _IOWR(VCHIQ_IOC_MAGIC, 5, struct vchiq_queue_bulk_transfer32) | |
| #define VCHIQ_IOC_QUEUE_BULK_RECEIVE32 \ | |
| _IOWR(VCHIQ_IOC_MAGIC, 6, struct vchiq_queue_bulk_transfer32) | |
| static long | |
| vchiq_compat_ioctl_queue_bulk(struct file *file, | |
| unsigned int cmd, | |
| struct vchiq_queue_bulk_transfer32 __user *argp) | |
| { | |
| struct vchiq_queue_bulk_transfer32 args32; | |
| struct vchiq_queue_bulk_transfer args; | |
| enum vchiq_bulk_dir dir = (cmd == VCHIQ_IOC_QUEUE_BULK_TRANSMIT32) ? | |
| VCHIQ_BULK_TRANSMIT : VCHIQ_BULK_RECEIVE; | |
| if (copy_from_user(&args32, argp, sizeof(args32))) | |
| return -EFAULT; | |
| args = (struct vchiq_queue_bulk_transfer) { | |
| .handle = args32.handle, | |
| .data = compat_ptr(args32.data), | |
| .size = args32.size, | |
| .userdata = compat_ptr(args32.userdata), | |
| .mode = args32.mode, | |
| }; | |
| return vchiq_irq_queue_bulk_tx_rx(file->private_data, &args, | |
| dir, &argp->mode); | |
| } | |
| struct vchiq_await_completion32 { | |
| unsigned int count; | |
| compat_uptr_t buf; | |
| unsigned int msgbufsize; | |
| unsigned int msgbufcount; /* IN/OUT */ | |
| compat_uptr_t msgbufs; | |
| }; | |
| #define VCHIQ_IOC_AWAIT_COMPLETION32 \ | |
| _IOWR(VCHIQ_IOC_MAGIC, 7, struct vchiq_await_completion32) | |
| static long | |
| vchiq_compat_ioctl_await_completion(struct file *file, | |
| unsigned int cmd, | |
| struct vchiq_await_completion32 __user *argp) | |
| { | |
| struct vchiq_await_completion args; | |
| struct vchiq_await_completion32 args32; | |
| if (copy_from_user(&args32, argp, sizeof(args32))) | |
| return -EFAULT; | |
| args = (struct vchiq_await_completion) { | |
| .count = args32.count, | |
| .buf = compat_ptr(args32.buf), | |
| .msgbufsize = args32.msgbufsize, | |
| .msgbufcount = args32.msgbufcount, | |
| .msgbufs = compat_ptr(args32.msgbufs), | |
| }; | |
| return vchiq_ioc_await_completion(file->private_data, &args, | |
| &argp->msgbufcount); | |
| } | |
| struct vchiq_dequeue_message32 { | |
| unsigned int handle; | |
| int blocking; | |
| unsigned int bufsize; | |
| compat_uptr_t buf; | |
| }; | |
| #define VCHIQ_IOC_DEQUEUE_MESSAGE32 \ | |
| _IOWR(VCHIQ_IOC_MAGIC, 8, struct vchiq_dequeue_message32) | |
| static long | |
| vchiq_compat_ioctl_dequeue_message(struct file *file, | |
| unsigned int cmd, | |
| struct vchiq_dequeue_message32 __user *arg) | |
| { | |
| struct vchiq_dequeue_message32 args32; | |
| struct vchiq_dequeue_message args; | |
| if (copy_from_user(&args32, arg, sizeof(args32))) | |
| return -EFAULT; | |
| args = (struct vchiq_dequeue_message) { | |
| .handle = args32.handle, | |
| .blocking = args32.blocking, | |
| .bufsize = args32.bufsize, | |
| .buf = compat_ptr(args32.buf), | |
| }; | |
| return vchiq_ioc_dequeue_message(file->private_data, &args); | |
| } | |
| struct vchiq_get_config32 { | |
| unsigned int config_size; | |
| compat_uptr_t pconfig; | |
| }; | |
| #define VCHIQ_IOC_GET_CONFIG32 \ | |
| _IOWR(VCHIQ_IOC_MAGIC, 10, struct vchiq_get_config32) | |
| static long | |
| vchiq_compat_ioctl_get_config(struct file *file, | |
| unsigned int cmd, | |
| struct vchiq_get_config32 __user *arg) | |
| { | |
| struct vchiq_get_config32 args32; | |
| struct vchiq_config config; | |
| void __user *ptr; | |
| if (copy_from_user(&args32, arg, sizeof(args32))) | |
| return -EFAULT; | |
| if (args32.config_size > sizeof(config)) | |
| return -EINVAL; | |
| vchiq_get_config(&config); | |
| ptr = compat_ptr(args32.pconfig); | |
| if (copy_to_user(ptr, &config, args32.config_size)) | |
| return -EFAULT; | |
| return 0; | |
| } | |
| static long | |
| vchiq_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg) | |
| { | |
| void __user *argp = compat_ptr(arg); | |
| switch (cmd) { | |
| case VCHIQ_IOC_CREATE_SERVICE32: | |
| return vchiq_compat_ioctl_create_service(file, cmd, argp); | |
| case VCHIQ_IOC_QUEUE_MESSAGE32: | |
| return vchiq_compat_ioctl_queue_message(file, cmd, argp); | |
| case VCHIQ_IOC_QUEUE_BULK_TRANSMIT32: | |
| case VCHIQ_IOC_QUEUE_BULK_RECEIVE32: | |
| return vchiq_compat_ioctl_queue_bulk(file, cmd, argp); | |
| case VCHIQ_IOC_AWAIT_COMPLETION32: | |
| return vchiq_compat_ioctl_await_completion(file, cmd, argp); | |
| case VCHIQ_IOC_DEQUEUE_MESSAGE32: | |
| return vchiq_compat_ioctl_dequeue_message(file, cmd, argp); | |
| case VCHIQ_IOC_GET_CONFIG32: | |
| return vchiq_compat_ioctl_get_config(file, cmd, argp); | |
| default: | |
| return vchiq_ioctl(file, cmd, (unsigned long)argp); | |
| } | |
| } | |
| #endif | |
| static int vchiq_open(struct inode *inode, struct file *file) | |
| { | |
| struct miscdevice *vchiq_miscdev = file->private_data; | |
| struct vchiq_drv_mgmt *mgmt = dev_get_drvdata(vchiq_miscdev->parent); | |
| struct vchiq_state *state = &mgmt->state; | |
| struct vchiq_instance *instance; | |
| dev_dbg(state->dev, "arm: vchiq open\n"); | |
| if (!vchiq_remote_initialised(state)) { | |
| dev_dbg(state->dev, "arm: vchiq has no connection to VideoCore\n"); | |
| return -ENOTCONN; | |
| } | |
| instance = kzalloc_obj(*instance); | |
| if (!instance) | |
| return -ENOMEM; | |
| instance->state = state; | |
| instance->pid = current->tgid; | |
| vchiq_debugfs_add_instance(instance); | |
| init_completion(&instance->insert_event); | |
| init_completion(&instance->remove_event); | |
| mutex_init(&instance->completion_mutex); | |
| mutex_init(&instance->bulk_waiter_list_mutex); | |
| INIT_LIST_HEAD(&instance->bulk_waiter_list); | |
| file->private_data = instance; | |
| return 0; | |
| } | |
| static int vchiq_release(struct inode *inode, struct file *file) | |
| { | |
| struct vchiq_instance *instance = file->private_data; | |
| struct vchiq_state *state = instance->state; | |
| struct vchiq_service *service; | |
| int ret = 0; | |
| int i; | |
| dev_dbg(state->dev, "arm: instance=%p\n", instance); | |
| if (!vchiq_remote_initialised(state)) { | |
| ret = -EPERM; | |
| goto out; | |
| } | |
| /* Ensure videocore is awake to allow termination. */ | |
| vchiq_use_internal(instance->state, NULL, USE_TYPE_VCHIQ); | |
| mutex_lock(&instance->completion_mutex); | |
| /* Wake the completion thread and ask it to exit */ | |
| instance->closing = 1; | |
| complete(&instance->insert_event); | |
| mutex_unlock(&instance->completion_mutex); | |
| /* Wake the slot handler if the completion queue is full. */ | |
| complete(&instance->remove_event); | |
| /* Mark all services for termination... */ | |
| i = 0; | |
| while ((service = next_service_by_instance(state, instance, &i))) { | |
| struct user_service *user_service = service->base.userdata; | |
| /* Wake the slot handler if the msg queue is full. */ | |
| complete(&user_service->remove_event); | |
| vchiq_terminate_service_internal(service); | |
| vchiq_service_put(service); | |
| } | |
| /* ...and wait for them to die */ | |
| i = 0; | |
| while ((service = next_service_by_instance(state, instance, &i))) { | |
| struct user_service *user_service = service->base.userdata; | |
| wait_for_completion(&service->remove_event); | |
| if (WARN_ON(service->srvstate != VCHIQ_SRVSTATE_FREE)) { | |
| vchiq_service_put(service); | |
| break; | |
| } | |
| spin_lock(&service->state->msg_queue_spinlock); | |
| while (user_service->msg_remove != user_service->msg_insert) { | |
| struct vchiq_header *header; | |
| int m = user_service->msg_remove & (MSG_QUEUE_SIZE - 1); | |
| header = user_service->msg_queue[m]; | |
| user_service->msg_remove++; | |
| spin_unlock(&service->state->msg_queue_spinlock); | |
| if (header) | |
| vchiq_release_message(instance, service->handle, header); | |
| spin_lock(&service->state->msg_queue_spinlock); | |
| } | |
| spin_unlock(&service->state->msg_queue_spinlock); | |
| vchiq_service_put(service); | |
| } | |
| /* Release any closed services */ | |
| while (instance->completion_remove != instance->completion_insert) { | |
| struct vchiq_completion_data_kernel *completion; | |
| struct vchiq_service *service; | |
| completion = &instance->completions[instance->completion_remove | |
| & (MAX_COMPLETIONS - 1)]; | |
| service = completion->service_userdata; | |
| if (completion->reason == VCHIQ_SERVICE_CLOSED) { | |
| struct user_service *user_service = | |
| service->base.userdata; | |
| /* Wake any blocked user-thread */ | |
| if (instance->use_close_delivered) | |
| complete(&user_service->close_event); | |
| vchiq_service_put(service); | |
| } | |
| instance->completion_remove++; | |
| } | |
| /* Release the PEER service count. */ | |
| vchiq_release_internal(instance->state, NULL); | |
| free_bulk_waiter(instance); | |
| vchiq_debugfs_remove_instance(instance); | |
| kfree(instance); | |
| file->private_data = NULL; | |
| out: | |
| return ret; | |
| } | |
| static const struct file_operations | |
| vchiq_fops = { | |
| .owner = THIS_MODULE, | |
| .unlocked_ioctl = vchiq_ioctl, | |
| #if defined(CONFIG_COMPAT) | |
| .compat_ioctl = vchiq_compat_ioctl, | |
| #endif | |
| .open = vchiq_open, | |
| .release = vchiq_release, | |
| }; | |
| static struct miscdevice vchiq_miscdev = { | |
| .fops = &vchiq_fops, | |
| .minor = MISC_DYNAMIC_MINOR, | |
| .name = "vchiq", | |
| }; | |
| /** | |
| * vchiq_register_chrdev - Register the char driver for vchiq | |
| * and create the necessary class and | |
| * device files in userspace. | |
| * @parent: The parent of the char device. | |
| * | |
| * Returns 0 on success else returns the error code. | |
| */ | |
| int vchiq_register_chrdev(struct device *parent) | |
| { | |
| vchiq_miscdev.parent = parent; | |
| return misc_register(&vchiq_miscdev); | |
| } | |
| /** | |
| * vchiq_deregister_chrdev - Deregister and cleanup the vchiq char | |
| * driver and device files | |
| */ | |
| void vchiq_deregister_chrdev(void) | |
| { | |
| misc_deregister(&vchiq_miscdev); | |
| } |