linux/drivers/hid/hid-roccat.c

462 lines
10 KiB
C
Raw Normal View History

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Roccat driver for Linux
*
* Copyright (c) 2010 Stefan Achatz <erazor_de@users.sourceforge.net>
*/
/*
*/
/*
* Module roccat is a char device used to report special events of roccat
* hardware to userland. These events include requests for on-screen-display of
* profile or dpi settings or requests for execution of macro sequences that are
* not stored in device. The information in these events depends on hid device
* implementation and contains data that is not available in a single hid event
* or else hidraw could have been used.
* It is inspired by hidraw, but uses only one circular buffer for all readers.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/cdev.h>
#include <linux/poll.h>
#include <linux/sched/signal.h>
#include <linux/hid-roccat.h>
#include <linux/module.h>
#define ROCCAT_FIRST_MINOR 0
#define ROCCAT_MAX_DEVICES 8
/* should be a power of 2 for performance reason */
#define ROCCAT_CBUF_SIZE 16
struct roccat_report {
uint8_t *value;
};
struct roccat_device {
unsigned int minor;
int report_size;
int open;
int exist;
wait_queue_head_t wait;
struct device *dev;
struct hid_device *hid;
struct list_head readers;
/* protects modifications of readers list */
struct mutex readers_lock;
/*
* circular_buffer has one writer and multiple readers with their own
* read pointers
*/
struct roccat_report cbuf[ROCCAT_CBUF_SIZE];
int cbuf_end;
struct mutex cbuf_lock;
};
struct roccat_reader {
struct list_head node;
struct roccat_device *device;
int cbuf_start;
};
static int roccat_major;
static struct cdev roccat_cdev;
static struct roccat_device *devices[ROCCAT_MAX_DEVICES];
/* protects modifications of devices array */
static DEFINE_MUTEX(devices_lock);
static ssize_t roccat_read(struct file *file, char __user *buffer,
size_t count, loff_t *ppos)
{
struct roccat_reader *reader = file->private_data;
struct roccat_device *device = reader->device;
struct roccat_report *report;
ssize_t retval = 0, len;
DECLARE_WAITQUEUE(wait, current);
mutex_lock(&device->cbuf_lock);
/* no data? */
if (reader->cbuf_start == device->cbuf_end) {
add_wait_queue(&device->wait, &wait);
set_current_state(TASK_INTERRUPTIBLE);
/* wait for data */
while (reader->cbuf_start == device->cbuf_end) {
if (file->f_flags & O_NONBLOCK) {
retval = -EAGAIN;
break;
}
if (signal_pending(current)) {
retval = -ERESTARTSYS;
break;
}
if (!device->exist) {
retval = -EIO;
break;
}
mutex_unlock(&device->cbuf_lock);
schedule();
mutex_lock(&device->cbuf_lock);
set_current_state(TASK_INTERRUPTIBLE);
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&device->wait, &wait);
}
/* here we either have data or a reason to return if retval is set */
if (retval)
goto exit_unlock;
report = &device->cbuf[reader->cbuf_start];
/*
* If report is larger than requested amount of data, rest of report
* is lost!
*/
len = device->report_size > count ? count : device->report_size;
if (copy_to_user(buffer, report->value, len)) {
retval = -EFAULT;
goto exit_unlock;
}
retval += len;
reader->cbuf_start = (reader->cbuf_start + 1) % ROCCAT_CBUF_SIZE;
exit_unlock:
mutex_unlock(&device->cbuf_lock);
return retval;
}
static __poll_t roccat_poll(struct file *file, poll_table *wait)
{
struct roccat_reader *reader = file->private_data;
poll_wait(file, &reader->device->wait, wait);
if (reader->cbuf_start != reader->device->cbuf_end)
return EPOLLIN | EPOLLRDNORM;
if (!reader->device->exist)
return EPOLLERR | EPOLLHUP;
return 0;
}
static int roccat_open(struct inode *inode, struct file *file)
{
unsigned int minor = iminor(inode);
struct roccat_reader *reader;
struct roccat_device *device;
int error = 0;
reader = kzalloc(sizeof(struct roccat_reader), GFP_KERNEL);
if (!reader)
return -ENOMEM;
mutex_lock(&devices_lock);
device = devices[minor];
if (!device) {
pr_emerg("roccat device with minor %d doesn't exist\n", minor);
error = -ENODEV;
goto exit_err_devices;
}
mutex_lock(&device->readers_lock);
if (!device->open++) {
/* power on device on adding first reader */
error = hid_hw_power(device->hid, PM_HINT_FULLON);
if (error < 0) {
--device->open;
goto exit_err_readers;
}
error = hid_hw_open(device->hid);
if (error < 0) {
hid_hw_power(device->hid, PM_HINT_NORMAL);
--device->open;
goto exit_err_readers;
}
}
reader->device = device;
/* new reader doesn't get old events */
reader->cbuf_start = device->cbuf_end;
list_add_tail(&reader->node, &device->readers);
file->private_data = reader;
exit_err_readers:
mutex_unlock(&device->readers_lock);
exit_err_devices:
mutex_unlock(&devices_lock);
if (error)
kfree(reader);
return error;
}
static int roccat_release(struct inode *inode, struct file *file)
{
unsigned int minor = iminor(inode);
struct roccat_reader *reader = file->private_data;
struct roccat_device *device;
mutex_lock(&devices_lock);
device = devices[minor];
if (!device) {
mutex_unlock(&devices_lock);
pr_emerg("roccat device with minor %d doesn't exist\n", minor);
return -ENODEV;
}
mutex_lock(&device->readers_lock);
list_del(&reader->node);
mutex_unlock(&device->readers_lock);
kfree(reader);
if (!--device->open) {
/* removing last reader */
if (device->exist) {
hid_hw_power(device->hid, PM_HINT_NORMAL);
hid_hw_close(device->hid);
} else {
kfree(device);
}
}
mutex_unlock(&devices_lock);
return 0;
}
/*
* roccat_report_event() - output data to readers
* @minor: minor device number returned by roccat_connect()
* @data: pointer to data
*
* Return value is zero on success, a negative error code on failure.
*
* This is called from interrupt handler.
*/
int roccat_report_event(int minor, u8 const *data)
{
struct roccat_device *device;
struct roccat_reader *reader;
struct roccat_report *report;
uint8_t *new_value;
device = devices[minor];
new_value = kmemdup(data, device->report_size, GFP_ATOMIC);
if (!new_value)
return -ENOMEM;
HID: roccat: Fix use-after-free in roccat_read() roccat_report_event() is responsible for registering roccat-related reports in struct roccat_device. int roccat_report_event(int minor, u8 const *data) { struct roccat_device *device; struct roccat_reader *reader; struct roccat_report *report; uint8_t *new_value; device = devices[minor]; new_value = kmemdup(data, device->report_size, GFP_ATOMIC); if (!new_value) return -ENOMEM; report = &device->cbuf[device->cbuf_end]; /* passing NULL is safe */ kfree(report->value); ... The registered report is stored in the struct roccat_device member "struct roccat_report cbuf[ROCCAT_CBUF_SIZE];". If more reports are received than the "ROCCAT_CBUF_SIZE" value, kfree() the saved report from cbuf[0] and allocates a new reprot. Since there is no lock when this kfree() is performed, kfree() can be performed even while reading the saved report. static ssize_t roccat_read(struct file *file, char __user *buffer, size_t count, loff_t *ppos) { struct roccat_reader *reader = file->private_data; struct roccat_device *device = reader->device; struct roccat_report *report; ssize_t retval = 0, len; DECLARE_WAITQUEUE(wait, current); mutex_lock(&device->cbuf_lock); ... report = &device->cbuf[reader->cbuf_start]; /* * If report is larger than requested amount of data, rest of report * is lost! */ len = device->report_size > count ? count : device->report_size; if (copy_to_user(buffer, report->value, len)) { retval = -EFAULT; goto exit_unlock; } ... The roccat_read() function receives the device->cbuf report and delivers it to the user through copy_to_user(). If the N+ROCCAT_CBUF_SIZE th report is received while copying of the Nth report->value is in progress, the pointer that copy_to_user() is working on is kfree()ed and UAF read may occur. (race condition) Since the device node of this driver does not set separate permissions, this is not a security vulnerability, but because it is used for requesting screen display of profile or dpi settings, a user using the roccat device can apply udev to this device node or There is a possibility to use it by giving. Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com> Signed-off-by: Jiri Kosina <jkosina@suse.cz>
2022-09-04 22:31:15 +03:00
mutex_lock(&device->cbuf_lock);
report = &device->cbuf[device->cbuf_end];
/* passing NULL is safe */
kfree(report->value);
report->value = new_value;
device->cbuf_end = (device->cbuf_end + 1) % ROCCAT_CBUF_SIZE;
list_for_each_entry(reader, &device->readers, node) {
/*
* As we already inserted one element, the buffer can't be
* empty. If start and end are equal, buffer is full and we
* increase start, so that slow reader misses one event, but
* gets the newer ones in the right order.
*/
if (reader->cbuf_start == device->cbuf_end)
reader->cbuf_start = (reader->cbuf_start + 1) % ROCCAT_CBUF_SIZE;
}
HID: roccat: Fix use-after-free in roccat_read() roccat_report_event() is responsible for registering roccat-related reports in struct roccat_device. int roccat_report_event(int minor, u8 const *data) { struct roccat_device *device; struct roccat_reader *reader; struct roccat_report *report; uint8_t *new_value; device = devices[minor]; new_value = kmemdup(data, device->report_size, GFP_ATOMIC); if (!new_value) return -ENOMEM; report = &device->cbuf[device->cbuf_end]; /* passing NULL is safe */ kfree(report->value); ... The registered report is stored in the struct roccat_device member "struct roccat_report cbuf[ROCCAT_CBUF_SIZE];". If more reports are received than the "ROCCAT_CBUF_SIZE" value, kfree() the saved report from cbuf[0] and allocates a new reprot. Since there is no lock when this kfree() is performed, kfree() can be performed even while reading the saved report. static ssize_t roccat_read(struct file *file, char __user *buffer, size_t count, loff_t *ppos) { struct roccat_reader *reader = file->private_data; struct roccat_device *device = reader->device; struct roccat_report *report; ssize_t retval = 0, len; DECLARE_WAITQUEUE(wait, current); mutex_lock(&device->cbuf_lock); ... report = &device->cbuf[reader->cbuf_start]; /* * If report is larger than requested amount of data, rest of report * is lost! */ len = device->report_size > count ? count : device->report_size; if (copy_to_user(buffer, report->value, len)) { retval = -EFAULT; goto exit_unlock; } ... The roccat_read() function receives the device->cbuf report and delivers it to the user through copy_to_user(). If the N+ROCCAT_CBUF_SIZE th report is received while copying of the Nth report->value is in progress, the pointer that copy_to_user() is working on is kfree()ed and UAF read may occur. (race condition) Since the device node of this driver does not set separate permissions, this is not a security vulnerability, but because it is used for requesting screen display of profile or dpi settings, a user using the roccat device can apply udev to this device node or There is a possibility to use it by giving. Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com> Signed-off-by: Jiri Kosina <jkosina@suse.cz>
2022-09-04 22:31:15 +03:00
mutex_unlock(&device->cbuf_lock);
wake_up_interruptible(&device->wait);
return 0;
}
EXPORT_SYMBOL_GPL(roccat_report_event);
/*
* roccat_connect() - create a char device for special event output
* @class: the class thats used to create the device. Meant to hold device
* specific sysfs attributes.
* @hid: the hid device the char device should be connected to.
* @report_size: size of reports
*
* Return value is minor device number in Range [0, ROCCAT_MAX_DEVICES] on
* success, a negative error code on failure.
*/
int roccat_connect(struct class *klass, struct hid_device *hid, int report_size)
{
unsigned int minor;
struct roccat_device *device;
int temp;
device = kzalloc(sizeof(struct roccat_device), GFP_KERNEL);
if (!device)
return -ENOMEM;
mutex_lock(&devices_lock);
for (minor = 0; minor < ROCCAT_MAX_DEVICES; ++minor) {
if (devices[minor])
continue;
break;
}
if (minor < ROCCAT_MAX_DEVICES) {
devices[minor] = device;
} else {
mutex_unlock(&devices_lock);
kfree(device);
return -EINVAL;
}
device->dev = device_create(klass, &hid->dev,
MKDEV(roccat_major, minor), NULL,
"%s%s%d", "roccat", hid->driver->name, minor);
if (IS_ERR(device->dev)) {
devices[minor] = NULL;
mutex_unlock(&devices_lock);
temp = PTR_ERR(device->dev);
kfree(device);
return temp;
}
mutex_unlock(&devices_lock);
init_waitqueue_head(&device->wait);
INIT_LIST_HEAD(&device->readers);
mutex_init(&device->readers_lock);
mutex_init(&device->cbuf_lock);
device->minor = minor;
device->hid = hid;
device->exist = 1;
device->cbuf_end = 0;
device->report_size = report_size;
return minor;
}
EXPORT_SYMBOL_GPL(roccat_connect);
/* roccat_disconnect() - remove char device from hid device
* @minor: the minor device number returned by roccat_connect()
*/
void roccat_disconnect(int minor)
{
struct roccat_device *device;
mutex_lock(&devices_lock);
device = devices[minor];
mutex_unlock(&devices_lock);
device->exist = 0; /* TODO exist maybe not needed */
device_destroy(device->dev->class, MKDEV(roccat_major, minor));
mutex_lock(&devices_lock);
devices[minor] = NULL;
mutex_unlock(&devices_lock);
if (device->open) {
hid_hw_close(device->hid);
wake_up_interruptible(&device->wait);
} else {
kfree(device);
}
}
EXPORT_SYMBOL_GPL(roccat_disconnect);
static long roccat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct inode *inode = file_inode(file);
struct roccat_device *device;
unsigned int minor = iminor(inode);
long retval = 0;
mutex_lock(&devices_lock);
device = devices[minor];
if (!device) {
retval = -ENODEV;
goto out;
}
switch (cmd) {
case ROCCATIOCGREPSIZE:
if (put_user(device->report_size, (int __user *)arg))
retval = -EFAULT;
break;
default:
retval = -ENOTTY;
}
out:
mutex_unlock(&devices_lock);
return retval;
}
static const struct file_operations roccat_ops = {
.owner = THIS_MODULE,
.read = roccat_read,
.poll = roccat_poll,
.open = roccat_open,
.release = roccat_release,
llseek: automatically add .llseek fop All file_operations should get a .llseek operation so we can make nonseekable_open the default for future file operations without a .llseek pointer. The three cases that we can automatically detect are no_llseek, seq_lseek and default_llseek. For cases where we can we can automatically prove that the file offset is always ignored, we use noop_llseek, which maintains the current behavior of not returning an error from a seek. New drivers should normally not use noop_llseek but instead use no_llseek and call nonseekable_open at open time. Existing drivers can be converted to do the same when the maintainer knows for certain that no user code relies on calling seek on the device file. The generated code is often incorrectly indented and right now contains comments that clarify for each added line why a specific variant was chosen. In the version that gets submitted upstream, the comments will be gone and I will manually fix the indentation, because there does not seem to be a way to do that using coccinelle. Some amount of new code is currently sitting in linux-next that should get the same modifications, which I will do at the end of the merge window. Many thanks to Julia Lawall for helping me learn to write a semantic patch that does all this. ===== begin semantic patch ===== // This adds an llseek= method to all file operations, // as a preparation for making no_llseek the default. // // The rules are // - use no_llseek explicitly if we do nonseekable_open // - use seq_lseek for sequential files // - use default_llseek if we know we access f_pos // - use noop_llseek if we know we don't access f_pos, // but we still want to allow users to call lseek // @ open1 exists @ identifier nested_open; @@ nested_open(...) { <+... nonseekable_open(...) ...+> } @ open exists@ identifier open_f; identifier i, f; identifier open1.nested_open; @@ int open_f(struct inode *i, struct file *f) { <+... ( nonseekable_open(...) | nested_open(...) ) ...+> } @ read disable optional_qualifier exists @ identifier read_f; identifier f, p, s, off; type ssize_t, size_t, loff_t; expression E; identifier func; @@ ssize_t read_f(struct file *f, char *p, size_t s, loff_t *off) { <+... ( *off = E | *off += E | func(..., off, ...) | E = *off ) ...+> } @ read_no_fpos disable optional_qualifier exists @ identifier read_f; identifier f, p, s, off; type ssize_t, size_t, loff_t; @@ ssize_t read_f(struct file *f, char *p, size_t s, loff_t *off) { ... when != off } @ write @ identifier write_f; identifier f, p, s, off; type ssize_t, size_t, loff_t; expression E; identifier func; @@ ssize_t write_f(struct file *f, const char *p, size_t s, loff_t *off) { <+... ( *off = E | *off += E | func(..., off, ...) | E = *off ) ...+> } @ write_no_fpos @ identifier write_f; identifier f, p, s, off; type ssize_t, size_t, loff_t; @@ ssize_t write_f(struct file *f, const char *p, size_t s, loff_t *off) { ... when != off } @ fops0 @ identifier fops; @@ struct file_operations fops = { ... }; @ has_llseek depends on fops0 @ identifier fops0.fops; identifier llseek_f; @@ struct file_operations fops = { ... .llseek = llseek_f, ... }; @ has_read depends on fops0 @ identifier fops0.fops; identifier read_f; @@ struct file_operations fops = { ... .read = read_f, ... }; @ has_write depends on fops0 @ identifier fops0.fops; identifier write_f; @@ struct file_operations fops = { ... .write = write_f, ... }; @ has_open depends on fops0 @ identifier fops0.fops; identifier open_f; @@ struct file_operations fops = { ... .open = open_f, ... }; // use no_llseek if we call nonseekable_open //////////////////////////////////////////// @ nonseekable1 depends on !has_llseek && has_open @ identifier fops0.fops; identifier nso ~= "nonseekable_open"; @@ struct file_operations fops = { ... .open = nso, ... +.llseek = no_llseek, /* nonseekable */ }; @ nonseekable2 depends on !has_llseek @ identifier fops0.fops; identifier open.open_f; @@ struct file_operations fops = { ... .open = open_f, ... +.llseek = no_llseek, /* open uses nonseekable */ }; // use seq_lseek for sequential files ///////////////////////////////////// @ seq depends on !has_llseek @ identifier fops0.fops; identifier sr ~= "seq_read"; @@ struct file_operations fops = { ... .read = sr, ... +.llseek = seq_lseek, /* we have seq_read */ }; // use default_llseek if there is a readdir /////////////////////////////////////////// @ fops1 depends on !has_llseek && !nonseekable1 && !nonseekable2 && !seq @ identifier fops0.fops; identifier readdir_e; @@ // any other fop is used that changes pos struct file_operations fops = { ... .readdir = readdir_e, ... +.llseek = default_llseek, /* readdir is present */ }; // use default_llseek if at least one of read/write touches f_pos ///////////////////////////////////////////////////////////////// @ fops2 depends on !fops1 && !has_llseek && !nonseekable1 && !nonseekable2 && !seq @ identifier fops0.fops; identifier read.read_f; @@ // read fops use offset struct file_operations fops = { ... .read = read_f, ... +.llseek = default_llseek, /* read accesses f_pos */ }; @ fops3 depends on !fops1 && !fops2 && !has_llseek && !nonseekable1 && !nonseekable2 && !seq @ identifier fops0.fops; identifier write.write_f; @@ // write fops use offset struct file_operations fops = { ... .write = write_f, ... + .llseek = default_llseek, /* write accesses f_pos */ }; // Use noop_llseek if neither read nor write accesses f_pos /////////////////////////////////////////////////////////// @ fops4 depends on !fops1 && !fops2 && !fops3 && !has_llseek && !nonseekable1 && !nonseekable2 && !seq @ identifier fops0.fops; identifier read_no_fpos.read_f; identifier write_no_fpos.write_f; @@ // write fops use offset struct file_operations fops = { ... .write = write_f, .read = read_f, ... +.llseek = noop_llseek, /* read and write both use no f_pos */ }; @ depends on has_write && !has_read && !fops1 && !fops2 && !has_llseek && !nonseekable1 && !nonseekable2 && !seq @ identifier fops0.fops; identifier write_no_fpos.write_f; @@ struct file_operations fops = { ... .write = write_f, ... +.llseek = noop_llseek, /* write uses no f_pos */ }; @ depends on has_read && !has_write && !fops1 && !fops2 && !has_llseek && !nonseekable1 && !nonseekable2 && !seq @ identifier fops0.fops; identifier read_no_fpos.read_f; @@ struct file_operations fops = { ... .read = read_f, ... +.llseek = noop_llseek, /* read uses no f_pos */ }; @ depends on !has_read && !has_write && !fops1 && !fops2 && !has_llseek && !nonseekable1 && !nonseekable2 && !seq @ identifier fops0.fops; @@ struct file_operations fops = { ... +.llseek = noop_llseek, /* no read or write fn */ }; ===== End semantic patch ===== Signed-off-by: Arnd Bergmann <arnd@arndb.de> Cc: Julia Lawall <julia@diku.dk> Cc: Christoph Hellwig <hch@infradead.org>
2010-08-15 20:52:59 +04:00
.llseek = noop_llseek,
.unlocked_ioctl = roccat_ioctl,
};
static int __init roccat_init(void)
{
int retval;
dev_t dev_id;
retval = alloc_chrdev_region(&dev_id, ROCCAT_FIRST_MINOR,
ROCCAT_MAX_DEVICES, "roccat");
if (retval < 0) {
pr_warn("can't get major number\n");
goto error;
}
roccat_major = MAJOR(dev_id);
cdev_init(&roccat_cdev, &roccat_ops);
retval = cdev_add(&roccat_cdev, dev_id, ROCCAT_MAX_DEVICES);
if (retval < 0) {
pr_warn("cannot add cdev\n");
goto cleanup_alloc_chrdev_region;
}
return 0;
cleanup_alloc_chrdev_region:
unregister_chrdev_region(dev_id, ROCCAT_MAX_DEVICES);
error:
return retval;
}
static void __exit roccat_exit(void)
{
dev_t dev_id = MKDEV(roccat_major, 0);
cdev_del(&roccat_cdev);
unregister_chrdev_region(dev_id, ROCCAT_MAX_DEVICES);
}
module_init(roccat_init);
module_exit(roccat_exit);
MODULE_AUTHOR("Stefan Achatz");
MODULE_DESCRIPTION("USB Roccat char device");
MODULE_LICENSE("GPL v2");