| From dba0ca701b521fc0b3337e4a25a57bdbe39ce43d Mon Sep 17 00:00:00 2001 |
| From: Peter Oberparleiter <oberpar@linux.vnet.ibm.com> |
| Date: Thu, 9 Sep 2010 16:37:35 -0700 |
| Subject: [PATCH] gcov: fix null-pointer dereference for certain module types |
| |
| commit 85a0fdfd0f967507f3903e8419bc7e408f5a59de upstream. |
| |
| The gcov-kernel infrastructure expects that each object file is loaded |
| only once. This may not be true, e.g. when loading multiple kernel |
| modules which are linked to the same object file. As a result, loading |
| such kernel modules will result in incorrect gcov results while unloading |
| will cause a null-pointer dereference. |
| |
| This patch fixes these problems by changing the gcov-kernel infrastructure |
| so that multiple profiling data sets can be associated with one debugfs |
| entry. It applies to 2.6.36-rc1. |
| |
| Signed-off-by: Peter Oberparleiter <oberpar@linux.vnet.ibm.com> |
| Reported-by: Werner Spies <werner.spies@thalesgroup.com> |
| Signed-off-by: Andrew Morton <akpm@linux-foundation.org> |
| Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org> |
| Signed-off-by: Paul Gortmaker <paul.gortmaker@windriver.com> |
| --- |
| kernel/gcov/fs.c | 244 ++++++++++++++++++++++++++++++++++++++++-------------- |
| 1 files changed, 180 insertions(+), 64 deletions(-) |
| |
| diff --git a/kernel/gcov/fs.c b/kernel/gcov/fs.c |
| index ef3c3f8..f83972b 100644 |
| --- a/kernel/gcov/fs.c |
| +++ b/kernel/gcov/fs.c |
| @@ -33,10 +33,11 @@ |
| * @children: child nodes |
| * @all: list head for list of all nodes |
| * @parent: parent node |
| - * @info: associated profiling data structure if not a directory |
| - * @ghost: when an object file containing profiling data is unloaded we keep a |
| - * copy of the profiling data here to allow collecting coverage data |
| - * for cleanup code. Such a node is called a "ghost". |
| + * @loaded_info: array of pointers to profiling data sets for loaded object |
| + * files. |
| + * @num_loaded: number of profiling data sets for loaded object files. |
| + * @unloaded_info: accumulated copy of profiling data sets for unloaded |
| + * object files. Used only when gcov_persist=1. |
| * @dentry: main debugfs entry, either a directory or data file |
| * @links: associated symbolic links |
| * @name: data file basename |
| @@ -51,10 +52,11 @@ struct gcov_node { |
| struct list_head children; |
| struct list_head all; |
| struct gcov_node *parent; |
| - struct gcov_info *info; |
| - struct gcov_info *ghost; |
| + struct gcov_info **loaded_info; |
| + struct gcov_info *unloaded_info; |
| struct dentry *dentry; |
| struct dentry **links; |
| + int num_loaded; |
| char name[0]; |
| }; |
| |
| @@ -136,16 +138,37 @@ static const struct seq_operations gcov_seq_ops = { |
| }; |
| |
| /* |
| - * Return the profiling data set for a given node. This can either be the |
| - * original profiling data structure or a duplicate (also called "ghost") |
| - * in case the associated object file has been unloaded. |
| + * Return a profiling data set associated with the given node. This is |
| + * either a data set for a loaded object file or a data set copy in case |
| + * all associated object files have been unloaded. |
| */ |
| static struct gcov_info *get_node_info(struct gcov_node *node) |
| { |
| - if (node->info) |
| - return node->info; |
| + if (node->num_loaded > 0) |
| + return node->loaded_info[0]; |
| |
| - return node->ghost; |
| + return node->unloaded_info; |
| +} |
| + |
| +/* |
| + * Return a newly allocated profiling data set which contains the sum of |
| + * all profiling data associated with the given node. |
| + */ |
| +static struct gcov_info *get_accumulated_info(struct gcov_node *node) |
| +{ |
| + struct gcov_info *info; |
| + int i = 0; |
| + |
| + if (node->unloaded_info) |
| + info = gcov_info_dup(node->unloaded_info); |
| + else |
| + info = gcov_info_dup(node->loaded_info[i++]); |
| + if (!info) |
| + return NULL; |
| + for (; i < node->num_loaded; i++) |
| + gcov_info_add(info, node->loaded_info[i]); |
| + |
| + return info; |
| } |
| |
| /* |
| @@ -163,9 +186,10 @@ static int gcov_seq_open(struct inode *inode, struct file *file) |
| mutex_lock(&node_lock); |
| /* |
| * Read from a profiling data copy to minimize reference tracking |
| - * complexity and concurrent access. |
| + * complexity and concurrent access and to keep accumulating multiple |
| + * profiling data sets associated with one node simple. |
| */ |
| - info = gcov_info_dup(get_node_info(node)); |
| + info = get_accumulated_info(node); |
| if (!info) |
| goto out_unlock; |
| iter = gcov_iter_new(info); |
| @@ -225,12 +249,25 @@ static struct gcov_node *get_node_by_name(const char *name) |
| return NULL; |
| } |
| |
| +/* |
| + * Reset all profiling data associated with the specified node. |
| + */ |
| +static void reset_node(struct gcov_node *node) |
| +{ |
| + int i; |
| + |
| + if (node->unloaded_info) |
| + gcov_info_reset(node->unloaded_info); |
| + for (i = 0; i < node->num_loaded; i++) |
| + gcov_info_reset(node->loaded_info[i]); |
| +} |
| + |
| static void remove_node(struct gcov_node *node); |
| |
| /* |
| * write() implementation for gcov data files. Reset profiling data for the |
| - * associated file. If the object file has been unloaded (i.e. this is |
| - * a "ghost" node), remove the debug fs node as well. |
| + * corresponding file. If all associated object files have been unloaded, |
| + * remove the debug fs node as well. |
| */ |
| static ssize_t gcov_seq_write(struct file *file, const char __user *addr, |
| size_t len, loff_t *pos) |
| @@ -245,10 +282,10 @@ static ssize_t gcov_seq_write(struct file *file, const char __user *addr, |
| node = get_node_by_name(info->filename); |
| if (node) { |
| /* Reset counts or remove node for unloaded modules. */ |
| - if (node->ghost) |
| + if (node->num_loaded == 0) |
| remove_node(node); |
| else |
| - gcov_info_reset(node->info); |
| + reset_node(node); |
| } |
| /* Reset counts for open file. */ |
| gcov_info_reset(info); |
| @@ -378,7 +415,10 @@ static void init_node(struct gcov_node *node, struct gcov_info *info, |
| INIT_LIST_HEAD(&node->list); |
| INIT_LIST_HEAD(&node->children); |
| INIT_LIST_HEAD(&node->all); |
| - node->info = info; |
| + if (node->loaded_info) { |
| + node->loaded_info[0] = info; |
| + node->num_loaded = 1; |
| + } |
| node->parent = parent; |
| if (name) |
| strcpy(node->name, name); |
| @@ -394,9 +434,13 @@ static struct gcov_node *new_node(struct gcov_node *parent, |
| struct gcov_node *node; |
| |
| node = kzalloc(sizeof(struct gcov_node) + strlen(name) + 1, GFP_KERNEL); |
| - if (!node) { |
| - pr_warning("out of memory\n"); |
| - return NULL; |
| + if (!node) |
| + goto err_nomem; |
| + if (info) { |
| + node->loaded_info = kcalloc(1, sizeof(struct gcov_info *), |
| + GFP_KERNEL); |
| + if (!node->loaded_info) |
| + goto err_nomem; |
| } |
| init_node(node, info, name, parent); |
| /* Differentiate between gcov data file nodes and directory nodes. */ |
| @@ -416,6 +460,11 @@ static struct gcov_node *new_node(struct gcov_node *parent, |
| list_add(&node->all, &all_head); |
| |
| return node; |
| + |
| +err_nomem: |
| + kfree(node); |
| + pr_warning("out of memory\n"); |
| + return NULL; |
| } |
| |
| /* Remove symbolic links associated with node. */ |
| @@ -441,8 +490,9 @@ static void release_node(struct gcov_node *node) |
| list_del(&node->all); |
| debugfs_remove(node->dentry); |
| remove_links(node); |
| - if (node->ghost) |
| - gcov_info_free(node->ghost); |
| + kfree(node->loaded_info); |
| + if (node->unloaded_info) |
| + gcov_info_free(node->unloaded_info); |
| kfree(node); |
| } |
| |
| @@ -477,7 +527,7 @@ static struct gcov_node *get_child_by_name(struct gcov_node *parent, |
| |
| /* |
| * write() implementation for reset file. Reset all profiling data to zero |
| - * and remove ghost nodes. |
| + * and remove nodes for which all associated object files are unloaded. |
| */ |
| static ssize_t reset_write(struct file *file, const char __user *addr, |
| size_t len, loff_t *pos) |
| @@ -487,8 +537,8 @@ static ssize_t reset_write(struct file *file, const char __user *addr, |
| mutex_lock(&node_lock); |
| restart: |
| list_for_each_entry(node, &all_head, all) { |
| - if (node->info) |
| - gcov_info_reset(node->info); |
| + if (node->num_loaded > 0) |
| + reset_node(node); |
| else if (list_empty(&node->children)) { |
| remove_node(node); |
| /* Several nodes may have gone - restart loop. */ |
| @@ -564,37 +614,115 @@ err_remove: |
| } |
| |
| /* |
| - * The profiling data set associated with this node is being unloaded. Store a |
| - * copy of the profiling data and turn this node into a "ghost". |
| + * Associate a profiling data set with an existing node. Needs to be called |
| + * with node_lock held. |
| */ |
| -static int ghost_node(struct gcov_node *node) |
| +static void add_info(struct gcov_node *node, struct gcov_info *info) |
| { |
| - node->ghost = gcov_info_dup(node->info); |
| - if (!node->ghost) { |
| - pr_warning("could not save data for '%s' (out of memory)\n", |
| - node->info->filename); |
| - return -ENOMEM; |
| + struct gcov_info **loaded_info; |
| + int num = node->num_loaded; |
| + |
| + /* |
| + * Prepare new array. This is done first to simplify cleanup in |
| + * case the new data set is incompatible, the node only contains |
| + * unloaded data sets and there's not enough memory for the array. |
| + */ |
| + loaded_info = kcalloc(num + 1, sizeof(struct gcov_info *), GFP_KERNEL); |
| + if (!loaded_info) { |
| + pr_warning("could not add '%s' (out of memory)\n", |
| + info->filename); |
| + return; |
| + } |
| + memcpy(loaded_info, node->loaded_info, |
| + num * sizeof(struct gcov_info *)); |
| + loaded_info[num] = info; |
| + /* Check if the new data set is compatible. */ |
| + if (num == 0) { |
| + /* |
| + * A module was unloaded, modified and reloaded. The new |
| + * data set replaces the copy of the last one. |
| + */ |
| + if (!gcov_info_is_compatible(node->unloaded_info, info)) { |
| + pr_warning("discarding saved data for %s " |
| + "(incompatible version)\n", info->filename); |
| + gcov_info_free(node->unloaded_info); |
| + node->unloaded_info = NULL; |
| + } |
| + } else { |
| + /* |
| + * Two different versions of the same object file are loaded. |
| + * The initial one takes precedence. |
| + */ |
| + if (!gcov_info_is_compatible(node->loaded_info[0], info)) { |
| + pr_warning("could not add '%s' (incompatible " |
| + "version)\n", info->filename); |
| + kfree(loaded_info); |
| + return; |
| + } |
| } |
| - node->info = NULL; |
| + /* Overwrite previous array. */ |
| + kfree(node->loaded_info); |
| + node->loaded_info = loaded_info; |
| + node->num_loaded = num + 1; |
| +} |
| |
| - return 0; |
| +/* |
| + * Return the index of a profiling data set associated with a node. |
| + */ |
| +static int get_info_index(struct gcov_node *node, struct gcov_info *info) |
| +{ |
| + int i; |
| + |
| + for (i = 0; i < node->num_loaded; i++) { |
| + if (node->loaded_info[i] == info) |
| + return i; |
| + } |
| + return -ENOENT; |
| } |
| |
| /* |
| - * Profiling data for this node has been loaded again. Add profiling data |
| - * from previous instantiation and turn this node into a regular node. |
| + * Save the data of a profiling data set which is being unloaded. |
| */ |
| -static void revive_node(struct gcov_node *node, struct gcov_info *info) |
| +static void save_info(struct gcov_node *node, struct gcov_info *info) |
| { |
| - if (gcov_info_is_compatible(node->ghost, info)) |
| - gcov_info_add(info, node->ghost); |
| + if (node->unloaded_info) |
| + gcov_info_add(node->unloaded_info, info); |
| else { |
| - pr_warning("discarding saved data for '%s' (version changed)\n", |
| + node->unloaded_info = gcov_info_dup(info); |
| + if (!node->unloaded_info) { |
| + pr_warning("could not save data for '%s' " |
| + "(out of memory)\n", info->filename); |
| + } |
| + } |
| +} |
| + |
| +/* |
| + * Disassociate a profiling data set from a node. Needs to be called with |
| + * node_lock held. |
| + */ |
| +static void remove_info(struct gcov_node *node, struct gcov_info *info) |
| +{ |
| + int i; |
| + |
| + i = get_info_index(node, info); |
| + if (i < 0) { |
| + pr_warning("could not remove '%s' (not found)\n", |
| info->filename); |
| + return; |
| } |
| - gcov_info_free(node->ghost); |
| - node->ghost = NULL; |
| - node->info = info; |
| + if (gcov_persist) |
| + save_info(node, info); |
| + /* Shrink array. */ |
| + node->loaded_info[i] = node->loaded_info[node->num_loaded - 1]; |
| + node->num_loaded--; |
| + if (node->num_loaded > 0) |
| + return; |
| + /* Last loaded data set was removed. */ |
| + kfree(node->loaded_info); |
| + node->loaded_info = NULL; |
| + node->num_loaded = 0; |
| + if (!node->unloaded_info) |
| + remove_node(node); |
| } |
| |
| /* |
| @@ -609,30 +737,18 @@ void gcov_event(enum gcov_action action, struct gcov_info *info) |
| node = get_node_by_name(info->filename); |
| switch (action) { |
| case GCOV_ADD: |
| - /* Add new node or revive ghost. */ |
| - if (!node) { |
| + if (node) |
| + add_info(node, info); |
| + else |
| add_node(info); |
| - break; |
| - } |
| - if (gcov_persist) |
| - revive_node(node, info); |
| - else { |
| - pr_warning("could not add '%s' (already exists)\n", |
| - info->filename); |
| - } |
| break; |
| case GCOV_REMOVE: |
| - /* Remove node or turn into ghost. */ |
| - if (!node) { |
| + if (node) |
| + remove_info(node, info); |
| + else { |
| pr_warning("could not remove '%s' (not found)\n", |
| info->filename); |
| - break; |
| } |
| - if (gcov_persist) { |
| - if (!ghost_node(node)) |
| - break; |
| - } |
| - remove_node(node); |
| break; |
| } |
| mutex_unlock(&node_lock); |
| -- |
| 1.7.0.4 |
| |