Re: [PATCH v2 1/2] mm, kasan: improve double-free detection

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Fri, May 06, 2016 at 05:17:27PM +0530, Kuthonuzo Luruo wrote:
> Currently, KASAN may fail to detect concurrent deallocations of the same
> object due to a race in kasan_slab_free(). This patch makes double-free
> detection more reliable by serializing access to KASAN object metadata.
> New functions kasan_meta_lock() and kasan_meta_unlock() are provided to
> lock/unlock per-object metadata. Double-free errors are now reported via
> kasan_report().
> 
> Testing:
> - Tested with a modified version of the 'slab_test' microbenchmark where
>   allocs occur on CPU 0; then all other CPUs concurrently attempt to free
>   the same object.
> - Tested with new 'test_kasan' kasan_double_free() test in accompanying
>   patch.
> 
> Signed-off-by: Kuthonuzo Luruo <kuthonuzo.luruo@xxxxxxx>
> ---
> 
> Changes in v2:
> - Incorporated suggestions from Dmitry Vyukov. New per-object metadata
>   lock/unlock functions; kasan_alloc_meta modified to add new state while
>   using fewer bits overall.
> - Double-free pr_err promoted to kasan_report().
> - kasan_init_object() introduced to initialize KASAN object metadata
>   during slab creation. KASAN_STATE_INIT initialization removed from
>   kasan_poison_object_data().
>  
> ---
>  include/linux/kasan.h |    8 +++
>  mm/kasan/kasan.c      |  118 ++++++++++++++++++++++++++++++++++++-------------
>  mm/kasan/kasan.h      |   15 +++++-
>  mm/kasan/quarantine.c |    7 +++-
>  mm/kasan/report.c     |   31 +++++++++++--
>  mm/slab.c             |    1 +
>  6 files changed, 142 insertions(+), 38 deletions(-)
> 
> diff --git a/include/linux/kasan.h b/include/linux/kasan.h
> index 645c280..c7bf625 100644
> --- a/include/linux/kasan.h
> +++ b/include/linux/kasan.h
> @@ -78,6 +78,10 @@ struct kasan_cache {
>  int kasan_module_alloc(void *addr, size_t size);
>  void kasan_free_shadow(const struct vm_struct *vm);
>  
> +#ifdef CONFIG_SLAB
> +void kasan_init_object(struct kmem_cache *cache, void *object);
> +#endif
> +
>  #else /* CONFIG_KASAN */
>  
>  static inline void kasan_unpoison_shadow(const void *address, size_t size) {}
> @@ -124,6 +128,10 @@ static inline void kasan_poison_slab_free(struct kmem_cache *s, void *object) {}
>  static inline int kasan_module_alloc(void *addr, size_t size) { return 0; }
>  static inline void kasan_free_shadow(const struct vm_struct *vm) {}
>  
> +#ifdef CONFIG_SLAB
> +static inline void kasan_init_object(struct kmem_cache *cache, void *object) {}
> +#endif
> +
>  #endif /* CONFIG_KASAN */
>  
>  #endif /* LINUX_KASAN_H */
> diff --git a/mm/kasan/kasan.c b/mm/kasan/kasan.c
> index ef2e87b..a840b49 100644
> --- a/mm/kasan/kasan.c
> +++ b/mm/kasan/kasan.c
> @@ -34,6 +34,7 @@
>  #include <linux/string.h>
>  #include <linux/types.h>
>  #include <linux/vmalloc.h>
> +#include <linux/atomic.h>
>  
>  #include "kasan.h"
>  #include "../slab.h"
> @@ -419,13 +420,6 @@ void kasan_poison_object_data(struct kmem_cache *cache, void *object)
>  	kasan_poison_shadow(object,
>  			round_up(cache->object_size, KASAN_SHADOW_SCALE_SIZE),
>  			KASAN_KMALLOC_REDZONE);
> -#ifdef CONFIG_SLAB
> -	if (cache->flags & SLAB_KASAN) {
> -		struct kasan_alloc_meta *alloc_info =
> -			get_alloc_info(cache, object);
> -		alloc_info->state = KASAN_STATE_INIT;
> -	}
> -#endif
>  }
>  
>  #ifdef CONFIG_SLAB
> @@ -470,6 +464,18 @@ static inline depot_stack_handle_t save_stack(gfp_t flags)
>  	return depot_save_stack(&trace, flags);
>  }
>  
> +void kasan_init_object(struct kmem_cache *cache, void *object)
> +{
> +	struct kasan_alloc_meta *alloc_info;
> +
> +	if (cache->flags & SLAB_KASAN) {
> +		kasan_unpoison_object_data(cache, object);
> +		alloc_info = get_alloc_info(cache, object);
> +		__memset(alloc_info, 0, sizeof(*alloc_info));
> +		kasan_poison_object_data(cache, object);
> +	}
> +}
> +
>  static inline void set_track(struct kasan_track *track, gfp_t flags)
>  {
>  	track->pid = current->pid;
> @@ -489,6 +495,39 @@ struct kasan_free_meta *get_free_info(struct kmem_cache *cache,
>  	BUILD_BUG_ON(sizeof(struct kasan_free_meta) > 32);
>  	return (void *)object + cache->kasan_info.free_meta_offset;
>  }
> +
> +/* acquire per-object lock for access to KASAN metadata. */

I believe there's strong reason not to use standard spin_lock() or
similar. I think it's proper place to explain it.

> +void kasan_meta_lock(struct kasan_alloc_meta *alloc_info)
> +{
> +	union kasan_alloc_data old, new;
> +
> +	preempt_disable();

It's better to disable and enable preemption inside the loop
on each iteration, to decrease contention.

> +	for (;;) {
> +		old.packed = READ_ONCE(alloc_info->data);
> +		if (unlikely(old.lock)) {
> +			cpu_relax();
> +			continue;
> +		}
> +		new.packed = old.packed;
> +		new.lock = 1;
> +		if (likely(cmpxchg(&alloc_info->data, old.packed, new.packed)
> +					== old.packed))
> +			break;
> +	}
> +}
> +
> +/* release lock after a kasan_meta_lock(). */
> +void kasan_meta_unlock(struct kasan_alloc_meta *alloc_info)
> +{
> +	union kasan_alloc_data alloc_data;
> +
> +	alloc_data.packed = READ_ONCE(alloc_info->data);
> +	alloc_data.lock = 0;
> +	if (unlikely(xchg(&alloc_info->data, alloc_data.packed) !=
> +				(alloc_data.packed | 0x1U)))
> +		WARN_ONCE(1, "%s: lock not held!\n", __func__);

Nitpick. It never happens in normal case, correct?. Why don't you place it under
some developer config, or even leave at dev branch? The function will
be twice shorter without it.

> +	preempt_enable();
> +}
>  #endif
>  
>  void kasan_slab_alloc(struct kmem_cache *cache, void *object, gfp_t flags)
> @@ -511,32 +550,41 @@ void kasan_poison_slab_free(struct kmem_cache *cache, void *object)
>  bool kasan_slab_free(struct kmem_cache *cache, void *object)
>  {
>  #ifdef CONFIG_SLAB
> +	struct kasan_alloc_meta *alloc_info;
> +	struct kasan_free_meta *free_info;
> +	union kasan_alloc_data alloc_data;
> +
>  	/* RCU slabs could be legally used after free within the RCU period */
>  	if (unlikely(cache->flags & SLAB_DESTROY_BY_RCU))
>  		return false;
>  
> -	if (likely(cache->flags & SLAB_KASAN)) {
> -		struct kasan_alloc_meta *alloc_info =
> -			get_alloc_info(cache, object);
> -		struct kasan_free_meta *free_info =
> -			get_free_info(cache, object);
> -
> -		switch (alloc_info->state) {
> -		case KASAN_STATE_ALLOC:
> -			alloc_info->state = KASAN_STATE_QUARANTINE;
> -			quarantine_put(free_info, cache);
> -			set_track(&free_info->track, GFP_NOWAIT);
> -			kasan_poison_slab_free(cache, object);
> -			return true;
> -		case KASAN_STATE_QUARANTINE:
> -		case KASAN_STATE_FREE:
> -			pr_err("Double free");
> -			dump_stack();
> -			break;
> -		default:
> -			break;
> -		}
> +	if (unlikely(!(cache->flags & SLAB_KASAN)))
> +		return false;
> +
> +	alloc_info = get_alloc_info(cache, object);
> +	kasan_meta_lock(alloc_info);
> +	alloc_data.packed = alloc_info->data;
> +	if (alloc_data.state == KASAN_STATE_ALLOC) {
> +		free_info = get_free_info(cache, object);
> +		quarantine_put(free_info, cache);

I just pulled master and didn't find this function. If your patchset
is based on other branch, please notice it.

> +		set_track(&free_info->track, GFP_NOWAIT);

It may fail for many reasons. Is it OK to ignore it? If OK, I think it
should be explained.

> +		kasan_poison_slab_free(cache, object);
> +		alloc_data.state = KASAN_STATE_QUARANTINE;
> +		alloc_info->data = alloc_data.packed;
> +		kasan_meta_unlock(alloc_info);
> +		return true;
>  	}
> +	switch (alloc_data.state) {
> +	case KASAN_STATE_QUARANTINE:
> +	case KASAN_STATE_FREE:
> +		kasan_report((unsigned long)object, 0, false,
> +				(unsigned long)__builtin_return_address(1));

__builtin_return_address() is unsafe if argument is non-zero. Use
return_address() instead.

> +		kasan_meta_unlock(alloc_info);
> +		return true;
> +	default:
> +		break;
> +	}
> +	kasan_meta_unlock(alloc_info);
>  	return false;
>  #else
>  	kasan_poison_slab_free(cache, object);
> @@ -568,12 +616,20 @@ void kasan_kmalloc(struct kmem_cache *cache, const void *object, size_t size,
>  		KASAN_KMALLOC_REDZONE);
>  #ifdef CONFIG_SLAB
>  	if (cache->flags & SLAB_KASAN) {
> +		union kasan_alloc_data alloc_data;
>  		struct kasan_alloc_meta *alloc_info =
>  			get_alloc_info(cache, object);
> -
> -		alloc_info->state = KASAN_STATE_ALLOC;
> -		alloc_info->alloc_size = size;
> +		unsigned long flags;
> +
> +		local_irq_save(flags);
> +		kasan_meta_lock(alloc_info);
> +		alloc_data.packed = alloc_info->data;
> +		alloc_data.state = KASAN_STATE_ALLOC;
> +		alloc_data.size_delta = cache->object_size - size;
> +		alloc_info->data = alloc_data.packed;
>  		set_track(&alloc_info->track, flags);

Same as above

> +		kasan_meta_unlock(alloc_info);
> +		local_irq_restore(flags);
>  	}
>  #endif
>  }
> diff --git a/mm/kasan/kasan.h b/mm/kasan/kasan.h
> index 7da78a6..df2724d 100644
> --- a/mm/kasan/kasan.h
> +++ b/mm/kasan/kasan.h
> @@ -73,10 +73,19 @@ struct kasan_track {
>  	depot_stack_handle_t stack;
>  };
>  
> +union kasan_alloc_data {
> +	struct {
> +		u32 lock : 1;
> +		u32 state : 2;		/* enum kasan_state */
> +		u32 size_delta : 24;	/* object_size - alloc size */
> +		u32 unused : 5;
> +	};
> +	u32 packed;
> +};
> +
>  struct kasan_alloc_meta {
>  	struct kasan_track track;
> -	u32 state : 2;	/* enum kasan_state */
> -	u32 alloc_size : 30;
> +	u32 data;	/* encoded as union kasan_alloc_data */
>  	u32 reserved;
>  };
>  
> @@ -112,4 +121,6 @@ void quarantine_put(struct kasan_free_meta *info, struct kmem_cache *cache);
>  void quarantine_reduce(void);
>  void quarantine_remove_cache(struct kmem_cache *cache);
>  
> +void kasan_meta_lock(struct kasan_alloc_meta *alloc_info);
> +void kasan_meta_unlock(struct kasan_alloc_meta *alloc_info);
>  #endif
> diff --git a/mm/kasan/quarantine.c b/mm/kasan/quarantine.c
> index 40159a6..d59a569 100644
> --- a/mm/kasan/quarantine.c
> +++ b/mm/kasan/quarantine.c
> @@ -144,10 +144,15 @@ static void qlink_free(void **qlink, struct kmem_cache *cache)
>  {
>  	void *object = qlink_to_object(qlink, cache);
>  	struct kasan_alloc_meta *alloc_info = get_alloc_info(cache, object);
> +	union kasan_alloc_data alloc_data;
>  	unsigned long flags;
>  
>  	local_irq_save(flags);
> -	alloc_info->state = KASAN_STATE_FREE;
> +	kasan_meta_lock(alloc_info);
> +	alloc_data.packed = alloc_info->data;
> +	alloc_data.state = KASAN_STATE_FREE;
> +	alloc_info->data = alloc_data.packed;
> +	kasan_meta_unlock(alloc_info);
>  	___cache_free(cache, object, _THIS_IP_);
>  	local_irq_restore(flags);
>  }
> diff --git a/mm/kasan/report.c b/mm/kasan/report.c
> index b3c122d..cecf2fa 100644
> --- a/mm/kasan/report.c
> +++ b/mm/kasan/report.c
> @@ -53,6 +53,17 @@ static void print_error_description(struct kasan_access_info *info)
>  	const char *bug_type = "unknown-crash";
>  	u8 *shadow_addr;
>  
> +#ifdef CONFIG_SLAB
> +	if (!info->access_size) {
> +		pr_err("BUG: KASAN: double-free attempt in %pS on object at addr %p\n",
> +				(void *)info->ip, info->access_addr);
> +		pr_err("Double free by task %s/%d\n",
> +				current->comm, task_pid_nr(current));
> +		info->first_bad_addr = info->access_addr;
> +		return;
> +	}
> +#endif
> +
>  	info->first_bad_addr = find_first_bad_addr(info->access_addr,
>  						info->access_size);
>  
> @@ -131,29 +142,34 @@ static void print_track(struct kasan_track *track)
>  }
>  
>  static void object_err(struct kmem_cache *cache, struct page *page,
> -			void *object, char *unused_reason)
> +		void *object, char *unused_reason,
> +		struct kasan_access_info *info)
>  {
>  	struct kasan_alloc_meta *alloc_info = get_alloc_info(cache, object);
>  	struct kasan_free_meta *free_info;
> +	union kasan_alloc_data alloc_data;
>  
>  	dump_stack();
>  	pr_err("Object at %p, in cache %s\n", object, cache->name);
>  	if (!(cache->flags & SLAB_KASAN))
>  		return;
> -	switch (alloc_info->state) {
> +	if (info->access_size)
> +		kasan_meta_lock(alloc_info);
> +	alloc_data.packed = alloc_info->data;
> +	switch (alloc_data.state) {
>  	case KASAN_STATE_INIT:
>  		pr_err("Object not allocated yet\n");
>  		break;
>  	case KASAN_STATE_ALLOC:
>  		pr_err("Object allocated with size %u bytes.\n",
> -		       alloc_info->alloc_size);
> +				(cache->object_size - alloc_data.size_delta));
>  		pr_err("Allocation:\n");
>  		print_track(&alloc_info->track);
>  		break;
>  	case KASAN_STATE_FREE:
>  	case KASAN_STATE_QUARANTINE:
>  		pr_err("Object freed, allocated with size %u bytes\n",
> -		       alloc_info->alloc_size);
> +				(cache->object_size - alloc_data.size_delta));
>  		free_info = get_free_info(cache, object);
>  		pr_err("Allocation:\n");
>  		print_track(&alloc_info->track);
> @@ -161,6 +177,8 @@ static void object_err(struct kmem_cache *cache, struct page *page,
>  		print_track(&free_info->track);
>  		break;
>  	}
> +	if (info->access_size)
> +		kasan_meta_unlock(alloc_info);
>  }
>  #endif
>  
> @@ -177,8 +195,13 @@ static void print_address_description(struct kasan_access_info *info)
>  			struct kmem_cache *cache = page->slab_cache;
>  			object = nearest_obj(cache, page,
>  						(void *)info->access_addr);
> +#ifdef CONFIG_SLAB
> +			object_err(cache, page, object,
> +					"kasan: bad access detected", info);
> +#else
>  			object_err(cache, page, object,
>  					"kasan: bad access detected");
> +#endif
>  			return;
>  		}
>  		dump_page(page, "kasan: bad access detected");
> diff --git a/mm/slab.c b/mm/slab.c
> index 3f20800..110d586 100644
> --- a/mm/slab.c
> +++ b/mm/slab.c
> @@ -2651,6 +2651,7 @@ static void cache_init_objs(struct kmem_cache *cachep,
>  			cachep->ctor(objp);
>  			kasan_poison_object_data(cachep, objp);
>  		}
> +		kasan_init_object(cachep, index_to_obj(cachep, page, i));
>  
>  		if (!shuffled)
>  			set_free_obj(page, i, i);
> -- 
> 1.7.1

--
To unsubscribe, send a message with 'unsubscribe linux-mm' in
the body to majordomo@xxxxxxxxx.  For more info on Linux MM,
see: http://www.linux-mm.org/ .
Don't email: <a href=mailto:"dont@xxxxxxxxx";> email@xxxxxxxxx </a>



[Index of Archives]     [Linux ARM Kernel]     [Linux ARM]     [Linux Omap]     [Fedora ARM]     [IETF Annouce]     [Bugtraq]     [Linux]     [Linux OMAP]     [Linux MIPS]     [ECOS]     [Asterisk Internet PBX]     [Linux API]