blob: 17005cd2a92d1607825760dc4dc2b293eb909ab6 [file] [log] [blame]
/*
* Jailed character device
*
* Copyright (C) 2016 Google, Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/cdev.h>
#include <linux/cred.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/namei.h>
#include <linux/path.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/stat.h>
#include <linux/uaccess.h>
#include <linux/usb.h>
#include <uapi/linux/device_jail.h>
#include <uapi/linux/usbdevice_fs.h>
#include "jail_request.h"
#define JAIL_DEV_NAME "jailed-%d-%d"
#define JAIL_MAX_DEV 256
#define JAIL_NAME "device_jail"
static dev_t jail_devnum;
struct jail_device {
char *inner_name;
struct path inner_path;
int idx;
struct device dev;
struct cdev cdev;
};
struct jail_file_data {
struct file *inner_file;
int num_ifs_detached;
int ifnums_to_reattach[USB_MAXINTERFACES];
};
/* Indexed by minor number. */
static struct jail_device *jails[JAIL_MAX_DEV] = { };
static struct mutex jails_mutex;
/* Used in class_find_device or bus_find_device. */
static int match_device_by_devt(struct device *dev, const void *data)
{
const dev_t *devt = data;
return dev->devt == *devt;
}
/*
* This looks like casting away const but since it's a function argument
* this is actually an upcast that the compiler isn't smart enough to
* recognize...
*/
#define bus_match_device_by_devt \
((int (*)(struct device *, void *)) match_device_by_devt)
/*
* Sandboxing functions. For now these only have an effect on USB devices,
* and fail harmlessly for other classes of devices.
*/
static int jail_detach(struct jail_device *jail, struct jail_file_data *jfile)
{
struct device *dev;
struct usb_device *usbdev;
struct usb_host_config *config;
int i;
int num_intfs;
dev_t devt = jail->inner_path.dentry->d_inode->i_rdev;
dev = bus_find_device(&usb_bus_type, NULL, &devt,
bus_match_device_by_devt);
if (!dev)
return -EINVAL;
usbdev = to_usb_device(dev);
usb_lock_device(usbdev);
/* Look for all interfaces in the active configuration. */
config = usbdev->actconfig;
if (!config)
goto no_config;
num_intfs = min_t(int, config->desc.bNumInterfaces, USB_MAXINTERFACES);
for (i = 0; i < num_intfs; i++) {
struct usb_driver *driver;
struct usb_interface *intf = config->interface[i];
if (!intf)
continue;
/* Check if this interface is already detached. */
if (!intf->dev.driver)
continue;
driver = to_usb_driver(intf->dev.driver);
dev_info(&jail->dev, "detaching driver %s\n", driver->name);
usb_driver_release_interface(driver, intf);
jfile->ifnums_to_reattach[jfile->num_ifs_detached++] =
intf->altsetting[0].desc.bInterfaceNumber;
}
no_config:
usb_unlock_device(usbdev);
put_device(dev);
return 0;
}
static int jail_attach(struct jail_device *jail, struct jail_file_data *jfile)
{
struct device *dev;
struct usb_device *usbdev;
dev_t devt = jail->inner_path.dentry->d_inode->i_rdev;
int i;
int ret;
dev = bus_find_device(&usb_bus_type, NULL, &devt,
bus_match_device_by_devt);
if (!dev)
return 0;
usbdev = to_usb_device(dev);
usb_lock_device(usbdev);
for (i = 0; i < jfile->num_ifs_detached; i++) {
int ifnum = jfile->ifnums_to_reattach[i];
struct usb_interface *intf = usb_ifnum_to_if(usbdev, ifnum);
if (!intf) {
dev_warn(&jail->dev, "failed to find interface %d\n",
ifnum);
continue;
}
ret = device_attach(&intf->dev);
if (ret < 0) {
dev_err(&jail->dev,
"failed to reattach driver for interface %d\n",
ifnum);
break;
}
dev_info(&jail->dev, "reattached driver for interface %d\n",
ifnum);
}
usb_unlock_device(usbdev);
put_device(dev);
return ret;
}
/**
* jail_open - open a jailed device
*
* This attempts to open the underlying device, and then
* asks permission_broker if this is an allowable action,
* and if so what it should do with the resulting file.
*/
static int jail_open(struct inode *inode, struct file *file)
{
struct cdev *cdev = inode->i_cdev;
struct jail_device *jail = container_of(cdev, struct jail_device, cdev);
struct jail_file_data *jfile;
const struct cred *old_cred;
struct cred *new_cred;
int ret;
ret = -ENOMEM;
jfile = kzalloc(sizeof(*jfile), GFP_KERNEL);
if (!jfile)
goto err_alloc_file;
/*
* Escalate privileges to open the file. Permission broker will tell us
* what kind of sandboxing to do or whether we should just close the
* file.
*/
new_cred = prepare_creds();
if (!new_cred)
goto err_alloc_cred;
new_cred->fsuid = GLOBAL_ROOT_UID;
new_cred->fsgid = GLOBAL_ROOT_GID;
old_cred = override_creds(new_cred);
jfile->inner_file = dentry_open(&jail->inner_path, file->f_flags,
current_cred());
revert_creds(old_cred);
if (IS_ERR(jfile->inner_file)) {
ret = PTR_ERR(jfile->inner_file);
goto err_open_inner;
}
jfile->num_ifs_detached = 0;
ret = -EACCES;
switch (request_access(jail->inner_name)) {
case JAIL_REQUEST_ALLOW:
break;
case JAIL_REQUEST_ALLOW_WITH_LOCKDOWN:
usb_file_drop_privileges(jfile->inner_file);
break;
case JAIL_REQUEST_ALLOW_WITH_DETACH:
ret = jail_detach(jail, jfile);
if (ret < 0)
goto err_request_access;
break;
case JAIL_REQUEST_DENY:
goto err_request_access;
default:
dev_err(&jail->dev, "unknown access level\n");
goto err_request_access;
}
file->private_data = jfile;
return 0;
err_request_access:
fput(jfile->inner_file);
err_open_inner:
err_alloc_cred:
kfree(jfile);
err_alloc_file:
return ret;
}
static int jail_release(struct inode *inode, struct file *file)
{
struct cdev *cdev = inode->i_cdev;
struct jail_device *jail = container_of(cdev, struct jail_device, cdev);
struct jail_file_data *jfile = file->private_data;
if (jfile->num_ifs_detached)
jail_attach(jail, jfile);
fput(jfile->inner_file);
kfree(jfile);
return 0;
}
static ssize_t jail_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
struct jail_file_data *jfile = file->private_data;
struct file *inner = jfile->inner_file;
return vfs_read(inner, buf, count, ppos);
}
static ssize_t jail_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
struct jail_file_data *jfile = file->private_data;
struct file *inner = jfile->inner_file;
return vfs_write(inner, buf, count, ppos);
}
static long ioctl_wrapper(long (*ioctl_fn)(struct file *, unsigned int,
unsigned long),
struct file *file, unsigned int cmd,
unsigned long arg)
{
int ret;
if (!ioctl_fn)
return -ENOTTY;
/* ioctl filters can happen here */
ret = ioctl_fn(file, cmd, arg);
if (ret == -ENOIOCTLCMD)
return -ENOTTY;
return ret;
}
static long jail_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct jail_file_data *jfile = file->private_data;
struct file *inner = jfile->inner_file;
return ioctl_wrapper(inner->f_op->unlocked_ioctl,
inner, cmd, arg);
}
static long jail_compat_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct jail_file_data *jfile = file->private_data;
struct file *inner = jfile->inner_file;
return ioctl_wrapper(inner->f_op->compat_ioctl,
inner, cmd, arg);
}
static unsigned int jail_poll(struct file *file,
struct poll_table_struct *wait)
{
struct jail_file_data *jfile = file->private_data;
struct file *inner = jfile->inner_file;
if (inner->f_op->poll)
return inner->f_op->poll(inner, wait);
return -EINVAL;
}
static loff_t jail_llseek(struct file *file, loff_t off, int whence)
{
struct jail_file_data *jfile = file->private_data;
struct file *inner = jfile->inner_file;
return vfs_llseek(inner, off, whence);
}
static const struct file_operations jail_fops = {
.owner = THIS_MODULE,
.open = jail_open,
.release = jail_release,
.read = jail_read,
.write = jail_write,
.unlocked_ioctl = jail_ioctl,
.compat_ioctl = jail_compat_ioctl,
.poll = jail_poll,
.llseek = jail_llseek,
};
static struct class jail_class = {
.owner = THIS_MODULE,
.name = JAIL_NAME,
};
static int match_device_by_inner_devt(struct device *dev, const void *data)
{
const dev_t *devt = data;
struct jail_device *jail = container_of(dev, struct jail_device, dev);
return jail->inner_path.dentry->d_inode->i_rdev == *devt;
}
int add_jail_device(const char __user *path, dev_t *new_devt)
{
struct jail_device *jail;
struct device *existing;
struct device *usbdev;
struct inode *inode;
int idx;
int ret;
dev_t devt;
jail = kzalloc(sizeof(*jail), GFP_KERNEL);
if (!jail)
return -ENOMEM;
ret = user_path(path, &jail->inner_path);
if (ret < 0) {
pr_err("%s: could not resolve path\n", __func__);
goto err_path;
}
inode = jail->inner_path.dentry->d_inode;
ret = -EINVAL;
if (!S_ISCHR(inode->i_mode)) {
pr_err("%s: not a character device\n", __func__);
goto err_not_cdev;
}
usbdev = bus_find_device(&usb_bus_type, NULL, &inode->i_rdev,
bus_match_device_by_devt);
if (!usbdev) {
pr_err("%s: currently supports only USB devices\n", __func__);
goto err_not_usbdev;
}
put_device(usbdev);
mutex_lock(&jails_mutex);
/*
* If the device already exists, set *new_devt anyway.
* Do this here to avoid spamming the logs when we try to
* register another kobject later.
*/
ret = -EEXIST;
existing = class_find_device(
&jail_class, NULL, &inode->i_rdev, match_device_by_inner_devt);
if (existing) {
*new_devt = existing->devt;
put_device(existing);
goto err_exists;
}
/* Find an open spot in the jails array. */
ret = -ENOMEM;
for (idx = 0; idx < JAIL_MAX_DEV; idx++) {
if (!jails[idx])
break;
}
if (idx == JAIL_MAX_DEV)
goto err_full;
devt = jail_devnum + idx;
pr_info("%s: assigned new index %d (devt %d:%d)\n",
__func__, idx, MAJOR(devt), MINOR(devt));
ret = -ENOMEM;
jail->inner_name = kzalloc(PATH_MAX, GFP_KERNEL);
if (!jail->inner_name)
goto err_alloc_path;
ret = strncpy_from_user(jail->inner_name, path, PATH_MAX - 1);
if (ret < 0)
goto err_copy_path;
pr_info("%s: initializing device " JAIL_DEV_NAME "\n",
__func__, MAJOR(inode->i_rdev), MINOR(inode->i_rdev));
/* register cdev */
cdev_init(&jail->cdev, &jail_fops);
ret = cdev_add(&jail->cdev, devt, 1);
if (ret)
goto err_register_cdev;
/* register device */
jail->dev.class = &jail_class;
jail->dev.devt = devt;
dev_set_name(&jail->dev, JAIL_DEV_NAME,
MAJOR(inode->i_rdev), MINOR(inode->i_rdev));
ret = device_register(&jail->dev);
if (ret)
goto err_register_dev;
*new_devt = devt;
/* add jail to array */
jails[idx] = jail;
jail->idx = idx;
mutex_unlock(&jails_mutex);
return 0;
err_register_dev:
cdev_del(&jail->cdev);
err_register_cdev:
err_copy_path:
kfree(jail->inner_name);
err_alloc_path:
err_full:
err_exists:
mutex_unlock(&jails_mutex);
err_not_usbdev:
err_not_cdev:
path_put(&jail->inner_path);
err_path:
kfree(jail);
return ret;
}
static void destroy_jail_locked(struct jail_device *jail)
{
jails[jail->idx] = NULL;
device_unregister(&jail->dev);
cdev_del(&jail->cdev);
kfree(jail->inner_name);
path_put(&jail->inner_path);
kfree(jail);
}
int remove_jail_device(dev_t devt)
{
struct device *dev = class_find_device(&jail_class, NULL, &devt,
match_device_by_devt);
if (!dev)
return -ENOENT;
mutex_lock(&jails_mutex);
destroy_jail_locked(container_of(dev, struct jail_device, dev));
mutex_unlock(&jails_mutex);
return 0;
}
int jail_device_setup(void)
{
int ret;
ret = class_register(&jail_class);
if (ret) {
pr_err(JAIL_NAME ": failed to register device class\n");
return ret;
}
ret = alloc_chrdev_region(&jail_devnum, 0, JAIL_MAX_DEV, JAIL_NAME);
if (ret < 0) {
pr_err(JAIL_NAME ": failed to allocate chrdev region\n");
goto err_region;
}
pr_info(JAIL_NAME ": allocated device range %d -> %d\n",
jail_devnum, jail_devnum + JAIL_MAX_DEV);
mutex_init(&jails_mutex);
return 0;
err_region:
class_unregister(&jail_class);
return ret;
}
void jail_device_teardown(void)
{
int i;
mutex_lock(&jails_mutex);
for (i = 0; i < JAIL_MAX_DEV; i++) {
if (jails[i])
destroy_jail_locked(jails[i]);
}
mutex_unlock(&jails_mutex);
unregister_chrdev_region(jail_devnum, JAIL_MAX_DEV);
class_unregister(&jail_class);
}