1. 原理介绍
Kfence (Kernel Electric Fence) 是 Linux 内核引入的一种低开销的内存错误检测机制,因为是低开销的所以它可以在运行的生产环境中开启,同样由于是低开销所以它的功能相比较 KASAN 会偏弱。
Kfence 的基本原理非常简单,它创建了自己的专有检测内存池 kfence_pool
。在 data page
的两边加上了 fence page
电子栅栏,利用 MMU 的特性把 fence page
设置成不可访问。如果对 data page
的访问越过了 page 边界, 就会立刻触发异常。
Kfence 的主要特点如下:
item | Kfence | KASAN |
---|---|---|
检测密度 | 抽样法,默认每 100ms 提供一个可检测的内存 | 对所有内存访问进行检测 |
检测粒度 | 核心的检测粒度为 page | 检测粒度为字节 |
1.1 slub/slab hook
Kfence 把自己 hook 到 slub/slab
的 malloc()/free()
流程当中去。但并不是所有的 slub/slab
内存都会从 kfence_pool
内存池中分配。它规定了两个条件:
- 1、默认每隔 100 ms,开放从
kfence_pool
内存池中分配一次数据。分配成功后会把kfence_allocation_gate
加 1,阻止继续从kfence_pool
的分配。kfence_timer
定时到期以后,又会重新开放一次分配。这相当于一种抽样法
。 - 2、每次分配都会占用
kfence_pool
中的一个data page
,所以可分配的内存长度最大为 1 page。
1.2 out-of-bounds (over data page)
从 kfence_pool
中成功分配一个内存对象 obj
,不管 obj
的实际大小有多大,都会占据一个 data page
。
当原本访问 obj
的操作溢出到相邻的 fence page
时,会立即触发 CPU 异常,通过堆栈回溯揪出异常访问的元凶。
1.3 out-of-bounds (in data page)
大部分情况下 obj
是小于一个 page 的,对于 data page
剩余空间系统使用 canary pattern
进行填充。这种操作是为了检测超出了 obj
但还在 data page
范围内的溢出访问。
这种类型的溢出是不能在溢出发生时立刻触发的,它只能在 obj
free 时,通过检测 canary pattern
被破坏来检测到有 canary
区域的溢出访问。但是异常访问的元凶却不能直接抓出来。
1.4 use-after-free
在 obj
被 free 以后,对应 data page
也会被设置成不可访问状态。
这种状态下,如果有操作继续访问 obj
会立即触发 CPU 异常,通过堆栈回溯揪出异常访问的元凶。
1.5 invalid-free
在 obj
free 时会判断记录的 malloc 信息,判断是不是一次异常的 free。
2. 代码解析
分析以下关键的代码流程:
2.1 kfence_protect()
把 fence page
设置成不可访问的核心就是通过 MMU 清除掉 PTE 中的 present
标志位:
kfence_init_pool() → kfence_protect() → kfence_protect_page():
kfence_free() → __kfence_free() → kfence_guarded_free() → kfence_protect() → kfence_protect_page():
linux-5.16.14\arch\riscv\include\asm\kfence.h:
static inline bool kfence_protect_page(unsigned long addr, bool protect)
{
pte_t *pte = virt_to_kpte(addr);
if (protect)
set_pte(pte, __pte(pte_val(*pte) & ~_PAGE_PRESENT));
else
set_pte(pte, __pte(pte_val(*pte) | _PAGE_PRESENT));
flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
return true;
}
2.2 kfence_alloc_pool()
在系统启动时保留 Kfence 需要用到的内存 Page,默认保留 255 个 data page
:
start_kernel() → mm_init() → kfence_alloc_pool():
void __init kfence_alloc_pool(void)
{
if (!kfence_sample_interval)
return;
__kfence_pool = memblock_alloc(KFENCE_POOL_SIZE, PAGE_SIZE);
if (!__kfence_pool)
pr_err("failed to allocate pool\n");
}
#define KFENCE_POOL_SIZE ((CONFIG_KFENCE_NUM_OBJECTS + 1) * 2 * PAGE_SIZE)
config KFENCE_NUM_OBJECTS
int "Number of guarded objects available"
range 1 65535
default 255
2.3 kfence_init()
void __init kfence_init(void)
{
/* Setting kfence_sample_interval to 0 on boot disables KFENCE. */
if (!kfence_sample_interval)
return;
stack_hash_seed = (u32)random_get_entropy();
/* (1) 初始化 kfence pool 内存池 */
if (!kfence_init_pool()) {
pr_err("%s failed\n", __func__);
return;
}
if (!IS_ENABLED(CONFIG_KFENCE_STATIC_KEYS))
static_branch_enable(&kfence_allocation_key);
WRITE_ONCE(kfence_enabled, true);
/* (2) 初始化定时释放 guard 的 timer */
queue_delayed_work(system_unbound_wq, &kfence_timer, 0);
pr_info("initialized - using %lu bytes for %d objects at 0x%p-0x%p\n", KFENCE_POOL_SIZE,
CONFIG_KFENCE_NUM_OBJECTS, (void *)__kfence_pool,
(void *)(__kfence_pool + KFENCE_POOL_SIZE));
}
2.4 kfence_alloc()
内存分配流程:
kmem_cache_alloc() → slab_alloc() → kfence_alloc() → __kfence_alloc() → kfence_guarded_alloc():
2.5 kfence_free()
内存释放流程:
kfence_free() → __kfence_free() → kfence_guarded_free():
参考文档
1.Linux内存异常检测工具—kfence
2.Kernel Electric-Fence (KFENCE)
3.Linux Kernel Sanitizers
4.Linux开源动态之一种新的内存非法访问检查工具KFence