* [PATCH v2 0/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf().
@ 2026-10-27 22:25 Kuniyuki Iwashima
2026-10-27 22:25 ` [PATCH v2 1/2] " Kuniyuki Iwashima
2026-10-27 22:25 ` [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests " Kuniyuki Iwashima
0 siblings, 2 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-10-27 22:25 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.
Changes:
v2:
* Patch 1
* Collect Vlastimil and Alexander tags
* Add Fixes tag
* Add kdoc for @after_rcu_delay of kasan_slab_pre_free()
* Rename s/kasan_record_aux_stack_rcu/kasan_mark_rcu_free_pending/g
* Add comment for __kasan_record_aux_stack()
* Patch 2
* Move kasan_byte_accessible() check and
kvfree_rcu_barrier_on_cache() after switch
v1: https://lore.kernel.org/linux-mm/20261009214639.2878857-1-kuniyu@google.com/
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 | 15 +++--
mm/kasan/common.c | 13 ++--
mm/kasan/generic.c | 17 ++++-
mm/kasan/kasan.h | 34 +++++++++-
mm/kasan/kasan_test_c.c | 134 ++++++++++++++++++++++++++++++++++++++++
mm/slub.c | 13 +++-
6 files changed, 212 insertions(+), 14 deletions(-)
--
2.56.0.307.g5fe7bd680c-goog
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH v2 1/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf(). 2026-10-27 22:25 [PATCH v2 0/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf() Kuniyuki Iwashima @ 2026-10-27 22:25 ` Kuniyuki Iwashima 2026-10-28 16:22 ` Alexander Potapenko 2026-10-28 18:13 ` Andrey Ryabinin 2026-10-27 22:25 ` [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests " Kuniyuki Iwashima 1 sibling, 2 replies; 9+ messages in thread From: Kuniyuki Iwashima @ 2026-10-27 22:25 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] Fixes: ec66e0d59952 ("slab: add sheaf support for batching kfree_rcu() operations") Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com> Acked-by: Vlastimil Babka (SUSE) <vbabka@kernel.org> Tested-by: Alexander Potapenko <glider@google.com> --- v2: * Add Fixes tag * Add kdoc for @after_rcu_delay of kasan_slab_pre_free() * Rename s/kasan_record_aux_stack_rcu/kasan_mark_rcu_free_pending/g * Add comment for __kasan_record_aux_stack() --- include/linux/kasan.h | 15 +++++++++++---- mm/kasan/common.c | 13 ++++++++----- mm/kasan/generic.c | 17 ++++++++++++++++- mm/kasan/kasan.h | 34 +++++++++++++++++++++++++++++++++- mm/slub.c | 13 ++++++++++--- 5 files changed, 78 insertions(+), 14 deletions(-) diff --git a/include/linux/kasan.h b/include/linux/kasan.h index bf233bde68c7..c1ac7971601e 100644 --- a/include/linux/kasan.h +++ b/include/linux/kasan.h @@ -180,10 +180,12 @@ 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. + * @s: Cache the object belongs to. * @object: Object to be freed. + * @after_rcu_delay: Whether the free follows an RCU grace period. * * This function checks whether freeing the given object is safe. It may * check for double-free and invalid-free bugs and report them. @@ -193,10 +195,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 +428,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 +500,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_mark_rcu_free_pending(void *ptr); #else /* CONFIG_KASAN_GENERIC */ @@ -512,6 +518,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_mark_rcu_free_pending(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..3532a41ce966 100644 --- a/mm/kasan/generic.c +++ b/mm/kasan/generic.c @@ -533,10 +533,13 @@ size_t kasan_metadata_size(struct kmem_cache *cache, bool in_object) } /* + * Records an auxiliary stack trace for the object at @addr and, if @rcu_free + * is set, marks the object as pending an RCU-delayed free. + * * 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 +555,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_mark_rcu_free_pending(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 45e077c7526c..cbc51440b3d9 100644 --- a/mm/slub.c +++ b/mm/slub.c @@ -2722,7 +2722,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 @@ -2738,7 +2738,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_mark_rcu_free_pending(x); delayed_free->object = x; call_rcu(&delayed_free->head, slab_free_after_rcu_debug); @@ -6298,6 +6298,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_mark_rcu_free_pending(obj); rcu_sheaf = pcs->rcu_free; @@ -6338,6 +6344,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; @@ -6971,7 +6978,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.307.g5fe7bd680c-goog ^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH v2 1/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf(). 2026-10-27 22:25 ` [PATCH v2 1/2] " Kuniyuki Iwashima @ 2026-10-28 16:22 ` Alexander Potapenko 2026-10-28 18:13 ` Andrey Ryabinin 1 sibling, 0 replies; 9+ messages in thread From: Alexander Potapenko @ 2026-10-28 16:22 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 > > Link: https://lore.kernel.org/all/6ac959ee.70ed5d86.2ac85.007d.GAE@google.com/ #[0] > Fixes: ec66e0d59952 ("slab: add sheaf support for batching kfree_rcu() operations") > Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com> > Acked-by: Vlastimil Babka (SUSE) <vbabka@kernel.org> > Tested-by: Alexander Potapenko <glider@google.com> Reviewed-by: Alexander Potapenko <glider@google.com> ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 1/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf(). 2026-10-27 22:25 ` [PATCH v2 1/2] " Kuniyuki Iwashima 2026-10-28 16:22 ` Alexander Potapenko @ 2026-10-28 18:13 ` Andrey Ryabinin 1 sibling, 0 replies; 9+ messages in thread From: Andrey Ryabinin @ 2026-10-28 18:13 UTC (permalink / raw) To: Kuniyuki Iwashima, 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 <kuniyu@google.com> writes: > 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] > Fixes: ec66e0d59952 ("slab: add sheaf support for batching kfree_rcu() operations") > Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com> > Acked-by: Vlastimil Babka (SUSE) <vbabka@kernel.org> > Tested-by: Alexander Potapenko <glider@google.com> > --- Reviewed-by: Andrey Ryabinin <ryabinin.a.a@gmail.com> ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests involving kfree_rcu_sheaf(). 2026-10-27 22:25 [PATCH v2 0/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf() Kuniyuki Iwashima 2026-10-27 22:25 ` [PATCH v2 1/2] " Kuniyuki Iwashima @ 2026-10-27 22:25 ` Kuniyuki Iwashima 2026-10-28 16:23 ` Alexander Potapenko ` (2 more replies) 1 sibling, 3 replies; 9+ messages in thread From: Kuniyuki Iwashima @ 2026-10-27 22:25 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> --- v2: Move kasan_byte_accessible() check and kvfree_rcu_barrier_on_cache() after switch --- mm/kasan/kasan_test_c.c | 134 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/mm/kasan/kasan_test_c.c b/mm/kasan/kasan_test_c.c index b9e167ed5be3..83c418c0ef87 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,134 @@ 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)); + 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; + } + + /* + * 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. + */ + 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 +2368,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.307.g5fe7bd680c-goog ^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests involving kfree_rcu_sheaf(). 2026-10-27 22:25 ` [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests " Kuniyuki Iwashima @ 2026-10-28 16:23 ` Alexander Potapenko 2026-10-28 19:10 ` Andrey Ryabinin 2026-10-28 22:26 ` sashiko-bot 2 siblings, 0 replies; 9+ messages in thread From: Alexander Potapenko @ 2026-10-28 16:23 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 > 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> Reviewed-by: Alexander Potapenko <glider@google.com> ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests involving kfree_rcu_sheaf(). 2026-10-27 22:25 ` [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests " Kuniyuki Iwashima 2026-10-28 16:23 ` Alexander Potapenko @ 2026-10-28 19:10 ` Andrey Ryabinin 2026-10-28 19:24 ` Kuniyuki Iwashima 2026-10-28 22:26 ` sashiko-bot 2 siblings, 1 reply; 9+ messages in thread From: Andrey Ryabinin @ 2026-10-28 19:10 UTC (permalink / raw) To: Kuniyuki Iwashima, 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 <kuniyu@google.com> writes: > > +#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)); > + break; > + case 3: > + migrate_disable(); Shouldn't this be before allocation? If task migrates to another NUMA, the test would fail because kfree_rcu_sheaf() declines objects from foreign NUMA: mm/slab_common.c:kfree_rcu_sheaf() { ... if (likely(!IS_ENABLED(CONFIG_NUMA) || slab_nid(slab) == numa_mem_id())) return __kfree_rcu_sheaf(s, obj, SLAB_FREE_DEFAULT); return false; } Also the tests fail with CONFIG_PREEMPT_RT=y (causing use after free reports and all sorts of warnings) for similar reason, in PREEMPT_RT=y case kfree_rcu_sheaf() skipped: void kvfree_call_rcu(struct kvfree_rcu_head *head, void *ptr) { ... if (!IS_ENABLED(CONFIG_PREEMPT_RT) && kfree_rcu_sheaf(ptr)) [ 1.552763] ok 37 kmem_cache_double_free [ 1.553617] # kmem_cache_double_free_rcu_1: EXPECTATION FAILED at mm/kasan/kasan_test_c.c:1109 KASAN failure expected in "kfree_rcu(p, rcu)", but none occurred [ 1.606445] ================================================================== [ 1.606446] BUG: KASAN: slab-use-after-free in __kmem_cache_double_free_rcu+0x111a/0x1280 [ 1.606452] Read of size 4 at addr ffff88800da67b10 by task kunit_try_catch/203 .... ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests involving kfree_rcu_sheaf(). 2026-10-28 19:10 ` Andrey Ryabinin @ 2026-10-28 19:24 ` Kuniyuki Iwashima 0 siblings, 0 replies; 9+ messages in thread From: Kuniyuki Iwashima @ 2026-10-28 19:24 UTC (permalink / raw) To: Andrey Ryabinin Cc: Andrew Morton, Vlastimil Babka, Harry Yoo, Alexander Potapenko, Andrey Konovalov, Dmitry Vyukov, Vincenzo Frascino, Hao Li, Christoph Lameter, David Rientjes, Roman Gushchin, kasan-dev, linux-mm On Wed, 28 Oct 2026 12:10:23 -0700, Andrey Ryabinin wrote: > Kuniyuki Iwashima <kuniyu@google.com> writes: > > > > +#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)); > > + break; > > + case 3: > > + migrate_disable(); > > Shouldn't this be before allocation? If task migrates to another NUMA, > the test would fail > because kfree_rcu_sheaf() declines objects from foreign NUMA: Ah indeed, I'll squash this. ---8<--- @@ -1096,8 +1097,11 @@ static void __kmem_cache_double_free_rcu(struct kunit *test, int mode) goto out; } + migrate_disable(); + p = kmem_cache_alloc(cache, GFP_KERNEL); if (!p) { + migrate_enable(); kunit_err(test, "Allocation failed: %s\n", __func__); goto out; } @@ -1107,14 +1111,15 @@ static void __kmem_cache_double_free_rcu(struct kunit *test, int mode) kmem_cache_free(cache, p); /* p is already in quarantine. */ KUNIT_EXPECT_KASAN_FAIL(test, kfree_rcu(p, rcu)); + migrate_enable(); break; case 2: /* The actual free order is inverse. */ kfree_rcu(p, rcu); KUNIT_EXPECT_KASAN_FAIL(test, kmem_cache_free(cache, p)); + migrate_enable(); break; case 3: - migrate_disable(); kfree_rcu(p, rcu); for (i = 0; i < cache->sheaf_capacity - 1; i++) { ---8<--- > > mm/slab_common.c:kfree_rcu_sheaf() { > ... > if (likely(!IS_ENABLED(CONFIG_NUMA) || slab_nid(slab) == numa_mem_id())) > return __kfree_rcu_sheaf(s, obj, SLAB_FREE_DEFAULT); > > return false; > } > > > Also the tests fail with CONFIG_PREEMPT_RT=y (causing use after free > reports and all sorts of warnings) > for similar reason, in PREEMPT_RT=y case kfree_rcu_sheaf() skipped: Exactly, I'll add this to v3. ---8<--- diff --git a/mm/kasan/kasan_test_c.c b/mm/kasan/kasan_test_c.c index 83c418c0ef87..3c9ffee8001b 100644 --- a/mm/kasan/kasan_test_c.c +++ b/mm/kasan/kasan_test_c.c @@ -1079,6 +1079,7 @@ static void __kmem_cache_double_free_rcu(struct kunit *test, int mode) int i, n = 0; KASAN_TEST_NEEDS_CONFIG_ON(test, CONFIG_KASAN_GENERIC); + KASAN_TEST_NEEDS_CONFIG_OFF(test, CONFIG_PREEMPT_RT); cache = kmem_cache_create("test_cache", sizeof(*p), 0, SLAB_NO_MERGE, NULL); ---8<--- Thank you ! ^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests involving kfree_rcu_sheaf(). 2026-10-27 22:25 ` [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests " Kuniyuki Iwashima 2026-10-28 16:23 ` Alexander Potapenko 2026-10-28 19:10 ` Andrey Ryabinin @ 2026-10-28 22:26 ` sashiko-bot 2 siblings, 0 replies; 9+ messages in thread From: sashiko-bot @ 2026-10-28 22:26 UTC (permalink / raw) To: Kuniyuki Iwashima, 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 Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Medium] mm/kasan: missing Kconfig guards in __kmem_cache_double_free_rcu() - [Medium] mm/kasan/kunit: CPU and NUMA migration race in __kmem_cache_double_free_rcu() -- commit 2267f8620b312bfdd7e76e11214e3ba726322685 Author: Kuniyuki Iwashima <kuniyu@google.com> mm/kasan/kunit: Add double-free tests involving kfree_rcu_sheaf(). This commit adds KUnit test cases to verify that KASAN immediately detects double-free combinations involving kfree_rcu_sheaf() and prevents a reallocated victim object from being poisoned when the RCU sheaf is flushed. > diff --git a/mm/kasan/kasan_test_c.c b/mm/kasan/kasan_test_c.c > index b9e167ed5be32..83c418c0ef878 100644 > --- a/mm/kasan/kasan_test_c.c > +++ b/mm/kasan/kasan_test_c.c [ ... ] > @@ -1070,6 +1071,134 @@ 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"); > + } [Severity: Medium] Should __kmem_cache_double_free_rcu() also check that CONFIG_KVFREE_RCU_BATCHED is enabled and CONFIG_PREEMPT_RT is disabled? In calculate_sheaf_capacity(), cache->sheaf_capacity is set to a non-zero value whenever CONFIG_SLUB_TINY is disabled and debug flags are not set on the cache, regardless of CONFIG_PREEMPT_RT or CONFIG_KVFREE_RCU_BATCHED. However, kvfree_call_rcu() in mm/slab_common.c only calls kfree_rcu_sheaf() when CONFIG_KVFREE_RCU_BATCHED is enabled and CONFIG_PREEMPT_RT is disabled: mm/slab_common.c: #ifndef CONFIG_KVFREE_RCU_BATCHED void kvfree_call_rcu(struct kvfree_rcu_head *head, void *ptr) { if (head) { kasan_record_aux_stack(ptr); call_rcu(&head->head, kvfree_rcu_cb); return; } ... #endif and in the batched implementation: mm/slab_common.c:kvfree_call_rcu() { ... if (!IS_ENABLED(CONFIG_PREEMPT_RT) && kfree_rcu_sheaf(ptr)) return; ... } When CONFIG_PREEMPT_RT=y or CONFIG_KVFREE_RCU_BATCHED=n (for example with CONFIG_RCU_STRICT_GRACE_PERIOD=y or CONFIG_TINY_RCU=y), cache->sheaf_capacity is non-zero so the test does not skip, but kfree_rcu(p, rcu) bypasses __kfree_rcu_sheaf() where kasan_slab_pre_free() and kasan_mark_rcu_free_pending() are called. Wouldn't this cause KUNIT_EXPECT_KASAN_FAIL() to fail across all three modes, free and poison the reallocated live victim object during kvfree_rcu_barrier_on_cache(cache) in modes 1 and 2, and queue the same p->rcu head twice in mode 3? Adding KASAN_TEST_NEEDS_CONFIG_ON(test, CONFIG_KVFREE_RCU_BATCHED) and KASAN_TEST_NEEDS_CONFIG_OFF(test, CONFIG_PREEMPT_RT) would skip the test on configurations where kfree_rcu_sheaf() is not used. > + > + 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)); > + 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; > + } > + > + /* > + * 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. > + */ > + 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); [Severity: Medium] Can task migration across CPUs or NUMA nodes cause spurious failures in __kmem_cache_double_free_rcu()? Because kunit_try_catch_run() executes the test in an unbound kthread, the task can migrate whenever migration is not disabled: 1. p = kmem_cache_alloc(cache, GFP_KERNEL) runs before migrate_disable() in mode 3, and modes 1 and 2 do not call migrate_disable() at all. In kfree_rcu_sheaf() in mm/slab_common.c: s = slab->slab_cache; if (likely(!IS_ENABLED(CONFIG_NUMA) || slab_nid(slab) == numa_mem_id())) return __kfree_rcu_sheaf(s, obj, SLAB_FREE_DEFAULT); return false; If the task migrates to a CPU on a different NUMA node between allocating p and calling kfree_rcu(p, rcu), slab_nid(slab) == numa_mem_id() is false and kfree_rcu_sheaf() returns false, falling back to the non-sheaf kvfree_call_rcu() path. This bypasses __kfree_rcu_sheaf(), causing KUNIT_EXPECT_KASAN_FAIL() to fail and queueing the double-freed object into krcp. 2. Following the switch block, rcu_barrier(), kvfree_rcu_barrier_on_cache(cache), and kasan_quarantine_remove_cache(cache) can sleep with migration enabled, and the victim reallocation loop also runs without migrate_disable(). If the task migrates to another NUMA node before or during the loop, refill_objects() only refills from local_node while __refill_objects_any() skips p's node because n->nr_partial <= s->min_partial, allocating a new slab instead of p. Even on a single NUMA node, if the task migrates to another CPU mid-loop after refill_sheaf() moves p into the first CPU's pcs->main->objects[0], p remains in the first CPU's per-CPU sheaf and KUNIT_EXPECT_PTR_EQ(test, victim, p) fails. Would it make sense to wrap the initial allocation and free sequence in migrate_disable() / migrate_enable() for all modes, and either keep the task on the same CPU/node or use kmem_cache_alloc_node(..., folio_nid(virt_to_folio(p))) under migrate_disable() when reallocating victim? -- Sashiko AI review · https://sashiko.dev/#/patchset/20261027222557.1072105-1-kuniyu@google.com?part=2 ^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-10-28 22:26 UTC | newest] Thread overview: 9+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-10-27 22:25 [PATCH v2 0/2] mm/kasan: Detect double-free involving kfree_rcu_sheaf() Kuniyuki Iwashima 2026-10-27 22:25 ` [PATCH v2 1/2] " Kuniyuki Iwashima 2026-10-28 16:22 ` Alexander Potapenko 2026-10-28 18:13 ` Andrey Ryabinin 2026-10-27 22:25 ` [PATCH v2 2/2] mm/kasan/kunit: Add double-free tests " Kuniyuki Iwashima 2026-10-28 16:23 ` Alexander Potapenko 2026-10-28 19:10 ` Andrey Ryabinin 2026-10-28 19:24 ` Kuniyuki Iwashima 2026-10-28 22:26 ` sashiko-bot
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox