| // 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); |