泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

泰晓RISC-V实验箱,转战RISC-V,开箱即用
请稍侯

Linux Kfence 详解

Peng Weilin 创作于 2022/06/07

Author: pwl999 Date: 2022/04/28 Project: RISC-V Linux 内核剖析

原理介绍

Kfence (Kernel Electric Fence) 是 Linux 内核引入的一种低开销的内存错误检测机制,因为是低开销的所以它可以在运行的生产环境中开启,同样由于是低开销所以它的功能相比较 Kasan 会偏弱。

Kfence 的基本原理非常简单,它创建了自己的专有检测内存池 kfence_pool。在 data page 的两边加上了 fence page 电子栅栏,利用 MMU 的特性把 fence page 设置成不可访问。如果对 data page 的访问越过了 page 边界, 就会立刻触发异常。

Kfence 的主要特点如下:

itemKfenceKasan
检测密度抽样法,默认每 100ms 提供一个可检测的内存对所有内存访问进行检测
检测粒度核心的检测粒度为 page检测粒度为字节

slub/slab hook

Kfence 把自己 hook 到 slub/slabmalloc()/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。

out-of-bounds (over data page)

kfence_pool 中成功分配一个内存对象 obj,不管 obj 的实际大小有多大,都会占据一个 data page

当原本访问 obj 的操作溢出到相邻的 fence page 时,会立即触发 CPU 异常,通过堆栈回溯揪出异常访问的元凶。

out-of-bounds (in data page)

大部分情况下 obj 是小于一个 page 的,对于 data page 剩余空间系统使用 canary pattern 进行填充。这种操作是为了检测超出了 obj 但还在 data page 范围内的溢出访问。

这种类型的溢出是不能在溢出发生时立刻触发的,它只能在 obj free 时,通过检测 canary pattern 被破坏来检测到有 canary 区域的溢出访问。但是异常访问的元凶却不能直接抓出来。

use-after-free

obj 被 free 以后,对应 data page 也会被设置成不可访问状态。

这种状态下,如果有操作继续访问 obj 会立即触发 CPU 异常,通过堆栈回溯揪出异常访问的元凶。

invalid-free

obj free 时会判断记录的 malloc 信息,判断是不是一次异常的 free。

代码解析

分析以下关键的代码流程:

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;
}

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

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));
}

kfence_alloc()

内存分配流程:

kmem_cache_alloc() → slab_alloc() → kfence_alloc() → __kfence_alloc() → kfence_guarded_alloc():

kfence_free()

内存释放流程:

kfence_free() → __kfence_free() → kfence_guarded_free():

参考文档

  1. Linux内存异常检测工具—kfence
  2. Kernel Electric-Fence (KFENCE)
  3. Linux Kernel Sanitizers
  4. Linux开源动态之一种新的内存非法访问检查工具KFence


Read Album:

Read Related:

Read Latest: