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 ... # 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: ... 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 Reviewed-by: Alexander Potapenko --- v3: Move migrate_disable() before p = kmem_cache_alloc(), and add KASAN_TEST_NEEDS_CONFIG_OFF() for PREEMPT_RT v2: Move kasan_byte_accessible() check and kvfree_rcu_barrier_on_cache() after switch --- mm/kasan/kasan_test_c.c | 139 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/mm/kasan/kasan_test_c.c b/mm/kasan/kasan_test_c.c index b9e167ed5be3..3c9ffee8001b 100644 --- a/mm/kasan/kasan_test_c.c +++ b/mm/kasan/kasan_test_c.c @@ -30,6 +30,7 @@ #include #include "kasan.h" +#include "../slab.h" #define OOB_TAG_OFF (IS_ENABLED(CONFIG_KASAN_GENERIC) ? 0 : KASAN_GRANULE_SIZE) @@ -1070,6 +1071,139 @@ 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); + KASAN_TEST_NEEDS_CONFIG_OFF(test, CONFIG_PREEMPT_RT); + + 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; + } + + migrate_disable(); + + p = kmem_cache_alloc(cache, GFP_KERNEL); + if (!p) { + migrate_enable(); + 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)); + 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: + 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 +2373,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.313.g2479322e68-goog