[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
[置顶] Linux Lab v1.4 升级部分内核到 v6.10,新增泰晓 RISC-V 实验箱支持,新增最小化内核配置支持大幅提升内核编译速度,在单终端内新增多窗口调试功能等Linux Lab 发布 v1.4 正式版,升级部分内核到 v6.10,新增泰晓实验箱支持
[置顶] 泰晓社区近日发布了一款儿童益智版 Linux 系统盘,集成了数十个教育类与益智游戏类开源软件国内首个儿童 Linux 系统来了,既可打字编程学习数理化,还能下棋研究数独提升智力
Linux Kfence 详解
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 的主要特点如下:
item | Kfence | Kasan |
---|---|---|
检测密度 | 抽样法,默认每 100ms 提供一个可检测的内存 | 对所有内存访问进行检测 |
检测粒度 | 核心的检测粒度为 page | 检测粒度为字节 |
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。
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():
参考文档
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 | 微信打赏 | |
请作者喝杯咖啡吧 |
Read Album:
- RISC-V Linux 内核及周边技术动态第 111 期
- RISC-V Linux 内核及周边技术动态第 110 期
- RISC-V Linux 内核及周边技术动态第 109 期
- The Real Time Linux 官方文档翻译
- RISC-V Linux 内核及周边技术动态第 108 期