forked from mirrors/linux
		
	lib: add reference counting tracking infrastructure
It can be hard to track where references are taken and released. In networking, we have annoying issues at device or netns dismantles, and we had various proposals to ease root causing them. This patch adds new infrastructure pairing refcount increases and decreases. This will self document code, because programmers will have to associate increments/decrements. This is controled by CONFIG_REF_TRACKER which can be selected by users of this feature. This adds both cpu and memory costs, and thus should probably be used with care. Signed-off-by: Eric Dumazet <edumazet@google.com> Reviewed-by: Dmitry Vyukov <dvyukov@google.com> Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
		
							parent
							
								
									ce83278f31
								
							
						
					
					
						commit
						4e66934eaa
					
				
					 4 changed files with 220 additions and 0 deletions
				
			
		
							
								
								
									
										73
									
								
								include/linux/ref_tracker.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								include/linux/ref_tracker.h
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,73 @@ | ||||||
|  | // SPDX-License-Identifier: GPL-2.0-or-later
 | ||||||
|  | #ifndef _LINUX_REF_TRACKER_H | ||||||
|  | #define _LINUX_REF_TRACKER_H | ||||||
|  | #include <linux/refcount.h> | ||||||
|  | #include <linux/types.h> | ||||||
|  | #include <linux/spinlock.h> | ||||||
|  | 
 | ||||||
|  | struct ref_tracker; | ||||||
|  | 
 | ||||||
