blob: a709598923fd2d12d7d3d2b94cb87bce4c06a095 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0
/* Copyright (C) 2018 Google, Inc. */
#include "gasket.h"
#include "gasket_ioctl.h"
#include "gasket_constants.h"
#include "gasket_core.h"
#include "gasket_interrupt.h"
#include "gasket_page_table.h"
#include <linux/compiler.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#ifdef GASKET_KERNEL_TRACE_SUPPORT
#define CREATE_TRACE_POINTS
#include <trace/events/gasket_ioctl.h>
#else
#define trace_gasket_ioctl_entry(x, ...)
#define trace_gasket_ioctl_exit(x)
#define trace_gasket_ioctl_integer_data(x)
#define trace_gasket_ioctl_eventfd_data(x, ...)
#define trace_gasket_ioctl_page_table_data(x, ...)
#define trace_gasket_ioctl_page_table_flags_data(x, ...)
#define trace_gasket_ioctl_config_coherent_allocator(x, ...)
#endif
/* Associate an eventfd with an interrupt. */
static int gasket_set_event_fd(struct gasket_dev *gasket_dev,
struct gasket_interrupt_eventfd __user *argp)
{
struct gasket_interrupt_eventfd die;
if (copy_from_user(&die, argp, sizeof(struct gasket_interrupt_eventfd)))
return -EFAULT;
trace_gasket_ioctl_eventfd_data(die.interrupt, die.event_fd);
return gasket_interrupt_set_eventfd(
gasket_dev->interrupt_data, die.interrupt, die.event_fd);
}
/* Read the size of the page table. */
static int gasket_read_page_table_size(
struct gasket_dev *gasket_dev,
struct gasket_page_table_ioctl __user *argp)
{
int ret = 0;
struct gasket_page_table_ioctl ibuf;
if (copy_from_user(&ibuf, argp, sizeof(struct gasket_page_table_ioctl)))
return -EFAULT;
if (ibuf.page_table_index >= gasket_dev->num_page_tables)
return -EFAULT;
ibuf.size = gasket_page_table_num_entries(
gasket_dev->page_table[ibuf.page_table_index]);
trace_gasket_ioctl_page_table_data(
ibuf.page_table_index, ibuf.size, ibuf.host_address,
ibuf.device_address);
if (copy_to_user(argp, &ibuf, sizeof(ibuf)))
return -EFAULT;
return ret;
}
/* Read the size of the simple page table. */
static int gasket_read_simple_page_table_size(
struct gasket_dev *gasket_dev,
struct gasket_page_table_ioctl __user *argp)
{
int ret = 0;
struct gasket_page_table_ioctl ibuf;
if (copy_from_user(&ibuf, argp, sizeof(struct gasket_page_table_ioctl)))
return -EFAULT;
if (ibuf.page_table_index >= gasket_dev->num_page_tables)
return -EFAULT;
ibuf.size =
gasket_page_table_num_simple_entries(gasket_dev->page_table[ibuf.page_table_index]);
trace_gasket_ioctl_page_table_data(ibuf.page_table_index, ibuf.size,
ibuf.host_address,
ibuf.device_address);
if (copy_to_user(argp, &ibuf, sizeof(ibuf)))
return -EFAULT;
return ret;
}
/* Set the boundary between the simple and extended page tables. */
static int gasket_partition_page_table(
struct gasket_dev *gasket_dev,
struct gasket_page_table_ioctl __user *argp)
{
int ret;
struct gasket_page_table_ioctl ibuf;
uint max_page_table_size;
if (copy_from_user(&ibuf, argp, sizeof(struct gasket_page_table_ioctl)))
return -EFAULT;
trace_gasket_ioctl_page_table_data(
ibuf.page_table_index, ibuf.size, ibuf.host_address,
ibuf.device_address);
if (ibuf.page_table_index >= gasket_dev->num_page_tables)
return -EFAULT;
max_page_table_size = gasket_page_table_max_size(
gasket_dev->page_table[ibuf.page_table_index]);
if (ibuf.size > max_page_table_size) {
dev_dbg(gasket_dev->dev,
"Partition request 0x%llx too large, max is 0x%x\n",
ibuf.size, max_page_table_size);
return -EINVAL;
}
mutex_lock(&gasket_dev->mutex);
ret = gasket_page_table_partition(
gasket_dev->page_table[ibuf.page_table_index], ibuf.size);
mutex_unlock(&gasket_dev->mutex);
return ret;
}
/* Map a userspace buffer to a device virtual address. */
static int gasket_map_buffers_common(struct gasket_dev *gasket_dev,
struct gasket_page_table_ioctl_flags
*pibuf)
{
if (pibuf->base.page_table_index >= gasket_dev->num_page_tables)
return -EFAULT;
if (gasket_page_table_are_addrs_bad(gasket_dev->page_table[pibuf->base.page_table_index],
pibuf->base.host_address,
pibuf->base.device_address,
pibuf->base.size))
return -EINVAL;
return gasket_page_table_map(gasket_dev->page_table[pibuf->base.page_table_index],
pibuf->base.host_address,
pibuf->base.device_address,
pibuf->base.size / PAGE_SIZE,
pibuf->flags);
}
static int gasket_map_buffers(struct gasket_dev *gasket_dev,
struct gasket_page_table_ioctl __user *argp)
{
struct gasket_page_table_ioctl_flags ibuf;
if (copy_from_user(&ibuf.base, argp, sizeof(struct gasket_page_table_ioctl)))
return -EFAULT;
ibuf.flags = 0;
trace_gasket_ioctl_page_table_data(ibuf.base.page_table_index,
ibuf.base.size,
ibuf.base.host_address,
ibuf.base.device_address);
return gasket_map_buffers_common(gasket_dev, &ibuf);
}
static int gasket_map_buffers_flags(struct gasket_dev *gasket_dev,
struct gasket_page_table_ioctl_flags __user *argp)
{
struct gasket_page_table_ioctl_flags ibuf;
if (copy_from_user(&ibuf, argp, sizeof(struct gasket_page_table_ioctl_flags)))
return -EFAULT;
trace_gasket_ioctl_page_table_flags_data(ibuf.base.page_table_index,
ibuf.base.size,
ibuf.base.host_address,
ibuf.base.device_address,
ibuf.flags);
return gasket_map_buffers_common(gasket_dev, &ibuf);
}
/* Unmap a userspace buffer from a device virtual address. */
static int gasket_unmap_buffers(struct gasket_dev *gasket_dev,
struct gasket_page_table_ioctl __user *argp)
{
struct gasket_page_table_ioctl ibuf;
if (copy_from_user(&ibuf, argp, sizeof(struct gasket_page_table_ioctl)))
return -EFAULT;
trace_gasket_ioctl_page_table_data(ibuf.page_table_index, ibuf.size,
ibuf.host_address,
ibuf.device_address);
if (ibuf.page_table_index >= gasket_dev->num_page_tables)
return -EFAULT;
if (gasket_page_table_is_dev_addr_bad(gasket_dev->page_table[ibuf.page_table_index],
ibuf.device_address, ibuf.size))
return -EINVAL;
gasket_page_table_unmap(gasket_dev->page_table[ibuf.page_table_index],
ibuf.device_address, ibuf.size / PAGE_SIZE);
return 0;
}
/* Map/unmap dma-buf to/from a device virtual address. */
static int gasket_map_dmabuf(struct gasket_dev *gasket_dev,
struct gasket_page_table_ioctl_dmabuf __user *argp)
{
struct gasket_page_table_ioctl_dmabuf dbuf;
struct gasket_page_table *pg_tbl;
if (copy_from_user(&dbuf, argp, sizeof(dbuf)))
return -EFAULT;
if (dbuf.page_table_index >= gasket_dev->num_page_tables)
return -EFAULT;
pg_tbl = gasket_dev->page_table[dbuf.page_table_index];
if (gasket_page_table_is_dev_addr_bad(pg_tbl,
dbuf.device_address,
dbuf.num_pages * PAGE_SIZE))
return -EINVAL;
if (dbuf.map)
return gasket_page_table_map_dmabuf(pg_tbl,
dbuf.dmabuf_fd,
dbuf.device_address,
dbuf.num_pages,
dbuf.flags);
else
return gasket_page_table_unmap_dmabuf(pg_tbl,
dbuf.dmabuf_fd,
dbuf.device_address,
dbuf.num_pages);
}
/*
* Reserve structures for coherent allocation, and allocate or free the
* corresponding memory.
*/
static int gasket_config_coherent_allocator(
struct gasket_dev *gasket_dev,
struct gasket_coherent_alloc_config_ioctl __user *argp)
{
int ret;
struct gasket_coherent_alloc_config_ioctl ibuf;
dma_addr_t dma_address;
if (copy_from_user(&ibuf, argp,
sizeof(struct gasket_coherent_alloc_config_ioctl)))
return -EFAULT;
trace_gasket_ioctl_config_coherent_allocator(ibuf.enable, ibuf.size,
ibuf.dma_address);
if (ibuf.page_table_index >= gasket_dev->num_page_tables)
return -EFAULT;
if (ibuf.size > PAGE_SIZE * MAX_NUM_COHERENT_PAGES)
return -ENOMEM;
if (ibuf.enable == 0) {
dma_address = ibuf.dma_address;
ret = gasket_free_coherent_memory(gasket_dev, ibuf.size,
dma_address,
ibuf.page_table_index);
} else {
ret = gasket_alloc_coherent_memory(gasket_dev, ibuf.size,
&dma_address,
ibuf.page_table_index);
}
if (ret)
return ret;
if (ibuf.enable != 0)
ibuf.dma_address = dma_address;
if (copy_to_user(argp, &ibuf, sizeof(ibuf)))
return -EFAULT;
return 0;
}
/* Check permissions for Gasket ioctls. */
static bool gasket_ioctl_check_permissions(struct file *filp, uint cmd)
{
bool alive;
bool read, write;
struct gasket_dev *gasket_dev = (struct gasket_dev *)filp->private_data;
alive = (gasket_dev->status == GASKET_STATUS_ALIVE);
if (!alive)
dev_dbg(gasket_dev->dev, "%s alive %d status %d\n",
__func__, alive, gasket_dev->status);
read = !!(filp->f_mode & FMODE_READ);
write = !!(filp->f_mode & FMODE_WRITE);
switch (cmd) {
case GASKET_IOCTL_RESET:
case GASKET_IOCTL_CLEAR_INTERRUPT_COUNTS:
return write;
case GASKET_IOCTL_PAGE_TABLE_SIZE:
case GASKET_IOCTL_SIMPLE_PAGE_TABLE_SIZE:
case GASKET_IOCTL_NUMBER_PAGE_TABLES:
return read;
case GASKET_IOCTL_PARTITION_PAGE_TABLE:
case GASKET_IOCTL_CONFIG_COHERENT_ALLOCATOR:
return alive && write;
case GASKET_IOCTL_MAP_BUFFER:
case GASKET_IOCTL_MAP_BUFFER_FLAGS:
case GASKET_IOCTL_UNMAP_BUFFER:
case GASKET_IOCTL_MAP_DMABUF:
return alive && write;
case GASKET_IOCTL_CLEAR_EVENTFD:
case GASKET_IOCTL_SET_EVENTFD:
return alive && write;
}
return false; /* unknown permissions */
}
/*
* standard ioctl dispatch function.
* @filp: File structure pointer describing this node usage session.
* @cmd: ioctl number to handle.
* @argp: ioctl-specific data pointer.
*
* Standard ioctl dispatcher; forwards operations to individual handlers.
*/
long gasket_handle_ioctl(struct file *filp, uint cmd, void __user *argp)
{
struct gasket_dev *gasket_dev;
unsigned long arg = (unsigned long)argp;
gasket_ioctl_permissions_cb_t ioctl_permissions_cb;
int retval;
gasket_dev = (struct gasket_dev *)filp->private_data;
trace_gasket_ioctl_entry(gasket_dev->dev_info.name, cmd);
ioctl_permissions_cb = gasket_get_ioctl_permissions_cb(gasket_dev);
if (ioctl_permissions_cb) {
retval = ioctl_permissions_cb(filp, cmd, argp);
if (retval < 0) {
trace_gasket_ioctl_exit(retval);
return retval;
} else if (retval == 0) {
trace_gasket_ioctl_exit(-EPERM);
return -EPERM;
}
} else if (!gasket_ioctl_check_permissions(filp, cmd)) {
trace_gasket_ioctl_exit(-EPERM);
dev_dbg(gasket_dev->dev, "ioctl cmd=%x noperm\n", cmd);
return -EPERM;
}
/* Tracing happens in this switch statement for all ioctls with
* an integer argrument, but ioctls with a struct argument
* that needs copying and decoding, that tracing is done within
* the handler call.
*/
switch (cmd) {
case GASKET_IOCTL_RESET:
retval = gasket_reset(gasket_dev);
break;
case GASKET_IOCTL_SET_EVENTFD:
retval = gasket_set_event_fd(gasket_dev, argp);
break;
case GASKET_IOCTL_CLEAR_EVENTFD:
trace_gasket_ioctl_integer_data(arg);
retval =
gasket_interrupt_clear_eventfd(gasket_dev->interrupt_data,
(int)arg);
break;
case GASKET_IOCTL_PARTITION_PAGE_TABLE:
trace_gasket_ioctl_integer_data(arg);
retval = gasket_partition_page_table(gasket_dev, argp);
break;
case GASKET_IOCTL_NUMBER_PAGE_TABLES:
trace_gasket_ioctl_integer_data(gasket_dev->num_page_tables);
if (copy_to_user(argp, &gasket_dev->num_page_tables,
sizeof(uint64_t)))
retval = -EFAULT;
else
retval = 0;
break;
case GASKET_IOCTL_PAGE_TABLE_SIZE:
retval = gasket_read_page_table_size(gasket_dev, argp);
break;
case GASKET_IOCTL_SIMPLE_PAGE_TABLE_SIZE:
retval = gasket_read_simple_page_table_size(gasket_dev, argp);
break;
case GASKET_IOCTL_MAP_BUFFER:
retval = gasket_map_buffers(gasket_dev, argp);
break;
case GASKET_IOCTL_MAP_BUFFER_FLAGS:
retval = gasket_map_buffers_flags(gasket_dev, argp);
break;
case GASKET_IOCTL_CONFIG_COHERENT_ALLOCATOR:
retval = gasket_config_coherent_allocator(gasket_dev, argp);
break;
case GASKET_IOCTL_UNMAP_BUFFER:
retval = gasket_unmap_buffers(gasket_dev, argp);
break;
case GASKET_IOCTL_CLEAR_INTERRUPT_COUNTS:
/* Clear interrupt counts doesn't take an arg, so use 0. */
trace_gasket_ioctl_integer_data(0);
retval = gasket_interrupt_reset_counts(gasket_dev);
break;
case GASKET_IOCTL_MAP_DMABUF:
retval = gasket_map_dmabuf(gasket_dev, argp);
break;
default:
/* If we don't understand the ioctl, the best we can do is trace
* the arg.
*/
trace_gasket_ioctl_integer_data(arg);
dev_dbg(gasket_dev->dev,
"Unknown ioctl cmd=0x%x not caught by "
"gasket_is_supported_ioctl\n",
cmd);
retval = -EINVAL;
break;
}
trace_gasket_ioctl_exit(retval);
return retval;
}
/*
* Determines if an ioctl is part of the standard Gasket framework.
* @cmd: The ioctl number to handle.
*
* Returns 1 if the ioctl is supported and 0 otherwise.
*/
long gasket_is_supported_ioctl(uint cmd)
{
switch (cmd) {
case GASKET_IOCTL_RESET:
case GASKET_IOCTL_SET_EVENTFD:
case GASKET_IOCTL_CLEAR_EVENTFD:
case GASKET_IOCTL_PARTITION_PAGE_TABLE:
case GASKET_IOCTL_NUMBER_PAGE_TABLES:
case GASKET_IOCTL_PAGE_TABLE_SIZE:
case GASKET_IOCTL_SIMPLE_PAGE_TABLE_SIZE:
case GASKET_IOCTL_MAP_BUFFER:
case GASKET_IOCTL_MAP_BUFFER_FLAGS:
case GASKET_IOCTL_UNMAP_BUFFER:
case GASKET_IOCTL_MAP_DMABUF:
case GASKET_IOCTL_CLEAR_INTERRUPT_COUNTS:
case GASKET_IOCTL_CONFIG_COHERENT_ALLOCATOR:
return 1;
default:
return 0;
}
}