Dedicated caches are available For fixed size allocations via
kmem_cache_alloc(), but for dynamically sized allocations there is only
the global kmalloc API's set of buckets available. This means it isn't
possible to separate specific sets of dynamically sized allocations into
a separate collection of caches.

This leads to a use-after-free exploitation weakness in the Linux
kernel since many heap memory spraying/grooming attacks depend on using
userspace-controllable dynamically sized allocations to collide with
fixed size allocations that end up in same cache.

While CONFIG_RANDOM_KMALLOC_CACHES provides a probabilistic defense
against these kinds of "type confusion" attacks, including for fixed
same-size heap objects, we can create a complementary deterministic
defense for dynamically sized allocations.

In order to isolate user-controllable sized allocations from system
allocations, introduce kmem_buckets_create(), which behaves like
kmem_cache_create(). (The next patch will introduce kmem_buckets_alloc(),
which behaves like kmem_cache_alloc().)

Allows for confining allocations to a dedicated set of sized caches
(which have the same layout as the kmalloc caches).

This can also be used in the future once codetag allocation annotations
exist to implement per-caller allocation cache isolation[1] even for
dynamic allocations.

Link: https://lore.kernel.org/lkml/202402211449.401382D2AF@keescook [1]
Signed-off-by: Kees Cook <[email protected]>
---
Cc: Vlastimil Babka <[email protected]>
Cc: Christoph Lameter <[email protected]>
Cc: Pekka Enberg <[email protected]>
Cc: David Rientjes <[email protected]>
Cc: Joonsoo Kim <[email protected]>
Cc: Andrew Morton <[email protected]>
Cc: Roman Gushchin <[email protected]>
Cc: Hyeonggon Yoo <[email protected]>
Cc: [email protected]
---
 include/linux/slab.h |  5 +++
 mm/slab_common.c     | 72 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 77 insertions(+)

diff --git a/include/linux/slab.h b/include/linux/slab.h
index f26ac9a6ef9f..058d0e3cd181 100644
--- a/include/linux/slab.h
+++ b/include/linux/slab.h
@@ -493,6 +493,11 @@ void *kmem_cache_alloc_lru(struct kmem_cache *s, struct 
list_lru *lru,
                           gfp_t gfpflags) __assume_slab_alignment __malloc;
 void kmem_cache_free(struct kmem_cache *s, void *objp);
 
+kmem_buckets *kmem_buckets_create(const char *name, unsigned int align,
+                                 slab_flags_t flags,
+                                 unsigned int useroffset, unsigned int 
usersize,
+                                 void (*ctor)(void *));
+
 /*
  * Bulk allocation and freeing operations. These are accelerated in an
  * allocator specific way to avoid taking locks repeatedly or building
diff --git a/mm/slab_common.c b/mm/slab_common.c
index 1d0f25b6ae91..03ba9aac96b6 100644
--- a/mm/slab_common.c
+++ b/mm/slab_common.c
@@ -392,6 +392,74 @@ kmem_cache_create(const char *name, unsigned int size, 
unsigned int align,
 }
 EXPORT_SYMBOL(kmem_cache_create);
 
+static struct kmem_cache *kmem_buckets_cache __ro_after_init;
+
+kmem_buckets *kmem_buckets_create(const char *name, unsigned int align,
+                                 slab_flags_t flags,
+                                 unsigned int useroffset,
+                                 unsigned int usersize,
+                                 void (*ctor)(void *))
+{
+       kmem_buckets *b;
+       int idx;
+
+       if (WARN_ON(!kmem_buckets_cache))
+               return NULL;
+
+       b = kmem_cache_alloc(kmem_buckets_cache, GFP_KERNEL|__GFP_ZERO);
+       if (WARN_ON(!b))
+               return NULL;
+
+       flags |= SLAB_NO_MERGE;
+
+       for (idx = 0; idx < ARRAY_SIZE(kmalloc_caches[KMALLOC_NORMAL]); idx++) {
+               char *short_size, *cache_name;
+               unsigned int cache_useroffset, cache_usersize;
+               unsigned int size;
+
+               if (!kmalloc_caches[KMALLOC_NORMAL][idx])
+                       continue;
+
+               size = kmalloc_caches[KMALLOC_NORMAL][idx]->object_size;
+               if (!size)
+                       continue;
+
+               short_size = strchr(kmalloc_caches[KMALLOC_NORMAL][idx]->name, 
'-');
+               if (WARN_ON(!short_size))
+                       goto fail;
+
+               cache_name = kasprintf(GFP_KERNEL, "%s-%s", name, short_size + 
1);
+               if (WARN_ON(!cache_name))
+                       goto fail;
+
+               if (useroffset >= size) {
+                       cache_useroffset = 0;
+                       cache_usersize = 0;
+               } else {
+                       cache_useroffset = useroffset;
+                       cache_usersize = min(size - cache_useroffset, usersize);
+               }
+               (*b)[idx] = kmem_cache_create_usercopy(cache_name, size,
+                                       align, flags, cache_useroffset,
+                                       cache_usersize, ctor);
+               kfree(cache_name);
+               if (WARN_ON(!(*b)[idx]))
+                       goto fail;
+       }
+
+       return b;
+
+fail:
+       for (idx = 0; idx < ARRAY_SIZE(kmalloc_caches[KMALLOC_NORMAL]); idx++) {
+               if ((*b)[idx])
+                       kmem_cache_destroy((*b)[idx]);
+       }
+       kfree(b);
+
+       return NULL;
+}
+EXPORT_SYMBOL(kmem_buckets_create);
+
 #ifdef SLAB_SUPPORTS_SYSFS
 /*
  * For a given kmem_cache, kmem_cache_destroy() should only be called
@@ -933,6 +1001,10 @@ void __init create_kmalloc_caches(slab_flags_t flags)
 
        /* Kmalloc array is now usable */
        slab_state = UP;
+
+       kmem_buckets_cache = kmem_cache_create("kmalloc_buckets",
+                                              sizeof(kmem_buckets),
+                                              0, 0, NULL);
 }
 
 /**
-- 
2.34.1


Reply via email to