一、功能介绍
Linux 5.13引入一个新的内存错误检测工具:KFENCE(Kernel Electric-Fence,内核电子栅栏)。KFENCE是一个低开销的、基于采样的内存错误检测工具。KFENCE检测越界访问、释放后使用和非法释放(包括重复释放和释放的起始地址不是分配的起始地址)这3种错误。
KFENCE和KASAN是互补的。KASAN可以检测KFENCE支持的所有缺陷种类。KASAN依靠编译器插桩,对每个内存访问都检查地址的合法性,更精确,但是导致内核的性能下降,所以KASAN只适合测试环境。KFENCE使用采样的方法,牺牲了精度,但是性能开销几乎为零,它被设计为在产品内核中使用,发现在测试环境中测试用例没有执行的代码路径中的缺陷。
目前只有x86_64和ARM64两种架构支持KFENCE。
简要对比kasan,一目了然:
- 败在属于概率检测,不是所有的异常访问都能抓取。
- 胜在对系统的开销很小,可以直接在生产环境中使用。
二、使用说明
1. 开启kfence
- 内核功能宏
- HAVE_ARCH_KFENCE
- KFENCE:kfence总的开关
- 依赖于HAVE_ARCH_KFENCE
- KFENCE_STATIC_KEYS
- KFENCE_SAMPLE_INTERVAL:默认采样间隔,是指多久之后,guard page可以回收吗?
- KFENCE_NUM_OBJECTS:kfence obj的个数,kfence obj主要用于guard page
- KFENCE_STRESS_TEST_FAULTS
- 起机内核参数
- kfence.sample_interval:当值为0时,禁用kfence;当值大于0时,启动kfence
- 检测结果
2. 技术原理
KFENCE使用一个固定长度的内存池,如图2.1所示。配置宏CONFIG_KFENCE_NUM_OBJECTS指定对象的数量。每个对象需要2页,一页用来存放对象自身,另一页用作警戒页(guard page)。对象页和警戒页交替出现,每个对象页被两个警戒页包围。内存池的长度是“(对象数量 + 1)× 2 ×页长度”。第1页不是必需的,增加这一页是因为分配偶数个物理页可以简化把对象页地址转换为对象索引的计算。
在采样间隔到期以后,下一次从SLAB分配器(或者SLUB分配器)分配内存的时候,从KFENCE内存池分配一个对象(只支持分配长度不超过一页),如果内存池用完了,那么返回空指针,由SLAB分配器分配。
周期采样:
- 因为kfence object个数有限,当前实现采用定时采样的方式
- 这里所谓的采样就是每间隔kfence.sample_interval,允许进行分配一个kfence object进行检测
redzone: - 若实际申请大小小于PAGE_SIZE,那意味着内存页实际是有部分是未分配的。通过将内存页未分配部分填充为redzone,来实现单个页表里的改写
- 在申请内存时,根据meta->addr和meta->size,将未分配的部分填充为KFENCE_CANARY_PATTERN
- 在释放内存时,检测未分配部分的内容是否为KFENCE_CANARY_PATTERN,不是则报错
如果访问对象的时候越界访问到警戒页,那么触发页错误异常。在页错误异常处理程序里面,KFENCE拦截页错误异常,报告一个越界访问,如果开启了“panic_on_warn”(通过内核启动参数“panic_on_warn”开启,或者执行命令“echo 1 > /proc/sys/kernel/panic_on_warn”开启),那么重启设备,否则把正在访问的警戒页设置为可以访问,让出错的代码继续执行。
为了检测出在对象页里面的越界写,KFENCE使用红色区域。对象页有2种布局,如下。
(1)如图2.2所示,对象在对象页的前半部分,红色区域在对象页的后半部分。这种布局有利于检测左越界,如果向左越界访问左边的警戒页,就会触发页错误异常。
图2.2对象在对象页的前半部分
(2)如图2.3所示,对象在对象页的后半部分,红色区域在对象页的前半部分。这种布局有利于检测右越界,如果向右越界访问右边的警戒页,就会触发页错误异常。
图2.3 对象在对象页的后半部分
KFENCE在每次分配对象的时候,随机选择一种布局,并且用特定的字符填充红色区域。释放对象的时候,检查红色区域里面的字符是否变化,如果变化,那么报告错误。
释放一个KFENCE对象的时候,KFENCE把对象页设置为不可访问,并且把对象标记为空闲。继续访问这个对象就会触发一个页错误异常,KFENCE报告一个“释放后使用”错误。为了增加检测出“释放后使用”的机会,KFENCE把空闲对象插入空闲链表的尾部,让最早释放的空闲对象先被分配出去。
数据结构
- struct kfence_metadata kfence_metadata[CONFIG_KFENCE_NUM_OBJECTS]:kfence object的维护数据结构
- 采用下标的方式实现metadata与kfence内存页表间的映射
- 每个metadata指向2个连续的内存页
- static struct list_head kfence_freelist = LIST_HEAD_INIT(kfence_freelist):空闲的metadata节点链表
- static struct delayed_work kfence_timer:
三、实现分析
kfence导入的patch集:
- 框架:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.13&id=0ce20dd840897b12ae70869c69f1ba34d6d16965
- x86平台:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.13&id=1dc0da6e9ec0f8d735756374697912cd50f402cf
- arm64平台:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.13&id=840b239863449f27bf7522deb81e6746fbfbfeaf
- slub对接:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.13&id=b89fb5ef0ce611b5db8eb9d3a5a7fcaab2cbe9e4
- 异常堆栈打印优化:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.13&id=d438fabce7860df3cb9337776be6f90b59ced8ed
- 测试代码:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.13&id=bc8fbc5f305aecf63423da91e5faf4c0ce40bf38
- 文档说明:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.13&id=10efe55f883f2396a0024891ad1d7d5d040364b3
起机初始化:
- void __init kfence_alloc_pool(void):初始化,申请kfence obj pool
- __kfence_pool = memblock_alloc(KFENCE_POOL_SIZE, PAGE_SIZE);
- void __init kfence_init(void):初始化,设置sampling timer等,必须在kfence_alloc_pool()调用
- arch_kfence_init_pool():
申请内存:
- kfence_alloc():
- __kfence_alloc():
- 申请的大小超过PAGE_SIZE,直接返回NULL,不保护了?
- kfence_guarded_alloc():
- __kfence_alloc():
内存页属性:
- static bool kfence_protect(unsigned long addr):设置为不可访问
- kfence_protect_page(addr, bool)
- static bool kfence_unprotect(unsigned long addr):设置为可以访问
- kfence_protect_page(addr, bool)
设置redzone:
- static inline bool set_canary_byte(u8 *addr)
- static inline bool check_canary_byte(u8 *addr)
- static __always_inline void for_each_canary(const struct kfence_metadata meta, bool (fn)(u8 *))
定时采样
- static void toggle_allocation_gate(struct work_struct *work)
- schedule_delayed_work(&kfence_timer, msecs_to_jiffies(kfence_sample_interval));