#include <linux/device.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/percpu.h>
#include <asm/sections.h>
#include "base.h"
#include "trace.h"
struct devres {
struct devres_node node;
dr_release_t release;
u8 __aligned(ARCH_DMA_MINALIGN) data[];
};
struct devres_group {
struct devres_node node[2];
void *id;
int color;
};
void devres_node_init(struct devres_node *node,
dr_node_release_t release,
dr_node_free_t free_node)
{
INIT_LIST_HEAD(&node->entry);
node->release = release;
node->free_node = free_node;
}
static inline void free_node(struct devres_node *node)
{
node->free_node(node);
}
void devres_set_node_dbginfo(struct devres_node *node, const char *name,
size_t size)
{
node->name = name;
node->size = size;
}
#ifdef CONFIG_DEBUG_DEVRES
static int log_devres = 0;
module_param_named(log, log_devres, int, S_IRUGO | S_IWUSR);
static void devres_dbg(struct device *dev, struct devres_node *node,
const char *op)
{
if (unlikely(log_devres))
dev_err(dev, "DEVRES %3s %p %s (%zu bytes)\n",
op, node, node->name, node->size);
}
#else
#define devres_dbg(dev, node, op) do {} while (0)
#endif
static void devres_log(struct device *dev, struct devres_node *node,
const char *op)
{
trace_devres_log(dev, op, node, node->name, node->size);
devres_dbg(dev, node, op);
}
static void group_open_release(struct device *dev, struct devres_node *node)
{
}
static void group_close_release(struct device *dev, struct devres_node *node)
{
}
static struct devres_group *node_to_group(struct devres_node *node)
{
if (node->release == &group_open_release)
return container_of(node, struct devres_group, node[0]);
if (node->release == &group_close_release)
return container_of(node, struct devres_group, node[1]);
return NULL;
}
static bool check_dr_size(size_t size, size_t *tot_size)
{
if (unlikely(check_add_overflow(sizeof(struct devres),
size, tot_size)))
return false;
*tot_size = kmalloc_size_roundup(*tot_size);
return true;
}
static void dr_node_release(struct device *dev, struct devres_node *node)
{
struct devres *dr = container_of(node, struct devres, node);
dr->release(dev, dr->data);
}
static void dr_node_free(struct devres_node *node)
{
struct devres *dr = container_of(node, struct devres, node);
kfree(dr);
}
static __always_inline struct devres *alloc_dr(dr_release_t release,
size_t size, gfp_t gfp, int nid)
{
size_t tot_size;
struct devres *dr;
if (!check_dr_size(size, &tot_size))
return NULL;
dr = kmalloc_node_track_caller(tot_size, gfp, nid);
if (unlikely(!dr))
return NULL;
if (!(gfp & __GFP_ZERO))
memset(dr, 0, offsetof(struct devres, data));
devres_node_init(&dr->node, dr_node_release, dr_node_free);
dr->release = release;
return dr;
}
static void add_dr(struct device *dev, struct devres_node *node)
{
devres_log(dev, node, "ADD");
BUG_ON(!list_empty(&node->entry));
list_add_tail(&node->entry, &dev->devres_head);
}
static void replace_dr(struct device *dev,
struct devres_node *old, struct devres_node *new)
{
devres_log(dev, old, "REPLACE");
BUG_ON(!list_empty(&new->entry));
list_replace(&old->entry, &new->entry);
}
void *__devres_alloc_node(dr_release_t release, size_t size, gfp_t gfp, int nid,
const char *name)
{
struct devres *dr;
dr = alloc_dr(release, size, gfp | __GFP_ZERO, nid);
if (unlikely(!dr))
return NULL;
devres_set_node_dbginfo(&dr->node, name, size);
return dr->data;
}
EXPORT_SYMBOL_GPL(__devres_alloc_node);
void devres_for_each_res(struct device *dev, dr_release_t release,
dr_match_t match, void *match_data,
void (*fn)(struct device *, void *, void *),
void *data)
{
struct devres_node *node;
struct devres_node *tmp;
if (!fn)
return;
guard(spinlock_irqsave)(&dev->devres_lock);
list_for_each_entry_safe_reverse(node, tmp,
&dev->devres_head, entry) {
struct devres *dr = container_of(node, struct devres, node);
if (node->release != dr_node_release)
continue;
if (dr->release != release)
continue;
if (match && !match(dev, dr->data, match_data))
continue;
fn(dev, dr->data, data);
}
}
EXPORT_SYMBOL_GPL(devres_for_each_res);
static inline void free_dr(struct devres *dr)
{
free_node(&dr->node);
}
void devres_free(void *res)
{
if (res) {
struct devres *dr = container_of(res, struct devres, data);
BUG_ON(!list_empty(&dr->node.entry));
free_dr(dr);
}
}
EXPORT_SYMBOL_GPL(devres_free);
void devres_node_add(struct device *dev, struct devres_node *node)
{
guard(spinlock_irqsave)(&dev->devres_lock);
add_dr(dev, node);
}
void devres_add(struct device *dev, void *res)
{
struct devres *dr = container_of(res, struct devres, data);
devres_node_add(dev, &dr->node);
}
EXPORT_SYMBOL_GPL(devres_add);
static struct devres *find_dr(struct device *dev, dr_release_t release,
dr_match_t match, void *match_data)
{
struct devres_node *node;
list_for_each_entry_reverse(node, &dev->devres_head, entry) {
struct devres *dr = container_of(node, struct devres, node);
if (node->release != dr_node_release)
continue;
if (dr->release != release)
continue;
if (match && !match(dev, dr->data, match_data))
continue;
return dr;
}
return NULL;
}
void *devres_find(struct device *dev, dr_release_t release,
dr_match_t match, void *match_data)
{
struct devres *dr;
guard(spinlock_irqsave)(&dev->devres_lock);
dr = find_dr(dev, release, match, match_data);
if (dr)
return dr->data;
return NULL;
}
EXPORT_SYMBOL_GPL(devres_find);
void *devres_get(struct device *dev, void *new_res,
dr_match_t match, void *match_data)
{
struct devres *new_dr = container_of(new_res, struct devres, data);
struct devres *dr;
unsigned long flags;
spin_lock_irqsave(&dev->devres_lock, flags);
dr = find_dr(dev, new_dr->release, match, match_data);
if (!dr) {
add_dr(dev, &new_dr->node);
dr = new_dr;
new_res = NULL;
}
spin_unlock_irqrestore(&dev->devres_lock, flags);
devres_free(new_res);
return dr->data;
}
EXPORT_SYMBOL_GPL(devres_get);
bool devres_node_remove(struct device *dev, struct devres_node *node)
{
struct devres_node *__node;
guard(spinlock_irqsave)(&dev->devres_lock);
list_for_each_entry_reverse(__node, &dev->devres_head, entry) {
if (__node == node) {
list_del_init(&node->entry);
devres_log(dev, node, "REM");
return true;
}
}
return false;
}
void *devres_remove(struct device *dev, dr_release_t release,
dr_match_t match, void *match_data)
{
struct devres *dr;
guard(spinlock_irqsave)(&dev->devres_lock);
dr = find_dr(dev, release, match, match_data);
if (dr) {
list_del_init(&dr->node.entry);
devres_log(dev, &dr->node, "REM");
return dr->data;
}
return NULL;
}
EXPORT_SYMBOL_GPL(devres_remove);
int devres_destroy(struct device *dev, dr_release_t release,
dr_match_t match, void *match_data)
{
void *res;
res = devres_remove(dev, release, match, match_data);
if (unlikely(!res))
return -ENOENT;
devres_free(res);
return 0;
}
EXPORT_SYMBOL_GPL(devres_destroy);
int devres_release(struct device *dev, dr_release_t release,
dr_match_t match, void *match_data)
{
void *res;
res = devres_remove(dev, release, match, match_data);
if (unlikely(!res))
return -ENOENT;
(*release)(dev, res);
devres_free(res);
return 0;
}
EXPORT_SYMBOL_GPL(devres_release);
static int remove_nodes(struct device *dev,
struct list_head *first, struct list_head *end,
struct list_head *todo)
{
struct devres_node *node, *n;
int cnt = 0, nr_groups = 0;
node = list_entry(first, struct devres_node, entry);
list_for_each_entry_safe_from(node, n, end, entry) {
struct devres_group *grp;
grp = node_to_group(node);
if (grp) {
grp->color = 0;
nr_groups++;
} else {
if (&node->entry == first)
first = first->next;
list_move_tail(&node->entry, todo);
cnt++;
}
}
if (!nr_groups)
return cnt;
node = list_entry(first, struct devres_node, entry);
list_for_each_entry_safe_from(node, n, end, entry) {
struct devres_group *grp;
grp = node_to_group(node);
BUG_ON(!grp || list_empty(&grp->node[0].entry));
grp->color++;
if (list_empty(&grp->node[1].entry))
grp->color++;
BUG_ON(grp->color <= 0 || grp->color > 2);
if (grp->color == 2) {
list_move_tail(&grp->node[0].entry, todo);
list_del_init(&grp->node[1].entry);
}
}
return cnt;
}
static void release_nodes(struct device *dev, struct list_head *todo)
{
struct devres_node *node, *tmp;
list_for_each_entry_safe_reverse(node, tmp, todo, entry) {
devres_log(dev, node, "REL");
node->release(dev, node);
free_node(node);
}
}
int devres_release_all(struct device *dev)
{
unsigned long flags;
LIST_HEAD(todo);
int cnt;
if (WARN_ON(dev->devres_head.next == NULL))
return -ENODEV;
if (list_empty(&dev->devres_head))
return 0;
spin_lock_irqsave(&dev->devres_lock, flags);
cnt = remove_nodes(dev, dev->devres_head.next, &dev->devres_head, &todo);
spin_unlock_irqrestore(&dev->devres_lock, flags);
release_nodes(dev, &todo);
return cnt;
}
static void devres_group_free(struct devres_node *node)
{
struct devres_group *grp = container_of(node, struct devres_group, node[0]);
kfree(grp);
}
void *devres_open_group(struct device *dev, void *id, gfp_t gfp)
{
struct devres_group *grp;
grp = kmalloc_obj(*grp, gfp);
if (unlikely(!grp))
return NULL;
devres_node_init(&grp->node[0], &group_open_release, devres_group_free);
devres_node_init(&grp->node[1], &group_close_release, NULL);
devres_set_node_dbginfo(&grp->node[0], "grp<", 0);
devres_set_node_dbginfo(&grp->node[1], "grp>", 0);
grp->id = grp;
if (id)
grp->id = id;
grp->color = 0;
devres_node_add(dev, &grp->node[0]);
return grp->id;
}
EXPORT_SYMBOL_GPL(devres_open_group);
static struct devres_group *find_group(struct device *dev, void *id)
{
struct devres_node *node;
list_for_each_entry_reverse(node, &dev->devres_head, entry) {
struct devres_group *grp;
if (node->release != &group_open_release)
continue;
grp = container_of(node, struct devres_group, node[0]);
if (id) {
if (grp->id == id)
return grp;
} else if (list_empty(&grp->node[1].entry))
return grp;
}
return NULL;
}
void devres_close_group(struct device *dev, void *id)
{
struct devres_group *grp;
guard(spinlock_irqsave)(&dev->devres_lock);
grp = find_group(dev, id);
if (grp)
add_dr(dev, &grp->node[1]);
else
WARN_ON(1);
}
EXPORT_SYMBOL_GPL(devres_close_group);
void devres_remove_group(struct device *dev, void *id)
{
struct devres_group *grp;
unsigned long flags;
spin_lock_irqsave(&dev->devres_lock, flags);
grp = find_group(dev, id);
if (grp) {
list_del_init(&grp->node[0].entry);
list_del_init(&grp->node[1].entry);
devres_log(dev, &grp->node[0], "REM");
} else
WARN_ON(1);
spin_unlock_irqrestore(&dev->devres_lock, flags);
kfree(grp);
}
EXPORT_SYMBOL_GPL(devres_remove_group);
int devres_release_group(struct device *dev, void *id)
{
struct devres_group *grp;
unsigned long flags;
LIST_HEAD(todo);
int cnt = 0;
spin_lock_irqsave(&dev->devres_lock, flags);
grp = find_group(dev, id);
if (grp) {
struct list_head *first = &grp->node[0].entry;
struct list_head *end = &dev->devres_head;
if (!list_empty(&grp->node[1].entry))
end = grp->node[1].entry.next;
cnt = remove_nodes(dev, first, end, &todo);
} else if (list_empty(&dev->devres_head)) {
} else {
WARN_ON(1);
}
spin_unlock_irqrestore(&dev->devres_lock, flags);
release_nodes(dev, &todo);
return cnt;
}
EXPORT_SYMBOL_GPL(devres_release_group);
struct action_devres {
void *data;
void (*action)(void *);
};
struct devres_action {
struct devres_node node;
struct action_devres action;
};
static int devm_action_match(struct devres_action *devres, struct action_devres *target)
{
return devres->action.action == target->action &&
devres->action.data == target->data;
}
static void devm_action_release(struct device *dev, struct devres_node *node)
{
struct devres_action *devres = container_of(node, struct devres_action, node);
devres->action.action(devres->action.data);
}
static void devm_action_free(struct devres_node *node)
{
struct devres_action *action = container_of(node, struct devres_action, node);
kfree(action);
}
int __devm_add_action(struct device *dev, void (*action)(void *), void *data, const char *name)
{
struct devres_action *devres;
devres = kzalloc_obj(*devres);
if (!devres)
return -ENOMEM;
devres_node_init(&devres->node, devm_action_release, devm_action_free);
devres_set_node_dbginfo(&devres->node, name, sizeof(*devres));
devres->action.data = data;
devres->action.action = action;
devres_node_add(dev, &devres->node);
return 0;
}
EXPORT_SYMBOL_GPL(__devm_add_action);
static struct devres_action *devres_action_find(struct device *dev,
void (*action)(void *),
void *data)
{
struct devres_node *node;
struct action_devres target = {
.data = data,
.action = action,
};
list_for_each_entry_reverse(node, &dev->devres_head, entry) {
struct devres_action *dr = container_of(node, struct devres_action, node);
if (node->release != devm_action_release)
continue;
if (devm_action_match(dr, &target))
return dr;
}
return NULL;
}
bool devm_is_action_added(struct device *dev, void (*action)(void *), void *data)
{
guard(spinlock_irqsave)(&dev->devres_lock);
return !!devres_action_find(dev, action, data);
}
EXPORT_SYMBOL_GPL(devm_is_action_added);
static struct devres_action *remove_action(struct device *dev,
void (*action)(void *),
void *data)
{
struct devres_action *dr;
guard(spinlock_irqsave)(&dev->devres_lock);
dr = devres_action_find(dev, action, data);
if (!dr)
return ERR_PTR(-ENOENT);
list_del_init(&dr->node.entry);
devres_log(dev, &dr->node, "REM");
return dr;
}
int devm_remove_action_nowarn(struct device *dev,
void (*action)(void *),
void *data)
{
struct devres_action *dr;
dr = remove_action(dev, action, data);
if (IS_ERR(dr))
return PTR_ERR(dr);
kfree(dr);
return 0;
}
EXPORT_SYMBOL_GPL(devm_remove_action_nowarn);
void devm_release_action(struct device *dev, void (*action)(void *), void *data)
{
struct devres_action *dr;
dr = remove_action(dev, action, data);
if (WARN_ON(IS_ERR(dr)))
return;
dr->action.action(dr->action.data);
kfree(dr);
}
EXPORT_SYMBOL_GPL(devm_release_action);
static void devm_kmalloc_release(struct device *dev, void *res)
{
}
static int devm_kmalloc_match(struct device *dev, void *res, void *data)
{
return res == data;
}
void *devm_kmalloc(struct device *dev, size_t size, gfp_t gfp)
{
struct devres *dr;
if (unlikely(!size))
return ZERO_SIZE_PTR;
dr = alloc_dr(devm_kmalloc_release, size, gfp, dev_to_node(dev));
if (unlikely(!dr))
return NULL;
devres_set_node_dbginfo(&dr->node, "devm_kzalloc_release", size);
devres_add(dev, dr->data);
return dr->data;
}
EXPORT_SYMBOL_GPL(devm_kmalloc);
void *devm_krealloc(struct device *dev, void *ptr, size_t new_size, gfp_t gfp)
{
size_t total_new_size, total_old_size;
struct devres *old_dr, *new_dr;
unsigned long flags;
if (unlikely(!new_size)) {
devm_kfree(dev, ptr);
return ZERO_SIZE_PTR;
}
if (unlikely(ZERO_OR_NULL_PTR(ptr)))
return devm_kmalloc(dev, new_size, gfp);
if (WARN_ON(is_kernel_rodata((unsigned long)ptr)))
return NULL;
if (!check_dr_size(new_size, &total_new_size))
return NULL;
total_old_size = ksize(container_of(ptr, struct devres, data));
if (total_old_size == 0) {
WARN(1, "Pointer doesn't point to dynamically allocated memory.");
return NULL;
}
if (total_new_size <= total_old_size)
return ptr;
new_dr = alloc_dr(devm_kmalloc_release,
new_size, gfp, dev_to_node(dev));
if (!new_dr)
return NULL;
devres_set_node_dbginfo(&new_dr->node, "devm_krealloc_release", new_size);
spin_lock_irqsave(&dev->devres_lock, flags);
old_dr = find_dr(dev, devm_kmalloc_release, devm_kmalloc_match, ptr);
if (!old_dr) {
spin_unlock_irqrestore(&dev->devres_lock, flags);
free_dr(new_dr);
WARN(1, "Memory chunk not managed or managed by a different device.");
return NULL;
}
replace_dr(dev, &old_dr->node, &new_dr->node);
spin_unlock_irqrestore(&dev->devres_lock, flags);
memcpy(new_dr->data, old_dr->data,
total_old_size - offsetof(struct devres, data));
free_dr(old_dr);
return new_dr->data;
}
EXPORT_SYMBOL_GPL(devm_krealloc);
char *devm_kstrdup(struct device *dev, const char *s, gfp_t gfp)
{
if (!s)
return NULL;
return devm_kmemdup(dev, s, strlen(s) + 1, gfp);
}
EXPORT_SYMBOL_GPL(devm_kstrdup);
const char *devm_kstrdup_const(struct device *dev, const char *s, gfp_t gfp)
{
if (is_kernel_rodata((unsigned long)s))
return s;
return devm_kstrdup(dev, s, gfp);
}
EXPORT_SYMBOL_GPL(devm_kstrdup_const);
char *devm_kvasprintf(struct device *dev, gfp_t gfp, const char *fmt,
va_list ap)
{
unsigned int len;
char *p;
va_list aq;
va_copy(aq, ap);
len = vsnprintf(NULL, 0, fmt, aq);
va_end(aq);
p = devm_kmalloc(dev, len+1, gfp);
if (!p)
return NULL;
vsnprintf(p, len+1, fmt, ap);
return p;
}
EXPORT_SYMBOL(devm_kvasprintf);
char *devm_kasprintf(struct device *dev, gfp_t gfp, const char *fmt, ...)
{
va_list ap;
char *p;
va_start(ap, fmt);
p = devm_kvasprintf(dev, gfp, fmt, ap);
va_end(ap);
return p;
}
EXPORT_SYMBOL_GPL(devm_kasprintf);
void devm_kfree(struct device *dev, const void *p)
{
int rc;
if (unlikely(is_kernel_rodata((unsigned long)p) || ZERO_OR_NULL_PTR(p)))
return;
rc = devres_destroy(dev, devm_kmalloc_release,
devm_kmalloc_match, (void *)p);
WARN_ON(rc);
}
EXPORT_SYMBOL_GPL(devm_kfree);
void *devm_kmemdup(struct device *dev, const void *src, size_t len, gfp_t gfp)
{
void *p;
p = devm_kmalloc(dev, len, gfp);
if (p)
memcpy(p, src, len);
return p;
}
EXPORT_SYMBOL_GPL(devm_kmemdup);
const void *
devm_kmemdup_const(struct device *dev, const void *src, size_t len, gfp_t gfp)
{
if (is_kernel_rodata((unsigned long)src))
return src;
return devm_kmemdup(dev, src, len, gfp);
}
EXPORT_SYMBOL_GPL(devm_kmemdup_const);
struct pages_devres {
unsigned long addr;
unsigned int order;
};
static int devm_pages_match(struct device *dev, void *res, void *p)
{
struct pages_devres *devres = res;
struct pages_devres *target = p;
return devres->addr == target->addr;
}
static void devm_pages_release(struct device *dev, void *res)
{
struct pages_devres *devres = res;
free_pages(devres->addr, devres->order);
}
unsigned long devm_get_free_pages(struct device *dev,
gfp_t gfp_mask, unsigned int order)
{
struct pages_devres *devres;
unsigned long addr;
addr = __get_free_pages(gfp_mask, order);
if (unlikely(!addr))
return 0;
devres = devres_alloc(devm_pages_release,
sizeof(struct pages_devres), GFP_KERNEL);
if (unlikely(!devres)) {
free_pages(addr, order);
return 0;
}
devres->addr = addr;
devres->order = order;
devres_add(dev, devres);
return addr;
}
EXPORT_SYMBOL_GPL(devm_get_free_pages);
void devm_free_pages(struct device *dev, unsigned long addr)
{
struct pages_devres devres = { .addr = addr };
WARN_ON(devres_release(dev, devm_pages_release, devm_pages_match,
&devres));
}
EXPORT_SYMBOL_GPL(devm_free_pages);
static void devm_percpu_release(struct device *dev, void *pdata)
{
void __percpu *p;
p = *(void __percpu **)pdata;
free_percpu(p);
}
void __percpu *__devm_alloc_percpu(struct device *dev, size_t size,
size_t align)
{
void *p;
void __percpu *pcpu;
pcpu = __alloc_percpu(size, align);
if (!pcpu)
return NULL;
p = devres_alloc(devm_percpu_release, sizeof(void *), GFP_KERNEL);
if (!p) {
free_percpu(pcpu);
return NULL;
}
*(void __percpu **)p = pcpu;
devres_add(dev, p);
return pcpu;
}
EXPORT_SYMBOL_GPL(__devm_alloc_percpu);