| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * drivers/android/staging/vsoc.c | |
| * | |
| * Android Virtual System on a Chip (VSoC) driver | |
| * | |
| * Copyright (C) 2017 Google, Inc. | |
| * | |
| * Author: ghartman@google.com | |
| * | |
| * Based on drivers/char/kvm_ivshmem.c - driver for KVM Inter-VM shared memory | |
| * Copyright 2009 Cam Macdonell <cam@cs.ualberta.ca> | |
| * | |
| * Based on cirrusfb.c and 8139cp.c: | |
| * Copyright 1999-2001 Jeff Garzik | |
| * Copyright 2001-2004 Jeff Garzik | |
| */ | |
| #include <linux/dma-mapping.h> | |
| #include <linux/freezer.h> | |
| #include <linux/futex.h> | |
| #include <linux/init.h> | |
| #include <linux/kernel.h> | |
| #include <linux/module.h> | |
| #include <linux/mutex.h> | |
| #include <linux/pci.h> | |
| #include <linux/proc_fs.h> | |
| #include <linux/sched.h> | |
| #include <linux/syscalls.h> | |
| #include <linux/uaccess.h> | |
| #include <linux/interrupt.h> | |
| #include <linux/mutex.h> | |
| #include <linux/cdev.h> | |
| #include <linux/file.h> | |
| #include "uapi/vsoc_shm.h" | |
| #define VSOC_DEV_NAME "vsoc" | |
| /* | |
| * Description of the ivshmem-doorbell PCI device used by QEmu. These | |
| * constants follow docs/specs/ivshmem-spec.txt, which can be found in | |
| * the QEmu repository. This was last reconciled with the version that | |
| * came out with 2.8 | |
| */ | |
| /* | |
| * These constants are determined KVM Inter-VM shared memory device | |
| * register offsets | |
| */ | |
| enum { | |
| INTR_MASK = 0x00, /* Interrupt Mask */ | |
| INTR_STATUS = 0x04, /* Interrupt Status */ | |
| IV_POSITION = 0x08, /* VM ID */ | |
| DOORBELL = 0x0c, /* Doorbell */ | |
| }; | |
| static const int REGISTER_BAR; /* Equal to 0 */ | |
| static const int MAX_REGISTER_BAR_LEN = 0x100; | |
| /* | |
| * The MSI-x BAR is not used directly. | |
| * | |
| * static const int MSI_X_BAR = 1; | |
| */ | |
| static const int SHARED_MEMORY_BAR = 2; | |
| struct vsoc_region_data { | |
| char name[VSOC_DEVICE_NAME_SZ + 1]; | |
| wait_queue_head_t interrupt_wait_queue; | |
| /* TODO(b/73664181): Use multiple futex wait queues */ | |
| wait_queue_head_t futex_wait_queue; | |
| /* Flag indicating that an interrupt has been signalled by the host. */ | |
| atomic_t *incoming_signalled; | |
| /* Flag indicating the guest has signalled the host. */ | |
| atomic_t *outgoing_signalled; | |
| bool irq_requested; | |
| bool device_created; | |
| }; | |
| struct vsoc_device { | |
| /* Kernel virtual address of REGISTER_BAR. */ | |
| void __iomem *regs; | |
| /* Physical address of SHARED_MEMORY_BAR. */ | |
| phys_addr_t shm_phys_start; | |
| /* Kernel virtual address of SHARED_MEMORY_BAR. */ | |
| void __iomem *kernel_mapped_shm; | |
| /* Size of the entire shared memory window in bytes. */ | |
| size_t shm_size; | |
| /* | |
| * Pointer to the virtual address of the shared memory layout structure. | |
| * This is probably identical to kernel_mapped_shm, but saving this | |
| * here saves a lot of annoying casts. | |
| */ | |
| struct vsoc_shm_layout_descriptor *layout; | |
| /* | |
| * Points to a table of region descriptors in the kernel's virtual | |
| * address space. Calculated from | |
| * vsoc_shm_layout_descriptor.vsoc_region_desc_offset | |
| */ | |
| struct vsoc_device_region *regions; | |
| /* Head of a list of permissions that have been granted. */ | |
| struct list_head permissions; | |
| struct pci_dev *dev; | |
| /* Per-region (and therefore per-interrupt) information. */ | |
| struct vsoc_region_data *regions_data; | |
| /* | |
| * Table of msi-x entries. This has to be separated from struct | |
| * vsoc_region_data because the kernel deals with them as an array. | |
| */ | |
| struct msix_entry *msix_entries; | |
| /* Mutex that protectes the permission list */ | |
| struct mutex mtx; | |
| /* Major number assigned by the kernel */ | |
| int major; | |
| /* Character device assigned by the kernel */ | |
| struct cdev cdev; | |
| /* Device class assigned by the kernel */ | |
| struct class *class; | |
| /* | |
| * Flags that indicate what we've initialized. These are used to do an | |
| * orderly cleanup of the device. | |
| */ | |
| bool enabled_device; | |
| bool requested_regions; | |
| bool cdev_added; | |
| bool class_added; | |
| bool msix_enabled; | |
| }; | |
| static struct vsoc_device vsoc_dev; | |
| /* | |
| * TODO(ghartman): Add a /sys filesystem entry that summarizes the permissions. | |
| */ | |
| struct fd_scoped_permission_node { | |
| struct fd_scoped_permission permission; | |
| struct list_head list; | |
| }; | |
| struct vsoc_private_data { | |
| struct fd_scoped_permission_node *fd_scoped_permission_node; | |
| }; | |
| static long vsoc_ioctl(struct file *, unsigned int, unsigned long); | |
| static int vsoc_mmap(struct file *, struct vm_area_struct *); | |
| static int vsoc_open(struct inode *, struct file *); | |
| static int vsoc_release(struct inode *, struct file *); | |
| static ssize_t vsoc_read(struct file *, char __user *, size_t, loff_t *); | |
| static ssize_t vsoc_write(struct file *, const char __user *, size_t, loff_t *); | |
| static loff_t vsoc_lseek(struct file *filp, loff_t offset, int origin); | |
| static int | |
| do_create_fd_scoped_permission(struct vsoc_device_region *region_p, | |
| struct fd_scoped_permission_node *np, | |
| struct fd_scoped_permission_arg __user *arg); | |
| static void | |
| do_destroy_fd_scoped_permission(struct vsoc_device_region *owner_region_p, | |
| struct fd_scoped_permission *perm); | |
| static long do_vsoc_describe_region(struct file *, | |
| struct vsoc_device_region __user *); | |
| static ssize_t vsoc_get_area(struct file *filp, __u32 *perm_off); | |
| /** | |
| * Validate arguments on entry points to the driver. | |
| */ | |
| inline int vsoc_validate_inode(struct inode *inode) | |
| { | |
| if (iminor(inode) >= vsoc_dev.layout->region_count) { | |
| dev_err(&vsoc_dev.dev->dev, | |
| "describe_region: invalid region %d\n", iminor(inode)); | |
| return -ENODEV; | |
| } | |
| return 0; | |
| } | |
| inline int vsoc_validate_filep(struct file *filp) | |
| { | |
| int ret = vsoc_validate_inode(file_inode(filp)); | |
| if (ret) | |
| return ret; | |
| if (!filp->private_data) { | |
| dev_err(&vsoc_dev.dev->dev, | |
| "No private data on fd, region %d\n", | |
| iminor(file_inode(filp))); | |
| return -EBADFD; | |
| } | |
| return 0; | |
| } | |
| /* Converts from shared memory offset to virtual address */ | |
| static inline void *shm_off_to_virtual_addr(__u32 offset) | |
| { | |
| return (void __force *)vsoc_dev.kernel_mapped_shm + offset; | |
| } | |
| /* Converts from shared memory offset to physical address */ | |
| static inline phys_addr_t shm_off_to_phys_addr(__u32 offset) | |
| { | |
| return vsoc_dev.shm_phys_start + offset; | |
| } | |
| /** | |
| * Convenience functions to obtain the region from the inode or file. | |
| * Dangerous to call before validating the inode/file. | |
| */ | |
| static | |
| inline struct vsoc_device_region *vsoc_region_from_inode(struct inode *inode) | |
| { | |
| return &vsoc_dev.regions[iminor(inode)]; | |
| } | |
| static | |
| inline struct vsoc_device_region *vsoc_region_from_filep(struct file *inode) | |
| { | |
| return vsoc_region_from_inode(file_inode(inode)); | |
| } | |
| static inline uint32_t vsoc_device_region_size(struct vsoc_device_region *r) | |
| { | |
| return r->region_end_offset - r->region_begin_offset; | |
| } | |
| static const struct file_operations vsoc_ops = { | |
| .owner = THIS_MODULE, | |
| .open = vsoc_open, | |
| .mmap = vsoc_mmap, | |
| .read = vsoc_read, | |
| .unlocked_ioctl = vsoc_ioctl, | |
| .compat_ioctl = vsoc_ioctl, | |
| .write = vsoc_write, | |
| .llseek = vsoc_lseek, | |
| .release = vsoc_release, | |
| }; | |
| static struct pci_device_id vsoc_id_table[] = { | |
| {0x1af4, 0x1110, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0}, | |
| {0}, | |
| }; | |
| MODULE_DEVICE_TABLE(pci, vsoc_id_table); | |
| static void vsoc_remove_device(struct pci_dev *pdev); | |
| static int vsoc_probe_device(struct pci_dev *pdev, | |
| const struct pci_device_id *ent); | |
| static struct pci_driver vsoc_pci_driver = { | |
| .name = "vsoc", | |
| .id_table = vsoc_id_table, | |
| .probe = vsoc_probe_device, | |
| .remove = vsoc_remove_device, | |
| }; | |
| static int | |
| do_create_fd_scoped_permission(struct vsoc_device_region *region_p, | |
| struct fd_scoped_permission_node *np, | |
| struct fd_scoped_permission_arg __user *arg) | |
| { | |
| struct file *managed_filp; | |
| s32 managed_fd; | |
| atomic_t *owner_ptr = NULL; | |
| struct vsoc_device_region *managed_region_p; | |
| if (copy_from_user(&np->permission, &arg->perm, sizeof(*np)) || | |
| copy_from_user(&managed_fd, | |
| &arg->managed_region_fd, sizeof(managed_fd))) { | |
| return -EFAULT; | |
| } | |
| managed_filp = fdget(managed_fd).file; | |
| /* Check that it's a valid fd, */ | |
| if (!managed_filp || vsoc_validate_filep(managed_filp)) | |
| return -EPERM; | |
| /* EEXIST if the given fd already has a permission. */ | |
| if (((struct vsoc_private_data *)managed_filp->private_data)-> | |
| fd_scoped_permission_node) | |
| return -EEXIST; | |
| managed_region_p = vsoc_region_from_filep(managed_filp); | |
| /* Check that the provided region is managed by this one */ | |
| if (&vsoc_dev.regions[managed_region_p->managed_by] != region_p) | |
| return -EPERM; | |
| /* The area must be well formed and have non-zero size */ | |
| if (np->permission.begin_offset >= np->permission.end_offset) | |
| return -EINVAL; | |
| /* The area must fit in the memory window */ | |
| if (np->permission.end_offset > | |
| vsoc_device_region_size(managed_region_p)) | |
| return -ERANGE; | |
| /* The area must be in the region data section */ | |
| if (np->permission.begin_offset < | |
| managed_region_p->offset_of_region_data) | |
| return -ERANGE; | |
| /* The area must be page aligned */ | |
| if (!PAGE_ALIGNED(np->permission.begin_offset) || | |
| !PAGE_ALIGNED(np->permission.end_offset)) | |
| return -EINVAL; | |
| /* Owner offset must be naturally aligned in the window */ | |
| if (np->permission.owner_offset & | |
| (sizeof(np->permission.owner_offset) - 1)) | |
| return -EINVAL; | |
| /* The owner flag must reside in the owner memory */ | |
| if (np->permission.owner_offset + sizeof(np->permission.owner_offset) > | |
| vsoc_device_region_size(region_p)) | |
| return -ERANGE; | |
| /* The owner flag must reside in the data section */ | |
| if (np->permission.owner_offset < region_p->offset_of_region_data) | |
| return -EINVAL; | |
| /* The owner value must change to claim the memory */ | |
| if (np->permission.owned_value == VSOC_REGION_FREE) | |
| return -EINVAL; | |
| owner_ptr = | |
| (atomic_t *)shm_off_to_virtual_addr(region_p->region_begin_offset + | |
| np->permission.owner_offset); | |
| /* We've already verified that this is in the shared memory window, so | |
| * it should be safe to write to this address. | |
| */ | |
| if (atomic_cmpxchg(owner_ptr, | |
| VSOC_REGION_FREE, | |
| np->permission.owned_value) != VSOC_REGION_FREE) { | |
| return -EBUSY; | |
| } | |
| ((struct vsoc_private_data *)managed_filp->private_data)-> | |
| fd_scoped_permission_node = np; | |
| /* The file offset needs to be adjusted if the calling | |
| * process did any read/write operations on the fd | |
| * before creating the permission. | |
| */ | |
| if (managed_filp->f_pos) { | |
| if (managed_filp->f_pos > np->permission.end_offset) { | |
| /* If the offset is beyond the permission end, set it | |
| * to the end. | |
| */ | |
| managed_filp->f_pos = np->permission.end_offset; | |
| } else { | |
| /* If the offset is within the permission interval | |
| * keep it there otherwise reset it to zero. | |
| */ | |
| if (managed_filp->f_pos < np->permission.begin_offset) { | |
| managed_filp->f_pos = 0; | |
| } else { | |
| managed_filp->f_pos -= | |
| np->permission.begin_offset; | |
| } | |
| } | |
| } | |
| return 0; | |
| } | |
| static void | |
| do_destroy_fd_scoped_permission_node(struct vsoc_device_region *owner_region_p, | |
| struct fd_scoped_permission_node *node) | |
| { | |
| if (node) { | |
| do_destroy_fd_scoped_permission(owner_region_p, | |
| &node->permission); | |
| mutex_lock(&vsoc_dev.mtx); | |
| list_del(&node->list); | |
| mutex_unlock(&vsoc_dev.mtx); | |
| kfree(node); | |
| } | |
| } | |
| static void | |
| do_destroy_fd_scoped_permission(struct vsoc_device_region *owner_region_p, | |
| struct fd_scoped_permission *perm) | |
| { | |
| atomic_t *owner_ptr = NULL; | |
| int prev = 0; | |
| if (!perm) | |
| return; | |
| owner_ptr = (atomic_t *)shm_off_to_virtual_addr | |
| (owner_region_p->region_begin_offset + perm->owner_offset); | |
| prev = atomic_xchg(owner_ptr, VSOC_REGION_FREE); | |
| if (prev != perm->owned_value) | |
| dev_err(&vsoc_dev.dev->dev, | |
| "%x-%x: owner (%s) %x: expected to be %x was %x", | |
| perm->begin_offset, perm->end_offset, | |
| owner_region_p->device_name, perm->owner_offset, | |
| perm->owned_value, prev); | |
| } | |
| static long do_vsoc_describe_region(struct file *filp, | |
| struct vsoc_device_region __user *dest) | |
| { | |
| struct vsoc_device_region *region_p; | |
| int retval = vsoc_validate_filep(filp); | |
| if (retval) | |
| return retval; | |
| region_p = vsoc_region_from_filep(filp); | |
| if (copy_to_user(dest, region_p, sizeof(*region_p))) | |
| return -EFAULT; | |
| return 0; | |
| } | |
| /** | |
| * Implements the inner logic of cond_wait. Copies to and from userspace are | |
| * done in the helper function below. | |
| */ | |
| static int handle_vsoc_cond_wait(struct file *filp, struct vsoc_cond_wait *arg) | |
| { | |
| DEFINE_WAIT(wait); | |
| u32 region_number = iminor(file_inode(filp)); | |
| struct vsoc_region_data *data = vsoc_dev.regions_data + region_number; | |
| struct hrtimer_sleeper timeout, *to = NULL; | |
| int ret = 0; | |
| struct vsoc_device_region *region_p = vsoc_region_from_filep(filp); | |
| atomic_t *address = NULL; | |
| struct timespec ts; | |
| /* Ensure that the offset is aligned */ | |
| if (arg->offset & (sizeof(uint32_t) - 1)) | |
| return -EADDRNOTAVAIL; | |
| /* Ensure that the offset is within shared memory */ | |
| if (((uint64_t)arg->offset) + region_p->region_begin_offset + | |
| sizeof(uint32_t) > region_p->region_end_offset) | |
| return -E2BIG; | |
| address = shm_off_to_virtual_addr(region_p->region_begin_offset + | |
| arg->offset); | |
| /* Ensure that the type of wait is valid */ | |
| switch (arg->wait_type) { | |
| case VSOC_WAIT_IF_EQUAL: | |
| break; | |
| case VSOC_WAIT_IF_EQUAL_TIMEOUT: | |
| to = &timeout; | |
| break; | |
| default: | |
| return -EINVAL; | |
| } | |
| if (to) { | |
| /* Copy the user-supplied timesec into the kernel structure. | |
| * We do things this way to flatten differences between 32 bit | |
| * and 64 bit timespecs. | |
| */ | |
| ts.tv_sec = arg->wake_time_sec; | |
| ts.tv_nsec = arg->wake_time_nsec; | |
| if (!timespec_valid(&ts)) | |
| return -EINVAL; | |
| hrtimer_init_on_stack(&to->timer, CLOCK_MONOTONIC, | |
| HRTIMER_MODE_ABS); | |
| hrtimer_set_expires_range_ns(&to->timer, timespec_to_ktime(ts), | |
| current->timer_slack_ns); | |
| hrtimer_init_sleeper(to, current); | |
| } | |
| while (1) { | |
| prepare_to_wait(&data->futex_wait_queue, &wait, | |
| TASK_INTERRUPTIBLE); | |
| /* | |
| * Check the sentinel value after prepare_to_wait. If the value | |
| * changes after this check the writer will call signal, | |
| * changing the task state from INTERRUPTIBLE to RUNNING. That | |
| * will ensure that schedule() will eventually schedule this | |
| * task. | |
| */ | |
| if (atomic_read(address) != arg->value) { | |
| ret = 0; | |
| break; | |
| } | |
| if (to) { | |
| hrtimer_start_expires(&to->timer, HRTIMER_MODE_ABS); | |
| if (likely(to->task)) | |
| freezable_schedule(); | |
| hrtimer_cancel(&to->timer); | |
| if (!to->task) { | |
| ret = -ETIMEDOUT; | |
| break; | |
| } | |
| } else { | |
| freezable_schedule(); | |
| } | |
| /* Count the number of times that we woke up. This is useful | |
| * for unit testing. | |
| */ | |
| ++arg->wakes; | |
| if (signal_pending(current)) { | |
| ret = -EINTR; | |
| break; | |
| } | |
| } | |
| finish_wait(&data->futex_wait_queue, &wait); | |
| if (to) | |
| destroy_hrtimer_on_stack(&to->timer); | |
| return ret; | |
| } | |
| /** | |
| * Handles the details of copying from/to userspace to ensure that the copies | |
| * happen on all of the return paths of cond_wait. | |
| */ | |
| static int do_vsoc_cond_wait(struct file *filp, | |
| struct vsoc_cond_wait __user *untrusted_in) | |
| { | |
| struct vsoc_cond_wait arg; | |
| int rval = 0; | |
| if (copy_from_user(&arg, untrusted_in, sizeof(arg))) | |
| return -EFAULT; | |
| /* wakes is an out parameter. Initialize it to something sensible. */ | |
| arg.wakes = 0; | |
| rval = handle_vsoc_cond_wait(filp, &arg); | |
| if (copy_to_user(untrusted_in, &arg, sizeof(arg))) | |
| return -EFAULT; | |
| return rval; | |
| } | |
| static int do_vsoc_cond_wake(struct file *filp, uint32_t offset) | |
| { | |
| struct vsoc_device_region *region_p = vsoc_region_from_filep(filp); | |
| u32 region_number = iminor(file_inode(filp)); | |
| struct vsoc_region_data *data = vsoc_dev.regions_data + region_number; | |
| /* Ensure that the offset is aligned */ | |
| if (offset & (sizeof(uint32_t) - 1)) | |
| return -EADDRNOTAVAIL; | |
| /* Ensure that the offset is within shared memory */ | |
| if (((uint64_t)offset) + region_p->region_begin_offset + | |
| sizeof(uint32_t) > region_p->region_end_offset) | |
| return -E2BIG; | |
| /* | |
| * TODO(b/73664181): Use multiple futex wait queues. | |
| * We need to wake every sleeper when the condition changes. Typically | |
| * only a single thread will be waiting on the condition, but there | |
| * are exceptions. The worst case is about 10 threads. | |
| */ | |
| wake_up_interruptible_all(&data->futex_wait_queue); | |
| return 0; | |
| } | |
| static long vsoc_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) | |
| { | |
| int rv = 0; | |
| struct vsoc_device_region *region_p; | |
| u32 reg_num; | |
| struct vsoc_region_data *reg_data; | |
| int retval = vsoc_validate_filep(filp); | |
| if (retval) | |
| return retval; | |
| region_p = vsoc_region_from_filep(filp); | |
| reg_num = iminor(file_inode(filp)); | |
| reg_data = vsoc_dev.regions_data + reg_num; | |
| switch (cmd) { | |
| case VSOC_CREATE_FD_SCOPED_PERMISSION: | |
| { | |
| struct fd_scoped_permission_node *node = NULL; | |
| node = kzalloc(sizeof(*node), GFP_KERNEL); | |
| /* We can't allocate memory for the permission */ | |
| if (!node) | |
| return -ENOMEM; | |
| INIT_LIST_HEAD(&node->list); | |
| rv = do_create_fd_scoped_permission | |
| (region_p, | |
| node, | |
| (struct fd_scoped_permission_arg __user *)arg); | |
| if (!rv) { | |
| mutex_lock(&vsoc_dev.mtx); | |
| list_add(&node->list, &vsoc_dev.permissions); | |
| mutex_unlock(&vsoc_dev.mtx); | |
| } else { | |
| kfree(node); | |
| return rv; | |
| } | |
| } | |
| break; | |
| case VSOC_GET_FD_SCOPED_PERMISSION: | |
| { | |
| struct fd_scoped_permission_node *node = | |
| ((struct vsoc_private_data *)filp->private_data)-> | |
| fd_scoped_permission_node; | |
| if (!node) | |
| return -ENOENT; | |
| if (copy_to_user | |
| ((struct fd_scoped_permission __user *)arg, | |
| &node->permission, sizeof(node->permission))) | |
| return -EFAULT; | |
| } | |
| break; | |
| case VSOC_MAYBE_SEND_INTERRUPT_TO_HOST: | |
| if (!atomic_xchg(reg_data->outgoing_signalled, 1)) { | |
| writel(reg_num, vsoc_dev.regs + DOORBELL); | |
| return 0; | |
| } else { | |
| return -EBUSY; | |
| } | |
| break; | |
| case VSOC_SEND_INTERRUPT_TO_HOST: | |
| writel(reg_num, vsoc_dev.regs + DOORBELL); | |
| return 0; | |
| case VSOC_WAIT_FOR_INCOMING_INTERRUPT: | |
| wait_event_interruptible | |
| (reg_data->interrupt_wait_queue, | |
| (atomic_read(reg_data->incoming_signalled) != 0)); | |
| break; | |
| case VSOC_DESCRIBE_REGION: | |
| return do_vsoc_describe_region | |
| (filp, | |
| (struct vsoc_device_region __user *)arg); | |
| case VSOC_SELF_INTERRUPT: | |
| atomic_set(reg_data->incoming_signalled, 1); | |
| wake_up_interruptible(®_data->interrupt_wait_queue); | |
| break; | |
| case VSOC_COND_WAIT: | |
| return do_vsoc_cond_wait(filp, | |
| (struct vsoc_cond_wait __user *)arg); | |
| case VSOC_COND_WAKE: | |
| return do_vsoc_cond_wake(filp, arg); | |
| default: | |
| return -EINVAL; | |
| } | |
| return 0; | |
| } | |
| static ssize_t vsoc_read(struct file *filp, char __user *buffer, size_t len, | |
| loff_t *poffset) | |
| { | |
| __u32 area_off; | |
| const void *area_p; | |
| ssize_t area_len; | |
| int retval = vsoc_validate_filep(filp); | |
| if (retval) | |
| return retval; | |
| area_len = vsoc_get_area(filp, &area_off); | |
| area_p = shm_off_to_virtual_addr(area_off); | |
| area_p += *poffset; | |
| area_len -= *poffset; | |
| if (area_len <= 0) | |
| return 0; | |
| if (area_len < len) | |
| len = area_len; | |
| if (copy_to_user(buffer, area_p, len)) | |
| return -EFAULT; | |
| *poffset += len; | |
| return len; | |
| } | |
| static loff_t vsoc_lseek(struct file *filp, loff_t offset, int origin) | |
| { | |
| ssize_t area_len = 0; | |
| int retval = vsoc_validate_filep(filp); | |
| if (retval) | |
| return retval; | |
| area_len = vsoc_get_area(filp, NULL); | |
| switch (origin) { | |
| case SEEK_SET: | |
| break; | |
| case SEEK_CUR: | |
| if (offset > 0 && offset + filp->f_pos < 0) | |
| return -EOVERFLOW; | |
| offset += filp->f_pos; | |
| break; | |
| case SEEK_END: | |
| if (offset > 0 && offset + area_len < 0) | |
| return -EOVERFLOW; | |
| offset += area_len; | |
| break; | |
| case SEEK_DATA: | |
| if (offset >= area_len) | |
| return -EINVAL; | |
| if (offset < 0) | |
| offset = 0; | |
| break; | |
| case SEEK_HOLE: | |
| /* Next hole is always the end of the region, unless offset is | |
| * beyond that | |
| */ | |
| if (offset < area_len) | |
| offset = area_len; | |
| break; | |
| default: | |
| return -EINVAL; | |
| } | |
| if (offset < 0 || offset > area_len) | |
| return -EINVAL; | |
| filp->f_pos = offset; | |
| return offset; | |
| } | |
| static ssize_t vsoc_write(struct file *filp, const char __user *buffer, | |
| size_t len, loff_t *poffset) | |
| { | |
| __u32 area_off; | |
| void *area_p; | |
| ssize_t area_len; | |
| int retval = vsoc_validate_filep(filp); | |
| if (retval) | |
| return retval; | |
| area_len = vsoc_get_area(filp, &area_off); | |
| area_p = shm_off_to_virtual_addr(area_off); | |
| area_p += *poffset; | |
| area_len -= *poffset; | |
| if (area_len <= 0) | |
| return 0; | |
| if (area_len < len) | |
| len = area_len; | |
| if (copy_from_user(area_p, buffer, len)) | |
| return -EFAULT; | |
| *poffset += len; | |
| return len; | |
| } | |
| static irqreturn_t vsoc_interrupt(int irq, void *region_data_v) | |
| { | |
| struct vsoc_region_data *region_data = | |
| (struct vsoc_region_data *)region_data_v; | |
| int reg_num = region_data - vsoc_dev.regions_data; | |
| if (unlikely(!region_data)) | |
| return IRQ_NONE; | |
| if (unlikely(reg_num < 0 || | |
| reg_num >= vsoc_dev.layout->region_count)) { | |
| dev_err(&vsoc_dev.dev->dev, | |
| "invalid irq @%p reg_num=0x%04x\n", | |
| region_data, reg_num); | |
| return IRQ_NONE; | |
| } | |
| if (unlikely(vsoc_dev.regions_data + reg_num != region_data)) { | |
| dev_err(&vsoc_dev.dev->dev, | |
| "irq not aligned @%p reg_num=0x%04x\n", | |
| region_data, reg_num); | |
| return IRQ_NONE; | |
| } | |
| wake_up_interruptible(®ion_data->interrupt_wait_queue); | |
| return IRQ_HANDLED; | |
| } | |
| static int vsoc_probe_device(struct pci_dev *pdev, | |
| const struct pci_device_id *ent) | |
| { | |
| int result; | |
| int i; | |
| resource_size_t reg_size; | |
| dev_t devt; | |
| vsoc_dev.dev = pdev; | |
| result = pci_enable_device(pdev); | |
| if (result) { | |
| dev_err(&pdev->dev, | |
| "pci_enable_device failed %s: error %d\n", | |
| pci_name(pdev), result); | |
| return result; | |
| } | |
| vsoc_dev.enabled_device = true; | |
| result = pci_request_regions(pdev, "vsoc"); | |
| if (result < 0) { | |
| dev_err(&pdev->dev, "pci_request_regions failed\n"); | |
| vsoc_remove_device(pdev); | |
| return -EBUSY; | |
| } | |
| vsoc_dev.requested_regions = true; | |
| /* Set up the control registers in BAR 0 */ | |
| reg_size = pci_resource_len(pdev, REGISTER_BAR); | |
| if (reg_size > MAX_REGISTER_BAR_LEN) | |
| vsoc_dev.regs = | |
| pci_iomap(pdev, REGISTER_BAR, MAX_REGISTER_BAR_LEN); | |
| else | |
| vsoc_dev.regs = pci_iomap(pdev, REGISTER_BAR, reg_size); | |
| if (!vsoc_dev.regs) { | |
| dev_err(&pdev->dev, | |
| "cannot map registers of size %zu\n", | |
| (size_t)reg_size); | |
| vsoc_remove_device(pdev); | |
| return -EBUSY; | |
| } | |
| /* Map the shared memory in BAR 2 */ | |
| vsoc_dev.shm_phys_start = pci_resource_start(pdev, SHARED_MEMORY_BAR); | |
| vsoc_dev.shm_size = pci_resource_len(pdev, SHARED_MEMORY_BAR); | |
| dev_info(&pdev->dev, "shared memory @ DMA %pa size=0x%zx\n", | |
| &vsoc_dev.shm_phys_start, vsoc_dev.shm_size); | |
| vsoc_dev.kernel_mapped_shm = pci_iomap_wc(pdev, SHARED_MEMORY_BAR, 0); | |
| if (!vsoc_dev.kernel_mapped_shm) { | |
| dev_err(&vsoc_dev.dev->dev, "cannot iomap region\n"); | |
| vsoc_remove_device(pdev); | |
| return -EBUSY; | |
| } | |
| vsoc_dev.layout = (struct vsoc_shm_layout_descriptor __force *) | |
| vsoc_dev.kernel_mapped_shm; | |
| dev_info(&pdev->dev, "major_version: %d\n", | |
| vsoc_dev.layout->major_version); | |
| dev_info(&pdev->dev, "minor_version: %d\n", | |
| vsoc_dev.layout->minor_version); | |
| dev_info(&pdev->dev, "size: 0x%x\n", vsoc_dev.layout->size); | |
| dev_info(&pdev->dev, "regions: %d\n", vsoc_dev.layout->region_count); | |
| if (vsoc_dev.layout->major_version != | |
| CURRENT_VSOC_LAYOUT_MAJOR_VERSION) { | |
| dev_err(&vsoc_dev.dev->dev, | |
| "driver supports only major_version %d\n", | |
| CURRENT_VSOC_LAYOUT_MAJOR_VERSION); | |
| vsoc_remove_device(pdev); | |
| return -EBUSY; | |
| } | |
| result = alloc_chrdev_region(&devt, 0, vsoc_dev.layout->region_count, | |
| VSOC_DEV_NAME); | |
| if (result) { | |
| dev_err(&vsoc_dev.dev->dev, "alloc_chrdev_region failed\n"); | |
| vsoc_remove_device(pdev); | |
| return -EBUSY; | |
| } | |
| vsoc_dev.major = MAJOR(devt); | |
| cdev_init(&vsoc_dev.cdev, &vsoc_ops); | |
| vsoc_dev.cdev.owner = THIS_MODULE; | |
| result = cdev_add(&vsoc_dev.cdev, devt, vsoc_dev.layout->region_count); | |
| if (result) { | |
| dev_err(&vsoc_dev.dev->dev, "cdev_add error\n"); | |
| vsoc_remove_device(pdev); | |
| return -EBUSY; | |
| } | |
| vsoc_dev.cdev_added = true; | |
| vsoc_dev.class = class_create(THIS_MODULE, VSOC_DEV_NAME); | |
| if (IS_ERR(vsoc_dev.class)) { | |
| dev_err(&vsoc_dev.dev->dev, "class_create failed\n"); | |
| vsoc_remove_device(pdev); | |
| return PTR_ERR(vsoc_dev.class); | |
| } | |
| vsoc_dev.class_added = true; | |
| vsoc_dev.regions = (struct vsoc_device_region __force *) | |
| ((void *)vsoc_dev.layout + | |
| vsoc_dev.layout->vsoc_region_desc_offset); | |
| vsoc_dev.msix_entries = | |
| kcalloc(vsoc_dev.layout->region_count, | |
| sizeof(vsoc_dev.msix_entries[0]), GFP_KERNEL); | |
| if (!vsoc_dev.msix_entries) { | |
| dev_err(&vsoc_dev.dev->dev, | |
| "unable to allocate msix_entries\n"); | |
| vsoc_remove_device(pdev); | |
| return -ENOSPC; | |
| } | |
| vsoc_dev.regions_data = | |
| kcalloc(vsoc_dev.layout->region_count, | |
| sizeof(vsoc_dev.regions_data[0]), GFP_KERNEL); | |
| if (!vsoc_dev.regions_data) { | |
| dev_err(&vsoc_dev.dev->dev, | |
| "unable to allocate regions' data\n"); | |
| vsoc_remove_device(pdev); | |
| return -ENOSPC; | |
| } | |
| for (i = 0; i < vsoc_dev.layout->region_count; ++i) | |
| vsoc_dev.msix_entries[i].entry = i; | |
| result = pci_enable_msix_exact(vsoc_dev.dev, vsoc_dev.msix_entries, | |
| vsoc_dev.layout->region_count); | |
| if (result) { | |
| dev_info(&pdev->dev, "pci_enable_msix failed: %d\n", result); | |
| vsoc_remove_device(pdev); | |
| return -ENOSPC; | |
| } | |
| /* Check that all regions are well formed */ | |
| for (i = 0; i < vsoc_dev.layout->region_count; ++i) { | |
| const struct vsoc_device_region *region = vsoc_dev.regions + i; | |
| if (!PAGE_ALIGNED(region->region_begin_offset) || | |
| !PAGE_ALIGNED(region->region_end_offset)) { | |
| dev_err(&vsoc_dev.dev->dev, | |
| "region %d not aligned (%x:%x)", i, | |
| region->region_begin_offset, | |
| region->region_end_offset); | |
| vsoc_remove_device(pdev); | |
| return -EFAULT; | |
| } | |
| if (region->region_begin_offset >= region->region_end_offset || | |
| region->region_end_offset > vsoc_dev.shm_size) { | |
| dev_err(&vsoc_dev.dev->dev, | |
| "region %d offsets are wrong: %x %x %zx", | |
| i, region->region_begin_offset, | |
| region->region_end_offset, vsoc_dev.shm_size); | |
| vsoc_remove_device(pdev); | |
| return -EFAULT; | |
| } | |
| if (region->managed_by >= vsoc_dev.layout->region_count) { | |
| dev_err(&vsoc_dev.dev->dev, | |
| "region %d has invalid owner: %u", | |
| i, region->managed_by); | |
| vsoc_remove_device(pdev); | |
| return -EFAULT; | |
| } | |
| } | |
| vsoc_dev.msix_enabled = true; | |
| for (i = 0; i < vsoc_dev.layout->region_count; ++i) { | |
| const struct vsoc_device_region *region = vsoc_dev.regions + i; | |
| size_t name_sz = sizeof(vsoc_dev.regions_data[i].name) - 1; | |
| const struct vsoc_signal_table_layout *h_to_g_signal_table = | |
| ®ion->host_to_guest_signal_table; | |
| const struct vsoc_signal_table_layout *g_to_h_signal_table = | |
| ®ion->guest_to_host_signal_table; | |
| vsoc_dev.regions_data[i].name[name_sz] = '\0'; | |
| memcpy(vsoc_dev.regions_data[i].name, region->device_name, | |
| name_sz); | |
| dev_info(&pdev->dev, "region %d name=%s\n", | |
| i, vsoc_dev.regions_data[i].name); | |
| init_waitqueue_head | |
| (&vsoc_dev.regions_data[i].interrupt_wait_queue); | |
| init_waitqueue_head(&vsoc_dev.regions_data[i].futex_wait_queue); | |
| vsoc_dev.regions_data[i].incoming_signalled = | |
| shm_off_to_virtual_addr(region->region_begin_offset) + | |
| h_to_g_signal_table->interrupt_signalled_offset; | |
| vsoc_dev.regions_data[i].outgoing_signalled = | |
| shm_off_to_virtual_addr(region->region_begin_offset) + | |
| g_to_h_signal_table->interrupt_signalled_offset; | |
| result = request_irq(vsoc_dev.msix_entries[i].vector, | |
| vsoc_interrupt, 0, | |
| vsoc_dev.regions_data[i].name, | |
| vsoc_dev.regions_data + i); | |
| if (result) { | |
| dev_info(&pdev->dev, | |
| "request_irq failed irq=%d vector=%d\n", | |
| i, vsoc_dev.msix_entries[i].vector); | |
| vsoc_remove_device(pdev); | |
| return -ENOSPC; | |
| } | |
| vsoc_dev.regions_data[i].irq_requested = true; | |
| if (!device_create(vsoc_dev.class, NULL, | |
| MKDEV(vsoc_dev.major, i), | |
| NULL, vsoc_dev.regions_data[i].name)) { | |
| dev_err(&vsoc_dev.dev->dev, "device_create failed\n"); | |
| vsoc_remove_device(pdev); | |
| return -EBUSY; | |
| } | |
| vsoc_dev.regions_data[i].device_created = true; | |
| } | |
| return 0; | |
| } | |
| /* | |
| * This should undo all of the allocations in the probe function in reverse | |
| * order. | |
| * | |
| * Notes: | |
| * | |
| * The device may have been partially initialized, so double check | |
| * that the allocations happened. | |
| * | |
| * This function may be called multiple times, so mark resources as freed | |
| * as they are deallocated. | |
| */ | |
| static void vsoc_remove_device(struct pci_dev *pdev) | |
| { | |
| int i; | |
| /* | |
| * pdev is the first thing to be set on probe and the last thing | |
| * to be cleared here. If it's NULL then there is no cleanup. | |
| */ | |
| if (!pdev || !vsoc_dev.dev) | |
| return; | |
| dev_info(&pdev->dev, "remove_device\n"); | |
| if (vsoc_dev.regions_data) { | |
| for (i = 0; i < vsoc_dev.layout->region_count; ++i) { | |
| if (vsoc_dev.regions_data[i].device_created) { | |
| device_destroy(vsoc_dev.class, | |
| MKDEV(vsoc_dev.major, i)); | |
| vsoc_dev.regions_data[i].device_created = false; | |
| } | |
| if (vsoc_dev.regions_data[i].irq_requested) | |
| free_irq(vsoc_dev.msix_entries[i].vector, NULL); | |
| vsoc_dev.regions_data[i].irq_requested = false; | |
| } | |
| kfree(vsoc_dev.regions_data); | |
| vsoc_dev.regions_data = NULL; | |
| } | |
| if (vsoc_dev.msix_enabled) { | |
| pci_disable_msix(pdev); | |
| vsoc_dev.msix_enabled = false; | |
| } | |
| kfree(vsoc_dev.msix_entries); | |
| vsoc_dev.msix_entries = NULL; | |
| vsoc_dev.regions = NULL; | |
| if (vsoc_dev.class_added) { | |
| class_destroy(vsoc_dev.class); | |
| vsoc_dev.class_added = false; | |
| } | |
| if (vsoc_dev.cdev_added) { | |
| cdev_del(&vsoc_dev.cdev); | |
| vsoc_dev.cdev_added = false; | |
| } | |
| if (vsoc_dev.major && vsoc_dev.layout) { | |
| unregister_chrdev_region(MKDEV(vsoc_dev.major, 0), | |
| vsoc_dev.layout->region_count); | |
| vsoc_dev.major = 0; | |
| } | |
| vsoc_dev.layout = NULL; | |
| if (vsoc_dev.kernel_mapped_shm) { | |
| pci_iounmap(pdev, vsoc_dev.kernel_mapped_shm); | |
| vsoc_dev.kernel_mapped_shm = NULL; | |
| } | |
| if (vsoc_dev.regs) { | |
| pci_iounmap(pdev, vsoc_dev.regs); | |
| vsoc_dev.regs = NULL; | |
| } | |
| if (vsoc_dev.requested_regions) { | |
| pci_release_regions(pdev); | |
| vsoc_dev.requested_regions = false; | |
| } | |
| if (vsoc_dev.enabled_device) { | |
| pci_disable_device(pdev); | |
| vsoc_dev.enabled_device = false; | |
| } | |
| /* Do this last: it indicates that the device is not initialized. */ | |
| vsoc_dev.dev = NULL; | |
| } | |
| static void __exit vsoc_cleanup_module(void) | |
| { | |
| vsoc_remove_device(vsoc_dev.dev); | |
| pci_unregister_driver(&vsoc_pci_driver); | |
| } | |
| static int __init vsoc_init_module(void) | |
| { | |
| int err = -ENOMEM; | |
| INIT_LIST_HEAD(&vsoc_dev.permissions); | |
| mutex_init(&vsoc_dev.mtx); | |
| err = pci_register_driver(&vsoc_pci_driver); | |
| if (err < 0) | |
| return err; | |
| return 0; | |
| } | |
| static int vsoc_open(struct inode *inode, struct file *filp) | |
| { | |
| /* Can't use vsoc_validate_filep because filp is still incomplete */ | |
| int ret = vsoc_validate_inode(inode); | |
| if (ret) | |
| return ret; | |
| filp->private_data = | |
| kzalloc(sizeof(struct vsoc_private_data), GFP_KERNEL); | |
| if (!filp->private_data) | |
| return -ENOMEM; | |
| return 0; | |
| } | |
| static int vsoc_release(struct inode *inode, struct file *filp) | |
| { | |
| struct vsoc_private_data *private_data = NULL; | |
| struct fd_scoped_permission_node *node = NULL; | |
| struct vsoc_device_region *owner_region_p = NULL; | |
| int retval = vsoc_validate_filep(filp); | |
| if (retval) | |
| return retval; | |
| private_data = (struct vsoc_private_data *)filp->private_data; | |
| if (!private_data) | |
| return 0; | |
| node = private_data->fd_scoped_permission_node; | |
| if (node) { | |
| owner_region_p = vsoc_region_from_inode(inode); | |
| if (owner_region_p->managed_by != VSOC_REGION_WHOLE) { | |
| owner_region_p = | |
| &vsoc_dev.regions[owner_region_p->managed_by]; | |
| } | |
| do_destroy_fd_scoped_permission_node(owner_region_p, node); | |
| private_data->fd_scoped_permission_node = NULL; | |
| } | |
| kfree(private_data); | |
| filp->private_data = NULL; | |
| return 0; | |
| } | |
| /* | |
| * Returns the device relative offset and length of the area specified by the | |
| * fd scoped permission. If there is no fd scoped permission set, a default | |
| * permission covering the entire region is assumed, unless the region is owned | |
| * by another one, in which case the default is a permission with zero size. | |
| */ | |
| static ssize_t vsoc_get_area(struct file *filp, __u32 *area_offset) | |
| { | |
| __u32 off = 0; | |
| ssize_t length = 0; | |
| struct vsoc_device_region *region_p; | |
| struct fd_scoped_permission *perm; | |
| region_p = vsoc_region_from_filep(filp); | |
| off = region_p->region_begin_offset; | |
| perm = &((struct vsoc_private_data *)filp->private_data)-> | |
| fd_scoped_permission_node->permission; | |
| if (perm) { | |
| off += perm->begin_offset; | |
| length = perm->end_offset - perm->begin_offset; | |
| } else if (region_p->managed_by == VSOC_REGION_WHOLE) { | |
| /* No permission set and the regions is not owned by another, | |
| * default to full region access. | |
| */ | |
| length = vsoc_device_region_size(region_p); | |
| } else { | |
| /* return zero length, access is denied. */ | |
| length = 0; | |
| } | |
| if (area_offset) | |
| *area_offset = off; | |
| return length; | |
| } | |
| static int vsoc_mmap(struct file *filp, struct vm_area_struct *vma) | |
| { | |
| unsigned long len = vma->vm_end - vma->vm_start; | |
| __u32 area_off; | |
| phys_addr_t mem_off; | |
| ssize_t area_len; | |
| int retval = vsoc_validate_filep(filp); | |
| if (retval) | |
| return retval; | |
| area_len = vsoc_get_area(filp, &area_off); | |
| /* Add the requested offset */ | |
| area_off += (vma->vm_pgoff << PAGE_SHIFT); | |
| area_len -= (vma->vm_pgoff << PAGE_SHIFT); | |
| if (area_len < len) | |
| return -EINVAL; | |
| vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); | |
| mem_off = shm_off_to_phys_addr(area_off); | |
| if (io_remap_pfn_range(vma, vma->vm_start, mem_off >> PAGE_SHIFT, | |
| len, vma->vm_page_prot)) | |
| return -EAGAIN; | |
| return 0; | |
| } | |
| module_init(vsoc_init_module); | |
| module_exit(vsoc_cleanup_module); | |
| MODULE_LICENSE("GPL"); | |
| MODULE_AUTHOR("Greg Hartman <ghartman@google.com>"); | |
| MODULE_DESCRIPTION("VSoC interpretation of QEmu's ivshmem device"); | |
| MODULE_VERSION("1.0"); |