|  | struct ref_tracker_dir { | ||||||
|  | #ifdef CONFIG_REF_TRACKER | ||||||
|  | 	spinlock_t		lock; | ||||||
|  | 	unsigned int		quarantine_avail; | ||||||
|  | 	refcount_t		untracked; | ||||||
|  | 	struct list_head	list; /* List of active trackers */ | ||||||
|  | 	struct list_head	quarantine; /* List of dead trackers */ | ||||||
|  | #endif | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #ifdef CONFIG_REF_TRACKER | ||||||
|  | static inline void ref_tracker_dir_init(struct ref_tracker_dir *dir, | ||||||
|  | 					unsigned int quarantine_count) | ||||||
|  | { | ||||||
|  | 	INIT_LIST_HEAD(&dir->list); | ||||||
|  | 	INIT_LIST_HEAD(&dir->quarantine); | ||||||
|  | 	spin_lock_init(&dir->lock); | ||||||
|  | 	dir->quarantine_avail = quarantine_count; | ||||||
|  | 	refcount_set(&dir->untracked, 1); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void ref_tracker_dir_exit(struct ref_tracker_dir *dir); | ||||||
|  | 
 | ||||||
|  | void ref_tracker_dir_print(struct ref_tracker_dir *dir, | ||||||
|  | 			   unsigned int display_limit); | ||||||
|  | 
 | ||||||
|  | int ref_tracker_alloc(struct ref_tracker_dir *dir, | ||||||
|  | 		      struct ref_tracker **trackerp, gfp_t gfp); | ||||||
|  | 
 | ||||||
|  | int ref_tracker_free(struct ref_tracker_dir *dir, | ||||||
|  | 		     struct ref_tracker **trackerp); | ||||||
|  | 
 | ||||||
|  | #else /* CONFIG_REF_TRACKER */ | ||||||
|  | 
 | ||||||
|  | static inline void ref_tracker_dir_init(struct ref_tracker_dir *dir, | ||||||
|  | 					unsigned int quarantine_count) | ||||||
|  | { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static inline void ref_tracker_dir_exit(struct ref_tracker_dir *dir) | ||||||
|  | { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static inline void ref_tracker_dir_print(struct ref_tracker_dir *dir, | ||||||
|  | 					 unsigned int display_limit) | ||||||
|  | { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static inline int ref_tracker_alloc(struct ref_tracker_dir *dir, | ||||||
|  | 				    struct ref_tracker **trackerp, | ||||||
|  | 				    gfp_t gfp) | ||||||
|  | { | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static inline int ref_tracker_free(struct ref_tracker_dir *dir, | ||||||
|  | 				   struct ref_tracker **trackerp) | ||||||
|  | { | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | #endif /* _LINUX_REF_TRACKER_H */ | ||||||
|  | @ -680,6 +680,11 @@ config STACK_HASH_ORDER | ||||||
| 	 Select the hash size as a power of 2 for the stackdepot hash table. | 	 Select the hash size as a power of 2 for the stackdepot hash table. | ||||||
| 	 Choose a lower value to reduce the memory impact. | 	 Choose a lower value to reduce the memory impact. | ||||||
| 
 | 
 | ||||||
|  | config REF_TRACKER | ||||||
|  | 	bool | ||||||
|  | 	depends on STACKTRACE_SUPPORT | ||||||
|  | 	select STACKDEPOT | ||||||
|  | 
 | ||||||
| config SBITMAP | config SBITMAP | ||||||
| 	bool | 	bool | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -270,6 +270,8 @@ obj-$(CONFIG_STACKDEPOT) += stackdepot.o | ||||||
| KASAN_SANITIZE_stackdepot.o := n | KASAN_SANITIZE_stackdepot.o := n | ||||||
| KCOV_INSTRUMENT_stackdepot.o := n | KCOV_INSTRUMENT_stackdepot.o := n | ||||||
| 
 | 
 | ||||||
|  | obj-$(CONFIG_REF_TRACKER) += ref_tracker.o | ||||||
|  | 
 | ||||||
| libfdt_files = fdt.o fdt_ro.o fdt_wip.o fdt_rw.o fdt_sw.o fdt_strerror.o \
 | libfdt_files = fdt.o fdt_ro.o fdt_wip.o fdt_rw.o fdt_sw.o fdt_strerror.o \
 | ||||||
| 	       fdt_empty_tree.o fdt_addresses.o | 	       fdt_empty_tree.o fdt_addresses.o | ||||||
| $(foreach file, $(libfdt_files), \ | $(foreach file, $(libfdt_files), \ | ||||||
|  |  | ||||||
							
								
								
									
										140
									
								
								lib/ref_tracker.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								lib/ref_tracker.c
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,140 @@ | ||||||
|  | // SPDX-License-Identifier: GPL-2.0-or-later
 | ||||||
|  | #include <linux/export.h> | ||||||
|  | #include <linux/ref_tracker.h> | ||||||
|  | #include <linux/slab.h> | ||||||
|  | #include <linux/stacktrace.h> | ||||||
|  | #include <linux/stackdepot.h> | ||||||
|  | 
 | ||||||
|  | #define REF_TRACKER_STACK_ENTRIES 16 | ||||||
|  | 
 | ||||||
|  | struct ref_tracker { | ||||||
|  | 	struct list_head	head;   /* anchor into dir->list or dir->quarantine */ | ||||||
|  | 	bool			dead; | ||||||
|  | 	depot_stack_handle_t	alloc_stack_handle; | ||||||
|  | 	depot_stack_handle_t	free_stack_handle; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | void ref_tracker_dir_exit(struct ref_tracker_dir *dir) | ||||||
|  | { | ||||||
|  | 	struct ref_tracker *tracker, *n; | ||||||
|  | 	unsigned long flags; | ||||||
|  | 	bool leak = false; | ||||||
|  | 
 | ||||||
|  | 	spin_lock_irqsave(&dir->lock, flags); | ||||||
|  | 	list_for_each_entry_safe(tracker, n, &dir->quarantine, head) { | ||||||
|  | 		list_del(&tracker->head); | ||||||
|  | 		kfree(tracker); | ||||||
|  | 		dir->quarantine_avail++; | ||||||
|  | 	} | ||||||
|  | 	list_for_each_entry_safe(tracker, n, &dir->list, head) { | ||||||
|  | 		pr_err("leaked reference.\n"); | ||||||
|  | 		if (tracker->alloc_stack_handle) | ||||||
|  | 			stack_depot_print(tracker->alloc_stack_handle); | ||||||
|  | 		leak = true; | ||||||
|  | 		list_del(&tracker->head); | ||||||
|  | 		kfree(tracker); | ||||||
|  | 	} | ||||||
|  | 	spin_unlock_irqrestore(&dir->lock, flags); | ||||||
|  | 	WARN_ON_ONCE(leak); | ||||||
|  | 	WARN_ON_ONCE(refcount_read(&dir->untracked) != 1); | ||||||
|  | } | ||||||
|  | EXPORT_SYMBOL(ref_tracker_dir_exit); | ||||||
|  | 
 | ||||||
|  | void ref_tracker_dir_print(struct ref_tracker_dir *dir, | ||||||
|  | 			   unsigned int display_limit) | ||||||
|  | { | ||||||
|  | 	struct ref_tracker *tracker; | ||||||
|  | 	unsigned long flags; | ||||||
|  | 	unsigned int i = 0; | ||||||
|  | 
 | ||||||
|  | 	spin_lock_irqsave(&dir->lock, flags); | ||||||
|  | 	list_for_each_entry(tracker, &dir->list, head) { | ||||||
|  | 		if (i < display_limit) { | ||||||
|  | 			pr_err("leaked reference.\n"); | ||||||
|  | 			if (tracker->alloc_stack_handle) | ||||||
|  | 				stack_depot_print(tracker->alloc_stack_handle); | ||||||
|  | 			i++; | ||||||
|  | 		} else { | ||||||
|  | 			break; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	spin_unlock_irqrestore(&dir->lock, flags); | ||||||
|  | } | ||||||
|  | EXPORT_SYMBOL(ref_tracker_dir_print); | ||||||
|  | 
 | ||||||
|  | int ref_tracker_alloc(struct ref_tracker_dir *dir, | ||||||
|  | 		      struct ref_tracker **trackerp, | ||||||
|  | 		      gfp_t gfp) | ||||||
|  | { | ||||||
|  | 	unsigned long entries[REF_TRACKER_STACK_ENTRIES]; | ||||||
|  | 	struct ref_tracker *tracker; | ||||||
|  | 	unsigned int nr_entries; | ||||||
|  | 	unsigned long flags; | ||||||
|  | 
 | ||||||
|  | 	*trackerp = tracker = kzalloc(sizeof(*tracker), gfp | __GFP_NOFAIL); | ||||||
|  | 	if (unlikely(!tracker)) { | ||||||
|  | 		pr_err_once("memory allocation failure, unreliable refcount tracker.\n"); | ||||||
|  | 		refcount_inc(&dir->untracked); | ||||||
|  | 		return -ENOMEM; | ||||||
|  | 	} | ||||||
|  | 	nr_entries = stack_trace_save(entries, ARRAY_SIZE(entries), 1); | ||||||
|  | 	nr_entries = filter_irq_stacks(entries, nr_entries); | ||||||
|  | 	tracker->alloc_stack_handle = stack_depot_save(entries, nr_entries, gfp); | ||||||
|  | 
 | ||||||
|  | 	spin_lock_irqsave(&dir->lock, flags); | ||||||
|  | 	list_add(&tracker->head, &dir->list); | ||||||
|  | 	spin_unlock_irqrestore(&dir->lock, flags); | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  | EXPORT_SYMBOL_GPL(ref_tracker_alloc); | ||||||
|  | 
 | ||||||
|  | int ref_tracker_free(struct ref_tracker_dir *dir, | ||||||
|  | 		     struct ref_tracker **trackerp) | ||||||
|  | { | ||||||
|  | 	unsigned long entries[REF_TRACKER_STACK_ENTRIES]; | ||||||
|  | 	struct ref_tracker *tracker = *trackerp; | ||||||
|  | 	depot_stack_handle_t stack_handle; | ||||||
|  | 	unsigned int nr_entries; | ||||||
|  | 	unsigned long flags; | ||||||
|  | 
 | ||||||
|  | 	if (!tracker) { | ||||||
|  | 		refcount_dec(&dir->untracked); | ||||||
|  | 		return -EEXIST; | ||||||
|  | 	} | ||||||
|  | 	nr_entries = stack_trace_save(entries, ARRAY_SIZE(entries), 1); | ||||||
|  | 	nr_entries = filter_irq_stacks(entries, nr_entries); | ||||||
|  | 	stack_handle = stack_depot_save(entries, nr_entries, GFP_ATOMIC); | ||||||
|  | 
 | ||||||
|  | 	spin_lock_irqsave(&dir->lock, flags); | ||||||
|  | 	if (tracker->dead) { | ||||||
|  | 		pr_err("reference already released.\n"); | ||||||
|  | 		if (tracker->alloc_stack_handle) { | ||||||
|  | 			pr_err("allocated in:\n"); | ||||||
|  | 			stack_depot_print(tracker->alloc_stack_handle); | ||||||
|  | 		} | ||||||
|  | 		if (tracker->free_stack_handle) { | ||||||
|  | 			pr_err("freed in:\n"); | ||||||
|  | 			stack_depot_print(tracker->free_stack_handle); | ||||||
|  | 		} | ||||||
|  | 		spin_unlock_irqrestore(&dir->lock, flags); | ||||||
|  | 		WARN_ON_ONCE(1); | ||||||
|  | 		return -EINVAL; | ||||||
|  | 	} | ||||||
|  | 	tracker->dead = true; | ||||||
|  | 
 | ||||||
|  | 	tracker->free_stack_handle = stack_handle; | ||||||
|  | 
 | ||||||
|  | 	list_move_tail(&tracker->head, &dir->quarantine); | ||||||
|  | 	if (!dir->quarantine_avail) { | ||||||
|  | 		tracker = list_first_entry(&dir->quarantine, struct ref_tracker, head); | ||||||
|  | 		list_del(&tracker->head); | ||||||
|  | 	} else { | ||||||
|  | 		dir->quarantine_avail--; | ||||||
|  | 		tracker = NULL; | ||||||
|  | 	} | ||||||
|  | 	spin_unlock_irqrestore(&dir->lock, flags); | ||||||
|  | 
 | ||||||
|  | 	kfree(tracker); | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  | EXPORT_SYMBOL_GPL(ref_tracker_free); | ||||||
		Loading…
	
		Reference in a new issue
	
	 Eric Dumazet
						Eric Dumazet