Slab Merging 特性

该特性在条件允许的情况下可以实现将属性相近的 slab caches(即 struct kmem_cache)合并,以提高内存利用效率,减少内存碎片的产生。该特性最初独属于 SLUB 分配器(见补丁:81819f0fc828 (“SLUB core”) 的 G 小节),随后被拓展到了 SLAB 分配器:423c929cbbec (“mm/slab_common: commonize slab merge logic”)

然而从系统安全的角度,该特性带来一个问题:若由某子系统或模块创建的专用 slab cache 与通用的 slab caches(即 kmalloc() 使用的那些)合并,且该子系统或模块存在内存漏洞,那么攻击者就有可能通过操作通用 slab caches 来触发或利用该漏洞。

日常工作中,可以通过以下手段观察哪些 slab caches 被合并了:

  • 用户态工具 slabinfo -a,源码位于内核源码树下的 tools/mm/slabinfo.c
  • 观察 ls -l /sys/kernel/slab/ 中的软连接

此外,可以通过内核启动参数 sl{a,u}b_nomerge1 在启动时将该特性临时关闭。

先借助简单的 ko 体会一下

该用例来自这个博客 ,看完试过感觉不错,因此记录一下。

 1#include <linux/init.h>
 2#include <linux/module.h>
 3#include <linux/slab.h>
 4
 5static struct kmem_cache *my_cache_a, *my_cache_b;
 6
 7struct my_data_structure {
 8    uint64_t some_int;
 9    char name[256];
10};
11
12static int slab_user_init(void)
13{
14    my_cache_a = kmem_cache_create("MY_FOO", sizeof (struct my_data_structure),
15                                   0, 0, NULL);
16    my_cache_b = kmem_cache_create("MY_BAR", sizeof (struct my_data_structure),
17                                   0, 0, NULL);
18    return 0;
19}
20
21static void slab_user_exit(void)
22{
23    kmem_cache_destroy(my_cache_a);
24    kmem_cache_destroy(my_cache_b);
25}
26
27module_init(slab_user_init);
28module_exit(slab_user_exit);
29
30MODULE_LICENSE("GPL");
31MODULE_AUTHOR("nobody-cares");

将上述代码编译成 ko:

1obj-m := slab_user.o
2
3KERNEL_DIR=...
4
5all:
6	make -C ${KERNEL_DIR} M=$(PWD) modules
7
8clean:
9	make -C ${KERNEL_DIR} M=$(PWD) clean

起一个虚拟机,插入 ko 后观察这两个 slab caches,一般情况下它们会被合并,添加了 sl{a,u}b_nomerge 启动参数后则不会:

1grep MY /proc/slabinfo
2ls -ld /sys/kernel/slab/MY_*

与合并相关的 slab cache 属性

特殊标志组合

  1. SLAB_NEVER_MERGE

    1/* mm/slab_common.c */
    2#define SLAB_NEVER_MERGE (SLAB_RED_ZONE | SLAB_POISON | SLAB_STORE_USER | \
    3                SLAB_TRACE | SLAB_TYPESAFE_BY_RCU | SLAB_NOLEAKTRACE | \
    4                SLAB_FAILSLAB | kasan_never_merge())
    

    若某 slab cache 带有以上任意一个 SLAB_* 标志,则不能与其他 slab caches 实施合并。创建 slab cache 时与之相关的代码流程:

     1kmem_cache_create .. kmem_cache_create_usercopy {
     2	__kmem_cache_alias(name, size, align, flags, ctor) {
     3		s = find_mergeable(size, align, flags, name, ctor) {
     4			if (slab_nomerge) return NULL;
     5			if (flags & SLAB_NEVER_MERGE) return NULL;		// 【1】
     6			list_for_each_entry_reverse(s, &slab_caches, list) {
     7				if (slab_unmergeable(s) {			// 【2】
     8					if (slab_nomerge || (s->flags & SLAB_NEVER_MERGE)) return 1;
     9				}) continue;
    10				return s;
    11			}
    12			return NULL;
    13		}
    14	}
    15}
    

    其中【1】处检查正被创建的 slab cache 的标志,【2】处(逐个)检查已有 slab caches 的标志。

  2. SLAB_MERGE_SAME

    1/* mm/slab_common.c */
    2#define SLAB_MERGE_SAME (SLAB_RECLAIM_ACCOUNT | SLAB_CACHE_DMA | \
    3                         SLAB_CACHE_DMA32 | SLAB_ACCOUNT)
    

    若 slab caches 拥有上述标志的相同组合,则可以合并,否则不可合并。代码流程如下:

    1__kmem_cache_alias(..., flags, ...) {
    2	s = find_mergeable(..., flags, ...) {
    3		list_for_each_entry_reverse(s, &slab_caches, list) {
    4			if ((flags & SLAB_MERGE_SAME) != (s->flags & SLAB_MERGE_SAME)) continue;
    5			return s;
    6		}
    7		return NULL;
    8	}
    9}
    

注意坑爹的内部 flags

