该特性在条件允许的情况下可以实现将属性相近的 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_nomerge
1 在启动时将该特性临时关闭。
先借助简单的 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 属性
特殊标志组合
-
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 的标志。
-
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 }
参考资料
-
uninformativ.de, Linux: Slab merging
-
Chris Siebenmann, How
/proc/slabinfo
is not quite telling you what it looks like
-
由于该特性原本只属于 SLUB,因此命名为
slub_nomerge
。随着该特性被拓展到 SLAB,因此添加了别名slab_nomerge
。两者实际是由同一段逻辑来处理的。 ↩︎