mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2024-10-30 08:02:30 +00:00
8ac33b8b68
A race condition is possible when writing to events_queue_size where the
events kfifo is freed during the execution of a kfifo_in(), resulting in
a use-after-free. This patch prevents such a scenario by protecting the
events queue in operation with a spinlock and locking before performing
the events queue size adjustment.
The existing events_lock mutex is renamed to events_out_lock to reflect
that it only protects events queue out operations. Because the events
queue in operations can occur in an interrupt context, a new
events_in_lock spinlock is introduced and utilized.
Fixes: feff17a550
("counter: Implement events_queue_size sysfs attribute")
Cc: David Lechner <david@lechnology.com>
Signed-off-by: William Breathitt Gray <vilhelm.gray@gmail.com>
Link: https://lore.kernel.org/r/20211021103540.955639-1-vilhelm.gray@gmail.com
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
573 lines
14 KiB
C
573 lines
14 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Generic Counter character device interface
|
|
* Copyright (C) 2020 William Breathitt Gray
|
|
*/
|
|
#include <linux/cdev.h>
|
|
#include <linux/counter.h>
|
|
#include <linux/err.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/export.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/kfifo.h>
|
|
#include <linux/list.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/nospec.h>
|
|
#include <linux/poll.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/timekeeping.h>
|
|
#include <linux/types.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/wait.h>
|
|
|
|
#include "counter-chrdev.h"
|
|
|
|
struct counter_comp_node {
|
|
struct list_head l;
|
|
struct counter_component component;
|
|
struct counter_comp comp;
|
|
void *parent;
|
|
};
|
|
|
|
#define counter_comp_read_is_equal(a, b) \
|
|
(a.action_read == b.action_read || \
|
|
a.device_u8_read == b.device_u8_read || \
|
|
a.count_u8_read == b.count_u8_read || \
|
|
a.signal_u8_read == b.signal_u8_read || \
|
|
a.device_u32_read == b.device_u32_read || \
|
|
a.count_u32_read == b.count_u32_read || \
|
|
a.signal_u32_read == b.signal_u32_read || \
|
|
a.device_u64_read == b.device_u64_read || \
|
|
a.count_u64_read == b.count_u64_read || \
|
|
a.signal_u64_read == b.signal_u64_read)
|
|
|
|
#define counter_comp_read_is_set(comp) \
|
|
(comp.action_read || \
|
|
comp.device_u8_read || \
|
|
comp.count_u8_read || \
|
|
comp.signal_u8_read || \
|
|
comp.device_u32_read || \
|
|
comp.count_u32_read || \
|
|
comp.signal_u32_read || \
|
|
comp.device_u64_read || \
|
|
comp.count_u64_read || \
|
|
comp.signal_u64_read)
|
|
|
|
static ssize_t counter_chrdev_read(struct file *filp, char __user *buf,
|
|
size_t len, loff_t *f_ps)
|
|
{
|
|
struct counter_device *const counter = filp->private_data;
|
|
int err;
|
|
unsigned int copied;
|
|
|
|
if (!counter->ops)
|
|
return -ENODEV;
|
|
|
|
if (len < sizeof(struct counter_event))
|
|
return -EINVAL;
|
|
|
|
do {
|
|
if (kfifo_is_empty(&counter->events)) {
|
|
if (filp->f_flags & O_NONBLOCK)
|
|
return -EAGAIN;
|
|
|
|
err = wait_event_interruptible(counter->events_wait,
|
|
!kfifo_is_empty(&counter->events) ||
|
|
!counter->ops);
|
|
if (err < 0)
|
|
return err;
|
|
if (!counter->ops)
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (mutex_lock_interruptible(&counter->events_out_lock))
|
|
return -ERESTARTSYS;
|
|
err = kfifo_to_user(&counter->events, buf, len, &copied);
|
|
mutex_unlock(&counter->events_out_lock);
|
|
if (err < 0)
|
|
return err;
|
|
} while (!copied);
|
|
|
|
return copied;
|
|
}
|
|
|
|
static __poll_t counter_chrdev_poll(struct file *filp,
|
|
struct poll_table_struct *pollt)
|
|
{
|
|
struct counter_device *const counter = filp->private_data;
|
|
__poll_t events = 0;
|
|
|
|
if (!counter->ops)
|
|
return events;
|
|
|
|
poll_wait(filp, &counter->events_wait, pollt);
|
|
|
|
if (!kfifo_is_empty(&counter->events))
|
|
events = EPOLLIN | EPOLLRDNORM;
|
|
|
|
return events;
|
|
}
|
|
|
|
static void counter_events_list_free(struct list_head *const events_list)
|
|
{
|
|
struct counter_event_node *p, *n;
|
|
struct counter_comp_node *q, *o;
|
|
|
|
list_for_each_entry_safe(p, n, events_list, l) {
|
|
/* Free associated component nodes */
|
|
list_for_each_entry_safe(q, o, &p->comp_list, l) {
|
|
list_del(&q->l);
|
|
kfree(q);
|
|
}
|
|
|
|
/* Free event node */
|
|
list_del(&p->l);
|
|
kfree(p);
|
|
}
|
|
}
|
|
|
|
static int counter_set_event_node(struct counter_device *const counter,
|
|
struct counter_watch *const watch,
|
|
const struct counter_comp_node *const cfg)
|
|
{
|
|
struct counter_event_node *event_node;
|
|
int err = 0;
|
|
struct counter_comp_node *comp_node;
|
|
|
|
/* Search for event in the list */
|
|
list_for_each_entry(event_node, &counter->next_events_list, l)
|
|
if (event_node->event == watch->event &&
|
|
event_node->channel == watch->channel)
|
|
break;
|
|
|
|
/* If event is not already in the list */
|
|
if (&event_node->l == &counter->next_events_list) {
|
|
/* Allocate new event node */
|
|
event_node = kmalloc(sizeof(*event_node), GFP_KERNEL);
|
|
if (!event_node)
|
|
return -ENOMEM;
|
|
|
|
/* Configure event node and add to the list */
|
|
event_node->event = watch->event;
|
|
event_node->channel = watch->channel;
|
|
INIT_LIST_HEAD(&event_node->comp_list);
|
|
list_add(&event_node->l, &counter->next_events_list);
|
|
}
|
|
|
|
/* Check if component watch has already been set before */
|
|
list_for_each_entry(comp_node, &event_node->comp_list, l)
|
|
if (comp_node->parent == cfg->parent &&
|
|
counter_comp_read_is_equal(comp_node->comp, cfg->comp)) {
|
|
err = -EINVAL;
|
|
goto exit_free_event_node;
|
|
}
|
|
|
|
/* Allocate component node */
|
|
comp_node = kmalloc(sizeof(*comp_node), GFP_KERNEL);
|
|
if (!comp_node) {
|
|
err = -ENOMEM;
|
|
goto exit_free_event_node;
|
|
}
|
|
*comp_node = *cfg;
|
|
|
|
/* Add component node to event node */
|
|
list_add_tail(&comp_node->l, &event_node->comp_list);
|
|
|
|
exit_free_event_node:
|
|
/* Free event node if no one else is watching */
|
|
if (list_empty(&event_node->comp_list)) {
|
|
list_del(&event_node->l);
|
|
kfree(event_node);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int counter_enable_events(struct counter_device *const counter)
|
|
{
|
|
unsigned long flags;
|
|
int err = 0;
|
|
|
|
mutex_lock(&counter->n_events_list_lock);
|
|
spin_lock_irqsave(&counter->events_list_lock, flags);
|
|
|
|
counter_events_list_free(&counter->events_list);
|
|
list_replace_init(&counter->next_events_list,
|
|
&counter->events_list);
|
|
|
|
if (counter->ops->events_configure)
|
|
err = counter->ops->events_configure(counter);
|
|
|
|
spin_unlock_irqrestore(&counter->events_list_lock, flags);
|
|
mutex_unlock(&counter->n_events_list_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int counter_disable_events(struct counter_device *const counter)
|
|
{
|
|
unsigned long flags;
|
|
int err = 0;
|
|
|
|
spin_lock_irqsave(&counter->events_list_lock, flags);
|
|
|
|
counter_events_list_free(&counter->events_list);
|
|
|
|
if (counter->ops->events_configure)
|
|
err = counter->ops->events_configure(counter);
|
|
|
|
spin_unlock_irqrestore(&counter->events_list_lock, flags);
|
|
|
|
mutex_lock(&counter->n_events_list_lock);
|
|
|
|
counter_events_list_free(&counter->next_events_list);
|
|
|
|
mutex_unlock(&counter->n_events_list_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int counter_add_watch(struct counter_device *const counter,
|
|
const unsigned long arg)
|
|
{
|
|
void __user *const uwatch = (void __user *)arg;
|
|
struct counter_watch watch;
|
|
struct counter_comp_node comp_node = {};
|
|
size_t parent, id;
|
|
struct counter_comp *ext;
|
|
size_t num_ext;
|
|
int err = 0;
|
|
|
|
if (copy_from_user(&watch, uwatch, sizeof(watch)))
|
|
return -EFAULT;
|
|
|
|
if (watch.component.type == COUNTER_COMPONENT_NONE)
|
|
goto no_component;
|
|
|
|
parent = watch.component.parent;
|
|
|
|
/* Configure parent component info for comp node */
|
|
switch (watch.component.scope) {
|
|
case COUNTER_SCOPE_DEVICE:
|
|
ext = counter->ext;
|
|
num_ext = counter->num_ext;
|
|
break;
|
|
case COUNTER_SCOPE_SIGNAL:
|
|
if (parent >= counter->num_signals)
|
|
return -EINVAL;
|
|
parent = array_index_nospec(parent, counter->num_signals);
|
|
|
|
comp_node.parent = counter->signals + parent;
|
|
|
|
ext = counter->signals[parent].ext;
|
|
num_ext = counter->signals[parent].num_ext;
|
|
break;
|
|
case COUNTER_SCOPE_COUNT:
|
|
if (parent >= counter->num_counts)
|
|
return -EINVAL;
|
|
parent = array_index_nospec(parent, counter->num_counts);
|
|
|
|
comp_node.parent = counter->counts + parent;
|
|
|
|
ext = counter->counts[parent].ext;
|
|
num_ext = counter->counts[parent].num_ext;
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
id = watch.component.id;
|
|
|
|
/* Configure component info for comp node */
|
|
switch (watch.component.type) {
|
|
case COUNTER_COMPONENT_SIGNAL:
|
|
if (watch.component.scope != COUNTER_SCOPE_SIGNAL)
|
|
return -EINVAL;
|
|
|
|
comp_node.comp.type = COUNTER_COMP_SIGNAL_LEVEL;
|
|
comp_node.comp.signal_u32_read = counter->ops->signal_read;
|
|
break;
|
|
case COUNTER_COMPONENT_COUNT:
|
|
if (watch.component.scope != COUNTER_SCOPE_COUNT)
|
|
return -EINVAL;
|
|
|
|
comp_node.comp.type = COUNTER_COMP_U64;
|
|
comp_node.comp.count_u64_read = counter->ops->count_read;
|
|
break;
|
|
case COUNTER_COMPONENT_FUNCTION:
|
|
if (watch.component.scope != COUNTER_SCOPE_COUNT)
|
|
return -EINVAL;
|
|
|
|
comp_node.comp.type = COUNTER_COMP_FUNCTION;
|
|
comp_node.comp.count_u32_read = counter->ops->function_read;
|
|
break;
|
|
case COUNTER_COMPONENT_SYNAPSE_ACTION:
|
|
if (watch.component.scope != COUNTER_SCOPE_COUNT)
|
|
return -EINVAL;
|
|
if (id >= counter->counts[parent].num_synapses)
|
|
return -EINVAL;
|
|
id = array_index_nospec(id, counter->counts[parent].num_synapses);
|
|
|
|
comp_node.comp.type = COUNTER_COMP_SYNAPSE_ACTION;
|
|
comp_node.comp.action_read = counter->ops->action_read;
|
|
comp_node.comp.priv = counter->counts[parent].synapses + id;
|
|
break;
|
|
case COUNTER_COMPONENT_EXTENSION:
|
|
if (id >= num_ext)
|
|
return -EINVAL;
|
|
id = array_index_nospec(id, num_ext);
|
|
|
|
comp_node.comp = ext[id];
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
if (!counter_comp_read_is_set(comp_node.comp))
|
|
return -EOPNOTSUPP;
|
|
|
|
no_component:
|
|
mutex_lock(&counter->n_events_list_lock);
|
|
|
|
if (counter->ops->watch_validate) {
|
|
err = counter->ops->watch_validate(counter, &watch);
|
|
if (err < 0)
|
|
goto err_exit;
|
|
}
|
|
|
|
comp_node.component = watch.component;
|
|
|
|
err = counter_set_event_node(counter, &watch, &comp_node);
|
|
|
|
err_exit:
|
|
mutex_unlock(&counter->n_events_list_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static long counter_chrdev_ioctl(struct file *filp, unsigned int cmd,
|
|
unsigned long arg)
|
|
{
|
|
struct counter_device *const counter = filp->private_data;
|
|
int ret = -ENODEV;
|
|
|
|
mutex_lock(&counter->ops_exist_lock);
|
|
|
|
if (!counter->ops)
|
|
goto out_unlock;
|
|
|
|
switch (cmd) {
|
|
case COUNTER_ADD_WATCH_IOCTL:
|
|
ret = counter_add_watch(counter, arg);
|
|
break;
|
|
case COUNTER_ENABLE_EVENTS_IOCTL:
|
|
ret = counter_enable_events(counter);
|
|
break;
|
|
case COUNTER_DISABLE_EVENTS_IOCTL:
|
|
ret = counter_disable_events(counter);
|
|
break;
|
|
default:
|
|
ret = -ENOIOCTLCMD;
|
|
break;
|
|
}
|
|
|
|
out_unlock:
|
|
mutex_unlock(&counter->ops_exist_lock);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int counter_chrdev_open(struct inode *inode, struct file *filp)
|
|
{
|
|
struct counter_device *const counter = container_of(inode->i_cdev,
|
|
typeof(*counter),
|
|
chrdev);
|
|
|
|
get_device(&counter->dev);
|
|
filp->private_data = counter;
|
|
|
|
return nonseekable_open(inode, filp);
|
|
}
|
|
|
|
static int counter_chrdev_release(struct inode *inode, struct file *filp)
|
|
{
|
|
struct counter_device *const counter = filp->private_data;
|
|
int ret = 0;
|
|
|
|
mutex_lock(&counter->ops_exist_lock);
|
|
|
|
if (!counter->ops) {
|
|
/* Free any lingering held memory */
|
|
counter_events_list_free(&counter->events_list);
|
|
counter_events_list_free(&counter->next_events_list);
|
|
ret = -ENODEV;
|
|
goto out_unlock;
|
|
}
|
|
|
|
ret = counter_disable_events(counter);
|
|
if (ret < 0) {
|
|
mutex_unlock(&counter->ops_exist_lock);
|
|
return ret;
|
|
}
|
|
|
|
out_unlock:
|
|
mutex_unlock(&counter->ops_exist_lock);
|
|
|
|
put_device(&counter->dev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static const struct file_operations counter_fops = {
|
|
.owner = THIS_MODULE,
|
|
.llseek = no_llseek,
|
|
.read = counter_chrdev_read,
|
|
.poll = counter_chrdev_poll,
|
|
.unlocked_ioctl = counter_chrdev_ioctl,
|
|
.open = counter_chrdev_open,
|
|
.release = counter_chrdev_release,
|
|
};
|
|
|
|
int counter_chrdev_add(struct counter_device *const counter)
|
|
{
|
|
/* Initialize Counter events lists */
|
|
INIT_LIST_HEAD(&counter->events_list);
|
|
INIT_LIST_HEAD(&counter->next_events_list);
|
|
spin_lock_init(&counter->events_list_lock);
|
|
mutex_init(&counter->n_events_list_lock);
|
|
init_waitqueue_head(&counter->events_wait);
|
|
spin_lock_init(&counter->events_in_lock);
|
|
mutex_init(&counter->events_out_lock);
|
|
|
|
/* Initialize character device */
|
|
cdev_init(&counter->chrdev, &counter_fops);
|
|
|
|
/* Allocate Counter events queue */
|
|
return kfifo_alloc(&counter->events, 64, GFP_KERNEL);
|
|
}
|
|
|
|
void counter_chrdev_remove(struct counter_device *const counter)
|
|
{
|
|
kfifo_free(&counter->events);
|
|
}
|
|
|
|
static int counter_get_data(struct counter_device *const counter,
|
|
const struct counter_comp_node *const comp_node,
|
|
u64 *const value)
|
|
{
|
|
const struct counter_comp *const comp = &comp_node->comp;
|
|
void *const parent = comp_node->parent;
|
|
u8 value_u8 = 0;
|
|
u32 value_u32 = 0;
|
|
int ret;
|
|
|
|
if (comp_node->component.type == COUNTER_COMPONENT_NONE)
|
|
return 0;
|
|
|
|
switch (comp->type) {
|
|
case COUNTER_COMP_U8:
|
|
case COUNTER_COMP_BOOL:
|
|
switch (comp_node->component.scope) {
|
|
case COUNTER_SCOPE_DEVICE:
|
|
ret = comp->device_u8_read(counter, &value_u8);
|
|
break;
|
|
case COUNTER_SCOPE_SIGNAL:
|
|
ret = comp->signal_u8_read(counter, parent, &value_u8);
|
|
break;
|
|
case COUNTER_SCOPE_COUNT:
|
|
ret = comp->count_u8_read(counter, parent, &value_u8);
|
|
break;
|
|
}
|
|
*value = value_u8;
|
|
return ret;
|
|
case COUNTER_COMP_SIGNAL_LEVEL:
|
|
case COUNTER_COMP_FUNCTION:
|
|
case COUNTER_COMP_ENUM:
|
|
case COUNTER_COMP_COUNT_DIRECTION:
|
|
case COUNTER_COMP_COUNT_MODE:
|
|
switch (comp_node->component.scope) {
|
|
case COUNTER_SCOPE_DEVICE:
|
|
ret = comp->device_u32_read(counter, &value_u32);
|
|
break;
|
|
case COUNTER_SCOPE_SIGNAL:
|
|
ret = comp->signal_u32_read(counter, parent,
|
|
&value_u32);
|
|
break;
|
|
case COUNTER_SCOPE_COUNT:
|
|
ret = comp->count_u32_read(counter, parent, &value_u32);
|
|
break;
|
|
}
|
|
*value = value_u32;
|
|
return ret;
|
|
case COUNTER_COMP_U64:
|
|
switch (comp_node->component.scope) {
|
|
case COUNTER_SCOPE_DEVICE:
|
|
return comp->device_u64_read(counter, value);
|
|
case COUNTER_SCOPE_SIGNAL:
|
|
return comp->signal_u64_read(counter, parent, value);
|
|
case COUNTER_SCOPE_COUNT:
|
|
return comp->count_u64_read(counter, parent, value);
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
case COUNTER_COMP_SYNAPSE_ACTION:
|
|
ret = comp->action_read(counter, parent, comp->priv,
|
|
&value_u32);
|
|
*value = value_u32;
|
|
return ret;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* counter_push_event - queue event for userspace reading
|
|
* @counter: pointer to Counter structure
|
|
* @event: triggered event
|
|
* @channel: event channel
|
|
*
|
|
* Note: If no one is watching for the respective event, it is silently
|
|
* discarded.
|
|
*/
|
|
void counter_push_event(struct counter_device *const counter, const u8 event,
|
|
const u8 channel)
|
|
{
|
|
struct counter_event ev;
|
|
unsigned int copied = 0;
|
|
unsigned long flags;
|
|
struct counter_event_node *event_node;
|
|
struct counter_comp_node *comp_node;
|
|
|
|
ev.timestamp = ktime_get_ns();
|
|
ev.watch.event = event;
|
|
ev.watch.channel = channel;
|
|
|
|
/* Could be in an interrupt context, so use a spin lock */
|
|
spin_lock_irqsave(&counter->events_list_lock, flags);
|
|
|
|
/* Search for event in the list */
|
|
list_for_each_entry(event_node, &counter->events_list, l)
|
|
if (event_node->event == event &&
|
|
event_node->channel == channel)
|
|
break;
|
|
|
|
/* If event is not in the list */
|
|
if (&event_node->l == &counter->events_list)
|
|
goto exit_early;
|
|
|
|
/* Read and queue relevant comp for userspace */
|
|
list_for_each_entry(comp_node, &event_node->comp_list, l) {
|
|
ev.watch.component = comp_node->component;
|
|
ev.status = -counter_get_data(counter, comp_node, &ev.value);
|
|
|
|
copied += kfifo_in_spinlocked_noirqsave(&counter->events, &ev,
|
|
1, &counter->events_in_lock);
|
|
}
|
|
|
|
exit_early:
|
|
spin_unlock_irqrestore(&counter->events_list_lock, flags);
|
|
|
|
if (copied)
|
|
wake_up_poll(&counter->events_wait, EPOLLIN);
|
|
}
|
|
EXPORT_SYMBOL_GPL(counter_push_event);
|