在新增 SLAB_* 标志时,除了要避开 include/linux/slab.h 中已有的标志外,还要注意属于各分配器的内部 flags,比如 SLUB 的:

1// mm/slub.c
2/* Internal SLUB flags */
3/* Poison object */
4#define __OBJECT_POISON		((slab_flags_t __force)0x80000000U)
5/* Use cmpxchg_double */
6#define __CMPXCHG_DOUBLE	((slab_flags_t __force)0x40000000U)

以及 SLAB 的:

1// mm/slab.c
2#define CFLGS_OBJFREELIST_SLAB	((slab_flags_t __force)0x40000000U)
3#define CFLGS_OFF_SLAB		((slab_flags_t __force)0x80000000U)

若仅参考 include/linux/slab.h 来设立新标志,而新标志的值与这些内部 flags 重合,则可能导致预料之外的后果。比如:设立了一个新标志并加入不可合并的集合,导致所有 slab caches 都不合并了……

usercopy

该特性由 8eb8284b4129 (“usercopy: Prepare for usercopy whitelisting”) 于 v4.16 引入,最近的 v6.2-rc1 在代码上有所调整 。该特性旨在为每个由 Slab 分配的内存对象划定可用于与用户态访存的片段,并在跨内核态↔用户态拷贝时进行检查。该片段通过两个变量来定义:偏移 useroffset + 长度 usersize,检查的逻辑如下:

 1copy_from_user(to, n, false), copy_to_user(from, n, true)
 2  check_copy_size(addr:{to,from}, bytes:n, is_source:{false,true})
 3    check_object_size(ptr:addr, n:bytes, to_user:is_source)
 4      __check_object_size(ptr, n, to_user)
 5        check_heap_object(ptr, n, to_user) {
 6            folio = virt_to_folio(ptr);
 7            __check_heap_object(ptr, n, slab:folio_slab(folio), to_user) {
 8                s = slab->slab_cache;
 9                offset = (ptr - slab_address(slab)) % s->size;
10                if (offset >= s->useroffset &&                          // 【1】
11                    offset - s->useroffset <= s->usersize &&            // 【2】
12                    n <= s->useroffset - offset + s->usersize) return;  // 【3】
13                usercopy_abort("SLUB object", s->name, to_user, offset, n);
14            }
15        }

其中【1】和【2】保证写起始地址在内存对象内的偏移在 [useroffset, useroffset + usersize] 范围内,【3】保证写的长度不会超出 useroffset + usersize 的边界。

说回到 Slab Merging,在检查已有的 slab caches 时, usersize 也是一个被检查项:设置了 usersize 的 slab caches 不可被合并。

 1kmem_cache_create_usercopy(..., usersize, ...) {
 2    if (!usersize)
 3        s = __kmem_cache_alias(...)
 4            s = find_mergeable(...) {
 5                list_for_each_entry_reverse(s, &slab_caches, list) {
 6                    if (slab_unmergeable(s) {
 7                        if (s->usersize) return 1;
 8                    }) continue;
 9                }
10            }
11}

在 v6.2-rc1 之前,由于在启动阶段为 kmalloc 创建 slab caches 时有设置 usersize

1kmem_cache_init()
2  create_kmalloc_caches()
3    new_kmalloc_cache()
4      create_kmalloc_cache(..., usersize:kmalloc_info[idx].size)
5        create_boot_cache(..., usersize)
6          s->usersize = usersize

因此在系统的后续运行中 kmalloc 的 slab caches 是不会与他者合并的。对此,社区在 v6.2-rc1 上进行了调整 ,对 struct kmem_cache 中的 user{offset,size} 成员以及相关代码逻辑加入了 CONFIG_HARDENED_USERCOPY 宏控隔离的范围。

构造函数 (ctor)

SLAB 分配器可以设置构造函数:每分配一个新 slab 页,就对该页上所有的空闲内存对象逐个调用构造函数进行初始化。值得重申的是:该初始化行为是在分配 slab 页时完成,而不是在分配 slab 对象时。

 1new_slab()
 2  allocate_slab()
 3    shuffle_freelist() {
 4        cur = setup_object(s, object:cur) {
 5            s->ctor(object);
 6            return object;
 7        }
 8        for (idx = 1; idx < slab->objects; idx++) {
 9            next = setup_object(s, next);
10        }
11    }

对设置了构造函数的 slab caches,也是会被拒绝合并的:

 1kmem_cache_create_usercopy(..., ctor)
 2    s = __kmem_cache_alias(..., ctor)
 3        s = find_mergeable(..., ctor) {
 4            if (ctor) return NULL;
 5            list_for_each_entry_reverse(s, &slab_caches, list) {
 6                if (slab_unmergeable(s) {
 7                    if (s->ctor) return 1;
 8                }) continue;
 9            }
10        }

参考资料


  1. 由于该特性原本只属于 SLUB,因此命名为 slub_nomerge。随着该特性被拓展到 SLAB,因此添加了别名 slab_nomerge。两者实际是由同一段逻辑来处理的。 ↩︎


ARM64 虚拟地址长度判别
SELinux:系统启动时通过用户态配置动态关闭的流程