* [PATCH 0/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf().
@ 2026-10-09 21:46 Kuniyuki Iwashima
2026-10-09 21:46 ` [PATCH 1/2] " Kuniyuki Iwashima
` (2 more replies)
0 siblings, 3 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-10-09 21:46 UTC (permalink / raw)
To: Andrey Ryabinin, Andrew Morton, Vlastimil Babka, Harry Yoo
Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov,
Vincenzo Frascino, Hao Li, Christoph Lameter, David Rientjes,
Roman Gushchin, kasan-dev, linux-mm, Kuniyuki Iwashima
Recently, syzbot has reported several weird use-after-free splats.
What they have in common is that "Freed by" points to rcu_free_sheaf(),
whereas the object shown in "Call Trace:" and "Allocated by" is never
supposed to be freed via kfree_rcu().
This means the object was a victim of a double-free somewhere else and
was silently poisoned by rcu_free_sheaf() after being reallocated.
This happens because kfree_rcu_sheaf() queues an object into the RCU
sheaf without invoking any KASAN hooks.
This series enables KASAN to detect double-free involving
kfree_rcu_sheaf() immediately and adds KUnit tests for it.
Kuniyuki Iwashima (2):
mm/kasan: Detect double-free involving kfree_rcu_sheaf().
mm/kasan/kunit: Add double-free tests involving kfree_rcu_sheaf().
include/linux/kasan.h | 13 ++--
mm/kasan/common.c | 13 ++--
mm/kasan/generic.c | 14 ++++-
mm/kasan/kasan.h | 34 ++++++++++-
mm/kasan/kasan_test_c.c | 131 ++++++++++++++++++++++++++++++++++++++++
mm/slub.c | 13 +++-
6 files changed, 204 insertions(+), 14 deletions(-)
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH 1/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf(). 2026-10-09 21:46 [PATCH 0/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf() Kuniyuki Iwashima @ 2026-10-09 21:46 ` Kuniyuki Iwashima 2026-10-20 12:29 ` Vlastimil Babka (SUSE) 2026-10-27 12:48 ` Alexander Potapenko 2026-10-09 21:46 ` [PATCH 2/2] mm/kasan/kunit: Add double-free tests " Kuniyuki Iwashima 2026-10-27 6:15 ` [PATCH 0/2] mm/kasan: Detect double-free " Kuniyuki Iwashima 2 siblings, 2 replies; 9+ messages in thread From: Kuniyuki Iwashima @ 2026-10-09 21:46 UTC (permalink / raw) To: Andrey Ryabinin, Andrew Morton, Vlastimil Babka, Harry Yoo Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Hao Li, Christoph Lameter, David Rientjes, Roman Gushchin, kasan-dev, linux-mm, Kuniyuki Iwashima syzbot reported a weird use-after-free splat. [0] In the report, an object in the kmalloc-64 cache was freed via rcu_free_sheaf() while it was still in use, even though the object should always be freed with kfree() (in xfrm_hash_free()), implying that the object was a victim of a double-free involving kfree_rcu() somewhere else. When kvfree_call_rcu() frees an object via kfree_rcu_sheaf() and queues it into pcs->rcu_free, debug facilities are skipped, e.g., kasan_slab_pre_free(), kasan_record_aux_stack(), debug_rcu_head_queue(), etc. As a result, KASAN cannot detect the double-free at the call site. If the object is still in the KASAN quarantine when the sheaf is flushed, KASAN can detect the double-free in rcu_free_sheaf(), but with no trace of the suspect. Worse yet, once the object is evicted from the quarantine and reallocated before the sheaf is flushed, rcu_free_sheaf() silently poisons the live object, resulting in a false-positive use-after-free report on the new user. If __kfree_rcu_sheaf() simply calls kasan_slab_pre_free(), we can detect the second kfree_rcu(), but only while the object is poisoned in the KASAN quarantine after the first free: kfree() --(poisoned)--------------------> kfree_rcu() kfree_rcu() -> rcu_free_sheaf() -(poisoned)-> kfree_rcu() An object queued in pcs->rcu_free remains unpoisoned until the sheaf becomes full and rcu_free_sheaf() runs after the RCU grace period, so a subsequent kfree_rcu() or kfree() before rcu_free_sheaf() cannot be detected by shadow bytes alone: kfree_rcu() -(unpoisoned)-> kfree_rcu() -> rcu_free_sheaf() (too late!) kfree_rcu() -(unpoisoned)-> kfree() -> rcu_free_sheaf() (too late!) These cases can be detected if we call debug_rcu_head_queue() before kfree_rcu_sheaf(), but ODEBUG splat only shows the second free. To detect these cases with KASAN, we just need a 1-bit flag to indicate the state, which can reuse an unused bit in struct kasan_track.pid. (PID_MAX_LIMIT is 1<<22) The bit is marked along with recording the stack trace when deferring slab frees in kfree_rcu_sheaf() and checked in check_slab_allocation() so that any double-free pattern above can be detected immediately. Link: https://lore.kernel.org/all/6ac959ee.70ed5d86.2ac85.007d.GAE@google.com/ #[0] Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com> --- include/linux/kasan.h | 13 +++++++++---- mm/kasan/common.c | 13 ++++++++----- mm/kasan/generic.c | 14 +++++++++++++- mm/kasan/kasan.h | 34 +++++++++++++++++++++++++++++++++- mm/slub.c | 13 ++++++++++--- 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/include/linux/kasan.h b/include/linux/kasan.h index bf233bde68c7..3a43e3d2658e 100644 --- a/include/linux/kasan.h +++ b/include/linux/kasan.h @@ -180,7 +180,7 @@ static __always_inline void * __must_check kasan_init_slab_obj( } bool __kasan_slab_pre_free(struct kmem_cache *s, void *object, - unsigned long ip); + unsigned long ip, bool after_rcu_delay); /** * kasan_slab_pre_free - Check whether freeing a slab object is safe. * @object: Object to be freed. @@ -193,10 +193,12 @@ bool __kasan_slab_pre_free(struct kmem_cache *s, void *object, * @Return true if freeing the object is unsafe; false otherwise. */ static __always_inline bool kasan_slab_pre_free(struct kmem_cache *s, - void *object) + void *object, + bool after_rcu_delay) { if (kasan_enabled()) - return __kasan_slab_pre_free(s, object, _RET_IP_); + return __kasan_slab_pre_free(s, object, _RET_IP_, + after_rcu_delay); return false; } @@ -424,7 +426,8 @@ static inline void *kasan_init_slab_obj(struct kmem_cache *cache, return (void *)object; } -static inline bool kasan_slab_pre_free(struct kmem_cache *s, void *object) +static inline bool kasan_slab_pre_free(struct kmem_cache *s, void *object, + bool after_rcu_delay) { return false; } @@ -495,6 +498,7 @@ void kasan_cache_create(struct kmem_cache *cache, unsigned int *size, void kasan_cache_shrink(struct kmem_cache *cache); void kasan_cache_shutdown(struct kmem_cache *cache); void kasan_record_aux_stack(void *ptr); +void kasan_record_aux_stack_rcu(void *ptr); #else /* CONFIG_KASAN_GENERIC */ @@ -512,6 +516,7 @@ static inline void kasan_cache_create(struct kmem_cache *cache, static inline void kasan_cache_shrink(struct kmem_cache *cache) {} static inline void kasan_cache_shutdown(struct kmem_cache *cache) {} static inline void kasan_record_aux_stack(void *ptr) {} +static inline void kasan_record_aux_stack_rcu(void *ptr) {} #endif /* CONFIG_KASAN_GENERIC */ diff --git a/mm/kasan/common.c b/mm/kasan/common.c index 7d21c4db68d2..4187606a88cf 100644 --- a/mm/kasan/common.c +++ b/mm/kasan/common.c @@ -68,6 +68,7 @@ void kasan_set_track(struct kasan_track *track, depot_stack_handle_t stack) track->timestamp = ts_nsec >> 9; #endif /* CONFIG_KASAN_EXTRA_INFO */ track->pid = current->pid; + track->rcu_free = 0; track->stack = stack; } @@ -231,7 +232,7 @@ void * __must_check __kasan_init_slab_obj(struct kmem_cache *cache, /* Returns true when freeing the object is not safe. */ static bool check_slab_allocation(struct kmem_cache *cache, void *object, - unsigned long ip) + unsigned long ip, bool after_rcu_delay) { void *tagged_object = object; @@ -242,7 +243,8 @@ static bool check_slab_allocation(struct kmem_cache *cache, void *object, return true; } - if (!kasan_byte_accessible(tagged_object)) { + if (!kasan_byte_accessible(tagged_object) || + kasan_check_rcu_free(cache, object, after_rcu_delay)) { kasan_report_invalid_free(tagged_object, ip, KASAN_REPORT_DOUBLE_FREE); return true; } @@ -259,17 +261,18 @@ static inline void poison_slab_object(struct kmem_cache *cache, void *object, kasan_poison(object, round_up(cache->object_size, KASAN_GRANULE_SIZE), KASAN_SLAB_FREE, init); + kasan_clear_rcu_free(cache, object); if (kasan_stack_collection_enabled()) kasan_save_free_info(cache, tagged_object); } bool __kasan_slab_pre_free(struct kmem_cache *cache, void *object, - unsigned long ip) + unsigned long ip, bool after_rcu_delay) { if (is_kfence_address(object)) return false; - return check_slab_allocation(cache, object, ip); + return check_slab_allocation(cache, object, ip, after_rcu_delay); } bool __kasan_slab_free(struct kmem_cache *cache, void *object, bool init, @@ -544,7 +547,7 @@ bool __kasan_mempool_poison_object(void *ptr, unsigned long ip) slab = page_slab(page); - if (check_slab_allocation(slab->slab_cache, ptr, ip)) + if (check_slab_allocation(slab->slab_cache, ptr, ip, false)) return false; poison_slab_object(slab->slab_cache, ptr, false); diff --git a/mm/kasan/generic.c b/mm/kasan/generic.c index 2b8e73f5f6a7..07636ffe1626 100644 --- a/mm/kasan/generic.c +++ b/mm/kasan/generic.c @@ -536,7 +536,7 @@ size_t kasan_metadata_size(struct kmem_cache *cache, bool in_object) * This function avoids dynamic memory allocations and thus can be called from * contexts that do not allow allocating memory. */ -void kasan_record_aux_stack(void *addr) +static void __kasan_record_aux_stack(void *addr, bool rcu_free) { struct slab *slab = kasan_addr_to_slab(addr); struct kmem_cache *cache; @@ -552,10 +552,22 @@ void kasan_record_aux_stack(void *addr) if (!alloc_meta) return; + if (rcu_free) + alloc_meta->alloc_track.rcu_free = 1; alloc_meta->aux_stack[1] = alloc_meta->aux_stack[0]; alloc_meta->aux_stack[0] = kasan_save_stack(0, 0); } +void kasan_record_aux_stack(void *addr) +{ + __kasan_record_aux_stack(addr, false); +} + +void kasan_record_aux_stack_rcu(void *addr) +{ + __kasan_record_aux_stack(addr, true); +} + void kasan_save_alloc_info(struct kmem_cache *cache, void *object, gfp_t flags) { struct kasan_alloc_meta *alloc_meta; diff --git a/mm/kasan/kasan.h b/mm/kasan/kasan.h index fc9169a54766..43d608899544 100644 --- a/mm/kasan/kasan.h +++ b/mm/kasan/kasan.h @@ -191,7 +191,10 @@ static inline bool kasan_requires_meta(void) #define KASAN_STACK_DEPTH 64 struct kasan_track { - u32 pid; +#define KASAN_PID_BITS const_ilog2(PID_MAX_LIMIT) + u32 pid:KASAN_PID_BITS; + u32 rcu_free:1; + u32 unused:32 - (KASAN_PID_BITS + 1); depot_stack_handle_t stack; #ifdef CONFIG_KASAN_EXTRA_INFO u64 cpu:20; @@ -390,8 +393,37 @@ struct kasan_alloc_meta *kasan_get_alloc_meta(struct kmem_cache *cache, struct kasan_free_meta *kasan_get_free_meta(struct kmem_cache *cache, const void *object); void kasan_init_object_meta(struct kmem_cache *cache, const void *object); + +static inline bool kasan_check_rcu_free(struct kmem_cache *cache, + const void *object, + bool after_rcu_delay) +{ + struct kasan_alloc_meta *alloc_meta; + + alloc_meta = kasan_get_alloc_meta(cache, object); + if (!alloc_meta) + return false; + + return alloc_meta->alloc_track.rcu_free != after_rcu_delay; +} + +static inline void kasan_clear_rcu_free(struct kmem_cache *cache, + const void *object) +{ + struct kasan_alloc_meta *alloc_meta; + + alloc_meta = kasan_get_alloc_meta(cache, object); + if (alloc_meta) + alloc_meta->alloc_track.rcu_free = 0; +} #else static inline void kasan_init_object_meta(struct kmem_cache *cache, const void *object) { } +static inline bool kasan_check_rcu_free(struct kmem_cache *cache, const void *object, + bool after_rcu_delay) +{ + return false; +} +static inline void kasan_clear_rcu_free(struct kmem_cache *cache, const void *object) { } #endif depot_stack_handle_t kasan_save_stack(gfp_t flags, depot_flags_t depot_flags); diff --git a/mm/slub.c b/mm/slub.c index 544cff39762c..8a08ca73d76b 100644 --- a/mm/slub.c +++ b/mm/slub.c @@ -2690,7 +2690,7 @@ bool slab_free_hook(struct kmem_cache *s, void *x, bool init, * Give KASAN a chance to notice an invalid free operation before we * modify the object. */ - if (kasan_slab_pre_free(s, x)) + if (kasan_slab_pre_free(s, x, after_rcu_delay)) return false; #ifdef CONFIG_SLUB_RCU_DEBUG @@ -2706,7 +2706,7 @@ bool slab_free_hook(struct kmem_cache *s, void *x, bool init, * We have to do this manually because the rcu_head is * not located inside the object. */ - kasan_record_aux_stack(x); + kasan_record_aux_stack_rcu(x); delayed_free->object = x; call_rcu(&delayed_free->head, slab_free_after_rcu_debug); @@ -6177,6 +6177,12 @@ bool __kfree_rcu_sheaf(struct kmem_cache *s, void *obj, unsigned int free_flags) } do_free: + if (unlikely(kasan_slab_pre_free(s, obj, false))) { + local_unlock(&s->cpu_sheaves->lock); + goto out; + } + + kasan_record_aux_stack_rcu(obj); rcu_sheaf = pcs->rcu_free; @@ -6217,6 +6223,7 @@ bool __kfree_rcu_sheaf(struct kmem_cache *s, void *obj, unsigned int free_flags) local_unlock(&s->cpu_sheaves->lock); stat(s, FREE_RCU_SHEAF); +out: if (!IS_ENABLED(CONFIG_PREEMPT_RT)) lock_map_release(&kfree_rcu_sheaf_map); return true; @@ -6850,7 +6857,7 @@ void kfree_nolock(const void *object) * -> kfree_nolock() -> kasan_report_invalid_free() on the same CPU * is double buggy and deserves to deadlock. */ - if (kasan_slab_pre_free(s, x)) + if (kasan_slab_pre_free(s, x, false)) return; /* * memcg, kasan_slab_pre_free are done for 'x'. -- 2.56.0.385.gd3acb90ef8-goog ^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH 1/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf(). 2026-10-09 21:46 ` [PATCH 1/2] " Kuniyuki Iwashima @ 2026-10-20 12:29 ` Vlastimil Babka (SUSE) 2026-10-27 12:48 ` Alexander Potapenko 1 sibling, 0 replies; 9+ messages in thread From: Vlastimil Babka (SUSE) @ 2026-10-20 12:29 UTC (permalink / raw) To: Kuniyuki Iwashima, Andrey Ryabinin, Andrew Morton, Harry Yoo Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Hao Li, Christoph Lameter, David Rientjes, Roman Gushchin, kasan-dev, linux-mm On 10/9/26 23:46, Kuniyuki Iwashima wrote: > syzbot reported a weird use-after-free splat. [0] > > In the report, an object in the kmalloc-64 cache was freed via > rcu_free_sheaf() while it was still in use, even though the > object should always be freed with kfree() (in xfrm_hash_free()), > implying that the object was a victim of a double-free involving > kfree_rcu() somewhere else. > > When kvfree_call_rcu() frees an object via kfree_rcu_sheaf() and > queues it into pcs->rcu_free, debug facilities are skipped, e.g., > kasan_slab_pre_free(), kasan_record_aux_stack(), > debug_rcu_head_queue(), etc. > > As a result, KASAN cannot detect the double-free at the call site. > > If the object is still in the KASAN quarantine when the sheaf is > flushed, KASAN can detect the double-free in rcu_free_sheaf(), but > with no trace of the suspect. > > Worse yet, once the object is evicted from the quarantine and > reallocated before the sheaf is flushed, rcu_free_sheaf() silently > poisons the live object, resulting in a false-positive use-after-free > report on the new user. > > If __kfree_rcu_sheaf() simply calls kasan_slab_pre_free(), we > can detect the second kfree_rcu(), but only while the object is > poisoned in the KASAN quarantine after the first free: > > kfree() --(poisoned)--------------------> kfree_rcu() > kfree_rcu() -> rcu_free_sheaf() -(poisoned)-> kfree_rcu() > > An object queued in pcs->rcu_free remains unpoisoned until the > sheaf becomes full and rcu_free_sheaf() runs after the RCU > grace period, so a subsequent kfree_rcu() or kfree() before > rcu_free_sheaf() cannot be detected by shadow bytes alone: > > kfree_rcu() -(unpoisoned)-> kfree_rcu() -> rcu_free_sheaf() (too late!) > kfree_rcu() -(unpoisoned)-> kfree() -> rcu_free_sheaf() (too late!) > > These cases can be detected if we call debug_rcu_head_queue() > before kfree_rcu_sheaf(), but ODEBUG splat only shows the second > free. > > To detect these cases with KASAN, we just need a 1-bit flag > to indicate the state, which can reuse an unused bit in struct > kasan_track.pid. (PID_MAX_LIMIT is 1<<22) > > The bit is marked along with recording the stack trace when > deferring slab frees in kfree_rcu_sheaf() and checked in > check_slab_allocation() so that any double-free pattern above > can be detected immediately. > > Link: https://lore.kernel.org/all/6ac959ee.70ed5d86.2ac85.007d.GAE@google.com/ #[0] > Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com> Acked-by: Vlastimil Babka (SUSE) <vbabka@kernel.org> For the slab part. > --- > include/linux/kasan.h | 13 +++++++++---- > mm/kasan/common.c | 13 ++++++++----- > mm/kasan/generic.c | 14 +++++++++++++- > mm/kasan/kasan.h | 34 +++++++++++++++++++++++++++++++++- > mm/slub.c | 13 ++++++++++--- > 5 files changed, 73 insertions(+), 14 deletions(-) > > diff --git a/include/linux/kasan.h b/include/linux/kasan.h > index bf233bde68c7..3a43e3d2658e 100644 > --- a/include/linux/kasan.h > +++ b/include/linux/kasan.h > @@ -180,7 +180,7 @@ static __always_inline void * __must_check kasan_init_slab_obj( > } > > bool __kasan_slab_pre_free(struct kmem_cache *s, void *object, > - unsigned long ip); > + unsigned long ip, bool after_rcu_delay); > /** > * kasan_slab_pre_free - Check whether freeing a slab object is safe. > * @object: Object to be freed. > @@ -193,10 +193,12 @@ bool __kasan_slab_pre_free(struct kmem_cache *s, void *object, > * @Return true if freeing the object is unsafe; false otherwise. > */ > static __always_inline bool kasan_slab_pre_free(struct kmem_cache *s, > - void *object) > + void *object, > + bool after_rcu_delay) > { > if (kasan_enabled()) > - return __kasan_slab_pre_free(s, object, _RET_IP_); > + return __kasan_slab_pre_free(s, object, _RET_IP_, > + after_rcu_delay); > return false; > } > > @@ -424,7 +426,8 @@ static inline void *kasan_init_slab_obj(struct kmem_cache *cache, > return (void *)object; > } > > -static inline bool kasan_slab_pre_free(struct kmem_cache *s, void *object) > +static inline bool kasan_slab_pre_free(struct kmem_cache *s, void *object, > + bool after_rcu_delay) > { > return false; > } > @@ -495,6 +498,7 @@ void kasan_cache_create(struct kmem_cache *cache, unsigned int *size, > void kasan_cache_shrink(struct kmem_cache *cache); > void kasan_cache_shutdown(struct kmem_cache *cache); > void kasan_record_aux_stack(void *ptr); > +void kasan_record_aux_stack_rcu(void *ptr); > > #else /* CONFIG_KASAN_GENERIC */ > > @@ -512,6 +516,7 @@ static inline void kasan_cache_create(struct kmem_cache *cache, > static inline void kasan_cache_shrink(struct kmem_cache *cache) {} > static inline void kasan_cache_shutdown(struct kmem_cache *cache) {} > static inline void kasan_record_aux_stack(void *ptr) {} > +static inline void kasan_record_aux_stack_rcu(void *ptr) {} > > #endif /* CONFIG_KASAN_GENERIC */ > > diff --git a/mm/kasan/common.c b/mm/kasan/common.c > index 7d21c4db68d2..4187606a88cf 100644 > --- a/mm/kasan/common.c > +++ b/mm/kasan/common.c > @@ -68,6 +68,7 @@ void kasan_set_track(struct kasan_track *track, depot_stack_handle_t stack) > track->timestamp = ts_nsec >> 9; > #endif /* CONFIG_KASAN_EXTRA_INFO */ > track->pid = current->pid; > + track->rcu_free = 0; > track->stack = stack; > } > > @@ -231,7 +232,7 @@ void * __must_check __kasan_init_slab_obj(struct kmem_cache *cache, > > /* Returns true when freeing the object is not safe. */ > static bool check_slab_allocation(struct kmem_cache *cache, void *object, > - unsigned long ip) > + unsigned long ip, bool after_rcu_delay) > { > void *tagged_object = object; > > @@ -242,7 +243,8 @@ static bool check_slab_allocation(struct kmem_cache *cache, void *object, > return true; > } > > - if (!kasan_byte_accessible(tagged_object)) { > + if (!kasan_byte_accessible(tagged_object) || > + kasan_check_rcu_free(cache, object, after_rcu_delay)) { > kasan_report_invalid_free(tagged_object, ip, KASAN_REPORT_DOUBLE_FREE); > return true; > } > @@ -259,17 +261,18 @@ static inline void poison_slab_object(struct kmem_cache *cache, void *object, > > kasan_poison(object, round_up(cache->object_size, KASAN_GRANULE_SIZE), > KASAN_SLAB_FREE, init); > + kasan_clear_rcu_free(cache, object); > > if (kasan_stack_collection_enabled()) > kasan_save_free_info(cache, tagged_object); > } > > bool __kasan_slab_pre_free(struct kmem_cache *cache, void *object, > - unsigned long ip) > + unsigned long ip, bool after_rcu_delay) > { > if (is_kfence_address(object)) > return false; > - return check_slab_allocation(cache, object, ip); > + return check_slab_allocation(cache, object, ip, after_rcu_delay); > } > > bool __kasan_slab_free(struct kmem_cache *cache, void *object, bool init, > @@ -544,7 +547,7 @@ bool __kasan_mempool_poison_object(void *ptr, unsigned long ip) > > slab = page_slab(page); > > - if (check_slab_allocation(slab->slab_cache, ptr, ip)) > + if (check_slab_allocation(slab->slab_cache, ptr, ip, false)) > return false; > > poison_slab_object(slab->slab_cache, ptr, false); > diff --git a/mm/kasan/generic.c b/mm/kasan/generic.c > index 2b8e73f5f6a7..07636ffe1626 100644 > --- a/mm/kasan/generic.c > +++ b/mm/kasan/generic.c > @@ -536,7 +536,7 @@ size_t kasan_metadata_size(struct kmem_cache *cache, bool in_object) > * This function avoids dynamic memory allocations and thus can be called from > * contexts that do not allow allocating memory. > */ > -void kasan_record_aux_stack(void *addr) > +static void __kasan_record_aux_stack(void *addr, bool rcu_free) > { > struct slab *slab = kasan_addr_to_slab(addr); > struct kmem_cache *cache; > @@ -552,10 +552,22 @@ void kasan_record_aux_stack(void *addr) > if (!alloc_meta) > return; > > + if (rcu_free) > + alloc_meta->alloc_track.rcu_free = 1; > alloc_meta->aux_stack[1] = alloc_meta->aux_stack[0]; > alloc_meta->aux_stack[0] = kasan_save_stack(0, 0); > } > > +void kasan_record_aux_stack(void *addr) > +{ > + __kasan_record_aux_stack(addr, false); > +} > + > +void kasan_record_aux_stack_rcu(void *addr) > +{ > + __kasan_record_aux_stack(addr, true); > +} > + > void kasan_save_alloc_info(struct kmem_cache *cache, void *object, gfp_t flags) > { > struct kasan_alloc_meta *alloc_meta; > diff --git a/mm/kasan/kasan.h b/mm/kasan/kasan.h > index fc9169a54766..43d608899544 100644 > --- a/mm/kasan/kasan.h > +++ b/mm/kasan/kasan.h > @@ -191,7 +191,10 @@ static inline bool kasan_requires_meta(void) > #define KASAN_STACK_DEPTH 64 > > struct kasan_track { > - u32 pid; > +#define KASAN_PID_BITS const_ilog2(PID_MAX_LIMIT) > + u32 pid:KASAN_PID_BITS; > + u32 rcu_free:1; > + u32 unused:32 - (KASAN_PID_BITS + 1); > depot_stack_handle_t stack; > #ifdef CONFIG_KASAN_EXTRA_INFO > u64 cpu:20; > @@ -390,8 +393,37 @@ struct kasan_alloc_meta *kasan_get_alloc_meta(struct kmem_cache *cache, > struct kasan_free_meta *kasan_get_free_meta(struct kmem_cache *cache, > const void *object); > void kasan_init_object_meta(struct kmem_cache *cache, const void *object); > + > +static inline bool kasan_check_rcu_free(struct kmem_cache *cache, > + const void *object, > + bool after_rcu_delay) > +{ > + struct kasan_alloc_meta *alloc_meta; > + > + alloc_meta = kasan_get_alloc_meta(cache, object); > + if (!alloc_meta) > + return false; > + > + return alloc_meta->alloc_track.rcu_free != after_rcu_delay; > +} > + > +static inline void kasan_clear_rcu_free(struct kmem_cache *cache, > + const void *object) > +{ > + struct kasan_alloc_meta *alloc_meta; > + > + alloc_meta = kasan_get_alloc_meta(cache, object); > + if (alloc_meta) > + alloc_meta->alloc_track.rcu_free = 0; > +} > #else > static inline void kasan_init_object_meta(struct kmem_cache *cache, const void *object) { } > +static inline bool kasan_check_rcu_free(struct kmem_cache *cache, const void *object, > + bool after_rcu_delay) > +{ > + return false; > +} > +static inline void kasan_clear_rcu_free(struct kmem_cache *cache, const void *object) { } > #endif > > depot_stack_handle_t kasan_save_stack(gfp_t flags, depot_flags_t depot_flags); > diff --git a/mm/slub.c b/mm/slub.c > index 544cff39762c..8a08ca73d76b 100644 > --- a/mm/slub.c > +++ b/mm/slub.c > @@ -2690,7 +2690,7 @@ bool slab_free_hook(struct kmem_cache *s, void *x, bool init, > * Give KASAN a chance to notice an invalid free operation before we > * modify the object. > */ > - if (kasan_slab_pre_free(s, x)) > + if (kasan_slab_pre_free(s, x, after_rcu_delay)) > return false; > > #ifdef CONFIG_SLUB_RCU_DEBUG > @@ -2706,7 +2706,7 @@ bool slab_free_hook(struct kmem_cache *s, void *x, bool init, > * We have to do this manually because the rcu_head is > * not located inside the object. > */ > - kasan_record_aux_stack(x); > + kasan_record_aux_stack_rcu(x); > > delayed_free->object = x; > call_rcu(&delayed_free->head, slab_free_after_rcu_debug); > @@ -6177,6 +6177,12 @@ bool __kfree_rcu_sheaf(struct kmem_cache *s, void *obj, unsigned int free_flags) > } > > do_free: > + if (unlikely(kasan_slab_pre_free(s, obj, false))) { > + local_unlock(&s->cpu_sheaves->lock); > + goto out; > + } > + > + kasan_record_aux_stack_rcu(obj); > > rcu_sheaf = pcs->rcu_free; > > @@ -6217,6 +6223,7 @@ bool __kfree_rcu_sheaf(struct kmem_cache *s, void *obj, unsigned int free_flags) > local_unlock(&s->cpu_sheaves->lock); > > stat(s, FREE_RCU_SHEAF); > +out: > if (!IS_ENABLED(CONFIG_PREEMPT_RT)) > lock_map_release(&kfree_rcu_sheaf_map); > return true; > @@ -6850,7 +6857,7 @@ void kfree_nolock(const void *object) > * -> kfree_nolock() -> kasan_report_invalid_free() on the same CPU > * is double buggy and deserves to deadlock. > */ > - if (kasan_slab_pre_free(s, x)) > + if (kasan_slab_pre_free(s, x, false)) > return; > /* > * memcg, kasan_slab_pre_free are done for 'x'. ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH 1/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf(). 2026-10-09 21:46 ` [PATCH 1/2] " Kuniyuki Iwashima 2026-10-20 12:29 ` Vlastimil Babka (SUSE) @ 2026-10-27 12:48 ` Alexander Potapenko 2026-10-27 21:51 ` Kuniyuki Iwashima 1 sibling, 1 reply; 9+ messages in thread From: Alexander Potapenko @ 2026-10-27 12:48 UTC (permalink / raw) To: Kuniyuki Iwashima Cc: Andrey Ryabinin, Andrew Morton, Vlastimil Babka, Harry Yoo, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Hao Li, Christoph Lameter, David Rientjes, Roman Gushchin, kasan-dev, linux-mm On Fri, Oct 9, 2026 at 11:46 PM Kuniyuki Iwashima <kuniyu@google.com> wrote: > The bit is marked along with recording the stack trace when > deferring slab frees in kfree_rcu_sheaf() and checked in > check_slab_allocation() so that any double-free pattern above > can be detected immediately. > > Link: https://lore.kernel.org/all/6ac959ee.70ed5d86.2ac85.007d.GAE@google.com/ #[0] > Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com> Tested-by: Alexander Potapenko <glider@google.com> Do we need a Fixes: tag? > */ > static __always_inline bool kasan_slab_pre_free(struct kmem_cache *s, > - void *object) > + void *object, > + bool after_rcu_delay) Please update the doc comment as well. > poison_slab_object(slab->slab_cache, ptr, false); > diff --git a/mm/kasan/generic.c b/mm/kasan/generic.c > index 2b8e73f5f6a7..07636ffe1626 100644 > --- a/mm/kasan/generic.c > +++ b/mm/kasan/generic.c > @@ -536,7 +536,7 @@ size_t kasan_metadata_size(struct kmem_cache *cache, bool in_object) > * This function avoids dynamic memory allocations and thus can be called from > * contexts that do not allow allocating memory. > */ Please update the comment for this function. > +void kasan_record_aux_stack(void *addr) > +{ > + __kasan_record_aux_stack(addr, false); > +} > + I think there's an important distinction between kasan_record_aux_stack() and kasan_record_aux_stack_rcu(): namely, it affects how this object is handled later. Let's somehow reflect this in a comment ("records the aux stack and arms the check") and perhaps rename it to something like kasan_mark_rcu_free_pending()? Otherwise the series looks good, I'll be happy to stamp it once you fix the comments. ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH 1/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf(). 2026-10-27 12:48 ` Alexander Potapenko @ 2026-10-27 21:51 ` Kuniyuki Iwashima 0 siblings, 0 replies; 9+ messages in thread From: Kuniyuki Iwashima @ 2026-10-27 21:51 UTC (permalink / raw) To: Alexander Potapenko Cc: Andrey Ryabinin, Andrew Morton, Vlastimil Babka, Harry Yoo, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Hao Li, Christoph Lameter, David Rientjes, Roman Gushchin, kasan-dev, linux-mm On Tue, 27 Oct 2026 13:48:03 +0100, Alexander Potapenko wrote: > On Fri, Oct 9, 2026 at 11:46 PM Kuniyuki Iwashima <kuniyu@google.com> wrote: > > > The bit is marked along with recording the stack trace when > > deferring slab frees in kfree_rcu_sheaf() and checked in > > check_slab_allocation() so that any double-free pattern above > > can be detected immediately. > > > > Link: https://lore.kernel.org/all/6ac959ee.70ed5d86.2ac85.007d.GAE@google.com/ #[0] > > Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com> > Tested-by: Alexander Potapenko <glider@google.com> > > Do we need a Fixes: tag? Will add Fixes: ec66e0d59952 ("slab: add sheaf support for batching kfree_rcu() operations") > > > > > > */ > > static __always_inline bool kasan_slab_pre_free(struct kmem_cache *s, > > - void *object) > > + void *object, > > + bool after_rcu_delay) > > Please update the doc comment as well. Will do. > > > > poison_slab_object(slab->slab_cache, ptr, false); > > diff --git a/mm/kasan/generic.c b/mm/kasan/generic.c > > index 2b8e73f5f6a7..07636ffe1626 100644 > > --- a/mm/kasan/generic.c > > +++ b/mm/kasan/generic.c > > @@ -536,7 +536,7 @@ size_t kasan_metadata_size(struct kmem_cache *cache, bool in_object) > > * This function avoids dynamic memory allocations and thus can be called from > > * contexts that do not allow allocating memory. > > */ > > Please update the comment for this function. Will do. > > > > +void kasan_record_aux_stack(void *addr) > > +{ > > + __kasan_record_aux_stack(addr, false); > > +} > > + > > I think there's an important distinction between > kasan_record_aux_stack() and kasan_record_aux_stack_rcu(): namely, it > affects how this object is handled later. > Let's somehow reflect this in a comment ("records the aux stack and > arms the check") and perhaps rename it to something like > kasan_mark_rcu_free_pending()? Sure, will rename to it and add comments. > > Otherwise the series looks good, I'll be happy to stamp it once you > fix the comments. > Thank you ! ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 2/2] mm/kasan/kunit: Add double-free tests involving kfree_rcu_sheaf(). 2026-10-09 21:46 [PATCH 0/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf() Kuniyuki Iwashima 2026-10-09 21:46 ` [PATCH 1/2] " Kuniyuki Iwashima @ 2026-10-09 21:46 ` Kuniyuki Iwashima 2026-10-27 12:48 ` Alexander Potapenko 2026-10-27 6:15 ` [PATCH 0/2] mm/kasan: Detect double-free " Kuniyuki Iwashima 2 siblings, 1 reply; 9+ messages in thread From: Kuniyuki Iwashima @ 2026-10-09 21:46 UTC (permalink / raw) To: Andrey Ryabinin, Andrew Morton, Vlastimil Babka, Harry Yoo Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Hao Li, Christoph Lameter, David Rientjes, Roman Gushchin, kasan-dev, linux-mm, Kuniyuki Iwashima The new test cases verify that KASAN immediately detects all three combinations of double-free involving kfree_rcu_sheaf(): 1. kmem_cache_free() -> kfree_rcu() 2. kfree_rcu() -> kmem_cache_free() 3. kfree_rcu() -> kfree_rcu() Each test also evicts the object from the KASAN quarantine and reallocates it as a victim object before flushing the RCU sheaf to ensure the live victim object is not poisoned by the rejected double-free. Without the patch, all tests fail to detect the 2nd free, and the victim is poisoned: # kmem_cache_double_free_rcu_1: EXPECTATION FAILED at mm/kasan/kasan_test_c.c:1096 KASAN failure expected in "kfree_rcu(p, rcu)", but none occurred ... <splat below is logged here> # kmem_cache_double_free_rcu_1: EXPECTATION FAILED at mm/kasan/kasan_test_c.c:1156 Expected (<...snipped READ_ONCE()...>&(victim->i)); }) == 42, but <...snipped READ_ONCE()...>victim->i == 117729280 (0x7046800) # kasan: pass:0 fail:3 skip:0 total:3 # Totals: pass:0 fail:3 skip:0 total:3 not ok 1 kasan In all cases, KASAN logs a false-positive use-after-free splat at READ_ONCE(victim->i), and it has no information about the suspect; the two stack traces ("Call Trace:" and "Allocated by") are of the live victim, and "Freed by" is of the RCU task. BUG: KASAN: slab-use-after-free in __kmem_cache_double_free_rcu+0x9ee/0xaf0 Read of size 4 at addr ffff8881076c2938 by task kunit_try_catch/513 ... Freed by task 0: ... __rcu_free_sheaf_prepare+0xf6/0x240 rcu_free_sheaf_nobarn+0x31/0xe0 rcu_core+0x75e/0x16b0 With the patch, all tests can detect the 2nd free immediately and prevent the victim from being poisoned: # kasan: pass:3 fail:0 skip:0 total:3 # Totals: pass:3 fail:0 skip:0 total:3 ok 1 kasan All the stack traces point to the same object for double-free, and the two free traces are shown in "Call Trace:" and "Freed by" (or "Last potentially related work creation:" in the 2nd/3rd test cases). BUG: KASAN: double-free in kvfree_call_rcu+0xb6/0x3c0 Free of addr ffff888108495938 by task kunit_try_catch/511 ... Call Trace: <TASK> ... check_slab_allocation+0x10d/0x120 __kfree_rcu_sheaf+0x1c/0x2d0 kvfree_call_rcu+0xb6/0x3c0 __kmem_cache_double_free_rcu+0x310/0xaf0 Allocated by task 511: ... kmem_cache_alloc_noprof+0x14f/0x3c0 __kmem_cache_double_free_rcu+0x152/0xaf0 Freed by task 511: ... kmem_cache_free+0x14f/0x3c0 __kmem_cache_double_free_rcu+0x2c6/0xaf0 Notably, in the 2nd/3rd test cases, we can detect the 2nd free before the object is poisoned: Memory state around the buggy address: ffff888106dc7800: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc ffff888106dc7880: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc >ffff888106dc7900: fc fc fc fc fc fc fc 00 00 00 fc fc fc fc fc fc ^ ... ok 2 kmem_cache_double_free_rcu_2 Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com> --- mm/kasan/kasan_test_c.c | 131 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/mm/kasan/kasan_test_c.c b/mm/kasan/kasan_test_c.c index b9e167ed5be3..60d25eb7cb01 100644 --- a/mm/kasan/kasan_test_c.c +++ b/mm/kasan/kasan_test_c.c @@ -30,6 +30,7 @@ #include <asm/page.h> #include "kasan.h" +#include "../slab.h" #define OOB_TAG_OFF (IS_ENABLED(CONFIG_KASAN_GENERIC) ? 0 : KASAN_GRANULE_SIZE) @@ -1070,6 +1071,131 @@ static void kmem_cache_double_free(struct kunit *test) kmem_cache_destroy(cache); } +#ifndef MODULE +static void __kmem_cache_double_free_rcu(struct kunit *test, int mode) +{ + struct kasan_rcu_info *p, *victim = NULL, **objs; + struct kmem_cache *cache; + int i, n = 0; + + KASAN_TEST_NEEDS_CONFIG_ON(test, CONFIG_KASAN_GENERIC); + + cache = kmem_cache_create("test_cache", sizeof(*p), 0, SLAB_NO_MERGE, + NULL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cache); + + if (!cache->sheaf_capacity) { + kmem_cache_destroy(cache); + kunit_skip(test, "Test requires sheaves"); + } + + objs = kunit_kcalloc(test, cache->sheaf_capacity * 2, sizeof(*objs), + GFP_KERNEL); + if (!objs) { + kunit_err(test, "Allocation failed: %s\n", __func__); + goto out; + } + + p = kmem_cache_alloc(cache, GFP_KERNEL); + if (!p) { + kunit_err(test, "Allocation failed: %s\n", __func__); + goto out; + } + + switch (mode) { + case 1: + kmem_cache_free(cache, p); + /* p is already in quarantine. */ + KUNIT_EXPECT_KASAN_FAIL(test, kfree_rcu(p, rcu)); + break; + case 2: + /* The actual free order is inverse. */ + kfree_rcu(p, rcu); + KUNIT_EXPECT_KASAN_FAIL(test, kmem_cache_free(cache, p)); + + /* Move p to quarantine if kmem_cache_free() was rejected. */ + if (kasan_byte_accessible(p)) + kvfree_rcu_barrier_on_cache(cache); + break; + case 3: + migrate_disable(); + kfree_rcu(p, rcu); + + for (i = 0; i < cache->sheaf_capacity - 1; i++) { + objs[i] = kmem_cache_alloc(cache, GFP_KERNEL); + kfree_rcu(objs[i], rcu); + } + + KUNIT_EXPECT_KASAN_FAIL(test, kfree_rcu(p, rcu)); + migrate_enable(); + + /* + * Move the 1st full sheaf to quarantine, keeping the + * 2nd in pcs->rcu_free. + */ + rcu_barrier(); + break; + } + + /* + * Evict p from quarantine and reallocate it as a live victim object + * before the RCU sheaf is flushed. + */ + kasan_quarantine_remove_cache(cache); + + for (i = 0; i < cache->sheaf_capacity * 2; i++) { + objs[n] = kmem_cache_alloc(cache, GFP_KERNEL); + if (!objs[n]) + break; + if (objs[n] == p) { + victim = objs[n]; + break; + } + n++; + } + + for (i = 0; i < n; i++) + kmem_cache_free(cache, objs[i]); + + KUNIT_EXPECT_PTR_EQ(test, victim, p); + if (victim != p) + goto out; + + WRITE_ONCE(victim->i, 42); + + /* + * If kfree_rcu() failed to reject the double-free above, + * flushing the RCU sheaf will silently poison the live + * victim and cause a false-positive UAF on victim->i. + */ + kvfree_rcu_barrier_on_cache(cache); + KUNIT_EXPECT_EQ(test, READ_ONCE(victim->i), 42); + + if (!READ_ONCE(test_status.report_found)) + kmem_cache_free(cache, victim); +out: + kmem_cache_destroy(cache); +} + +static void kmem_cache_double_free_rcu_1(struct kunit *test) +{ + /* kmem_cache_free() -> kfree_rcu() */ + __kmem_cache_double_free_rcu(test, 1); +} + +static void kmem_cache_double_free_rcu_2(struct kunit *test) +{ + /* kfree_rcu() -> kmem_cache_free() */ + __kmem_cache_double_free_rcu(test, 2); +} + +static void kmem_cache_double_free_rcu_3(struct kunit *test) +{ + /* kfree_rcu() -> kfree_rcu() */ + __kmem_cache_double_free_rcu(test, 3); +} +#endif + static void kmem_cache_invalid_free(struct kunit *test) { char *p; @@ -2239,6 +2365,11 @@ static struct kunit_case kasan_kunit_test_cases[] = { KUNIT_CASE(kfree_via_phys), KUNIT_CASE(kmem_cache_oob), KUNIT_CASE(kmem_cache_double_free), +#ifndef MODULE + KUNIT_CASE(kmem_cache_double_free_rcu_1), + KUNIT_CASE(kmem_cache_double_free_rcu_2), + KUNIT_CASE(kmem_cache_double_free_rcu_3), +#endif KUNIT_CASE(kmem_cache_invalid_free), KUNIT_CASE(kmem_cache_rcu_uaf), KUNIT_CASE(kmem_cache_rcu_reuse), -- 2.56.0.385.gd3acb90ef8-goog ^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH 2/2] mm/kasan/kunit: Add double-free tests involving kfree_rcu_sheaf(). 2026-10-09 21:46 ` [PATCH 2/2] mm/kasan/kunit: Add double-free tests " Kuniyuki Iwashima @ 2026-10-27 12:48 ` Alexander Potapenko 2026-10-27 22:16 ` Kuniyuki Iwashima 0 siblings, 1 reply; 9+ messages in thread From: Alexander Potapenko @ 2026-10-27 12:48 UTC (permalink / raw) To: Kuniyuki Iwashima Cc: Andrey Ryabinin, Andrew Morton, Vlastimil Babka, Harry Yoo, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Hao Li, Christoph Lameter, David Rientjes, Roman Gushchin, kasan-dev, linux-mm > ^ > ... > ok 2 kmem_cache_double_free_rcu_2 > > Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com> Tested-by: Alexander Potapenko <glider@google.com> > + case 3: > + migrate_disable(); > + kfree_rcu(p, rcu); > + > + for (i = 0; i < cache->sheaf_capacity - 1; i++) { > + objs[i] = kmem_cache_alloc(cache, GFP_KERNEL); > + kfree_rcu(objs[i], rcu); > + } > + > + KUNIT_EXPECT_KASAN_FAIL(test, kfree_rcu(p, rcu)); > + migrate_enable(); > + > + /* > + * Move the 1st full sheaf to quarantine, keeping the > + * 2nd in pcs->rcu_free. > + */ > + rcu_barrier(); > + break; > + } Unlike the existing double-free tests, this one can misfire on a healthy kernel: __kfree_rcu_sheaf() may decline (local_trylock() contention, alloc_empty_sheaf() failing under memory pressure), and kvfree_call_rcu() then falls back to call_rcu(kvfree_rcu_cb), which never sets rcu_free and ends in a plain kfree(). Could you drain pending frees unconditionally (kvfree_rcu_barrier_on_cache() + rcu_barrier()) right before kasan_quarantine_remove_cache()? In the good case it's a no-op and the victim check keeps its meaning; in the fallback case the stray kfree() hits p while it is still poisoned in quarantine and is reported as a double-free instead of corrupting the victim. ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH 2/2] mm/kasan/kunit: Add double-free tests involving kfree_rcu_sheaf(). 2026-10-27 12:48 ` Alexander Potapenko @ 2026-10-27 22:16 ` Kuniyuki Iwashima 0 siblings, 0 replies; 9+ messages in thread From: Kuniyuki Iwashima @ 2026-10-27 22:16 UTC (permalink / raw) To: Alexander Potapenko Cc: Andrey Ryabinin, Andrew Morton, Vlastimil Babka, Harry Yoo, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Hao Li, Christoph Lameter, David Rientjes, Roman Gushchin, kasan-dev, linux-mm On Tue, 27 Oct 2026 13:48:05 +0100, Alexander Potapenko wrote: > > ^ > > ... > > ok 2 kmem_cache_double_free_rcu_2 > > > > Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com> > Tested-by: Alexander Potapenko <glider@google.com> > > > + case 3: > > + migrate_disable(); > > + kfree_rcu(p, rcu); > > + > > + for (i = 0; i < cache->sheaf_capacity - 1; i++) { > > + objs[i] = kmem_cache_alloc(cache, GFP_KERNEL); > > + kfree_rcu(objs[i], rcu); > > + } > > + > > + KUNIT_EXPECT_KASAN_FAIL(test, kfree_rcu(p, rcu)); > > + migrate_enable(); > > + > > + /* > > + * Move the 1st full sheaf to quarantine, keeping the > > + * 2nd in pcs->rcu_free. > > + */ > > + rcu_barrier(); > > + break; > > + } > > Unlike the existing double-free tests, this one can misfire on a > healthy kernel: __kfree_rcu_sheaf() may decline (local_trylock() > contention, > alloc_empty_sheaf() failing under memory pressure), and > kvfree_call_rcu() then falls back to call_rcu(kvfree_rcu_cb), which > never sets rcu_free and > ends in a plain kfree(). > > Could you drain pending frees unconditionally > (kvfree_rcu_barrier_on_cache() + rcu_barrier()) right before > kasan_quarantine_remove_cache()? > In the good case it's a no-op and the victim check keeps its meaning; > in the fallback case the stray kfree() hits p while it is still > poisoned in > quarantine and is reported as a double-free instead of corrupting the victim. > Calling kvfree_rcu_barrier_on_cache() unconditionally before kasan_quarantine_remove_cache() would also flush the 2nd sheaf in pcs->rcu_free while p is still in quarantine, making the victim check a no-op if the 2nd kfree_rcu() wasn't rejected (e.g. without patch 1). Instead, I'll move the kasan_byte_accessible(p) check out of case 2 like below in v2. This keeps the 2nd sheaf for the victim check in the normal case, while draining pending frees if p hasn't reached quarantine yet due to the fallback path. This keeps the false-positive UAF w/o patch 1 and double-free w/ patch 1. ---8<--- diff --git a/mm/kasan/kasan_test_c.c b/mm/kasan/kasan_test_c.c index 60d25eb7cb01..83c418c0ef87 100644 --- a/mm/kasan/kasan_test_c.c +++ b/mm/kasan/kasan_test_c.c @@ -1112,10 +1112,6 @@ static void __kmem_cache_double_free_rcu(struct kunit *test, int mode) /* The actual free order is inverse. */ kfree_rcu(p, rcu); KUNIT_EXPECT_KASAN_FAIL(test, kmem_cache_free(cache, p)); - - /* Move p to quarantine if kmem_cache_free() was rejected. */ - if (kasan_byte_accessible(p)) - kvfree_rcu_barrier_on_cache(cache); break; case 3: migrate_disable(); @@ -1137,6 +1133,13 @@ static void __kmem_cache_double_free_rcu(struct kunit *test, int mode) break; } + /* + * Move p to quarantine if not poisoned yet (case 2, + * or fallback in case 3). + */ + if (kasan_byte_accessible(p)) + kvfree_rcu_barrier_on_cache(cache); + /* * Evict p from quarantine and reallocate it as a live victim object * before the RCU sheaf is flushed. ---8<--- ^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH 0/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf(). 2026-10-09 21:46 [PATCH 0/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf() Kuniyuki Iwashima 2026-10-09 21:46 ` [PATCH 1/2] " Kuniyuki Iwashima 2026-10-09 21:46 ` [PATCH 2/2] mm/kasan/kunit: Add double-free tests " Kuniyuki Iwashima @ 2026-10-27 6:15 ` Kuniyuki Iwashima 2 siblings, 0 replies; 9+ messages in thread From: Kuniyuki Iwashima @ 2026-10-27 6:15 UTC (permalink / raw) To: Andrey Ryabinin, Andrew Morton, Vlastimil Babka, Harry Yoo Cc: Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Hao Li, Christoph Lameter, David Rientjes, Roman Gushchin, kasan-dev, linux-mm On Fri, 09 Oct 2026 21:46:24 +0000, Kuniyuki Iwashima wrote: > Recently, syzbot has reported several weird use-after-free splats. > > What they have in common is that "Freed by" points to rcu_free_sheaf(), > whereas the object shown in "Call Trace:" and "Allocated by" is never > supposed to be freed via kfree_rcu(). > > This means the object was a victim of a double-free somewhere else and > was silently poisoned by rcu_free_sheaf() after being reallocated. > > This happens because kfree_rcu_sheaf() queues an object into the RCU > sheaf without invoking any KASAN hooks. > > This series enables KASAN to detect double-free involving > kfree_rcu_sheaf() immediately and adds KUnit tests for it. Hi KASAN maintainers, It would be appreciated if you review the series, or if respin/repost would be preferred, please let me know. Thank you ! ^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-10-27 22:17 UTC | newest] Thread overview: 9+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-10-09 21:46 [PATCH 0/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf() Kuniyuki Iwashima 2026-10-09 21:46 ` [PATCH 1/2] " Kuniyuki Iwashima 2026-10-20 12:29 ` Vlastimil Babka (SUSE) 2026-10-27 12:48 ` Alexander Potapenko 2026-10-27 21:51 ` Kuniyuki Iwashima 2026-10-09 21:46 ` [PATCH 2/2] mm/kasan/kunit: Add double-free tests " Kuniyuki Iwashima 2026-10-27 12:48 ` Alexander Potapenko 2026-10-27 22:16 ` Kuniyuki Iwashima 2026-10-27 6:15 ` [PATCH 0/2] mm/kasan: Detect double-free " Kuniyuki Iwashima
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox