版本基于:
Linux-5.10
约定:
PAGE_SIZE:4K
内存架构:UMA
0. 前言
本文 kfence 之外的代码版本是基于 Linux5.10,最近需要将 kfence 移植到 Linux5.10 中,本文借此机会将 kfence 机制详细地记录一下。
kfence,全称为 Kernel Electric-Fence,是 Linux5.12 版本新引入的内存使用错误检测机制。
kfence 基本原理非常简单,它创建了自己的专有检测内存池 kfence_pool。然后在 data page 的两边加上 fence page 电子栅栏,利用 MMU 的特性把 fence page 设置为不可访问。如果对 data page 的访问越过 page 边界,就会立刻触发异常。
检测的内存错误有:
- OOB:out-of-bounds access,访问越界;
- UAF:use-after-free,释放再使用;
- invalid free,无效释放;
现在,kfence 检测的内存错误类型不如 KASAN 多,但,kfence 设计的目的:
- be enabled in production kernels,在产品内核中使能
- has near zero performance overhead,接近 0 性能开销
kfence 机制依赖 slab 和kmalloc 机制,熟悉这两个机制能更好理解 kfence。
1. kfence 依赖的config
//当使用 arm64时,该config会被默认select,详细看arch/arm64/Kconfig
CONFIG_HAVE_ARCH_KFENCE//kfence 机制的核心config,需要手动配置,下面所有的config都依赖它
CONFIG_KFENCE------------------ 下面所有config都依赖CONFIG_KFENCE-----------//依赖CONFIG_JUMP_LABEL
//用以启动静态key功能,主要是来优化性能,每次读取kfence_allocation_gate的值是否为0来进行判断,这样的性能
//开销比较大
CONFIG_KFENCE_STATIC_KEYS//kfence pool的获取频率,默认为100ms
// 另外,该config可以设置为0,表示禁用 kfence功能
CONFIG_KFENCE_SAMPLE_INTERVAL//kfence pool中共支持多少个OBJECTS,默认为255,从1~65535之间取值
// 一个kfence object需要申请两个pages
CONFIG_KFENCE_NUM_OBJECTS//stress tesing of fault handling and error reporting, default 0
CONFIG_KFENCE_STRESS_TEST_FAULTS//依赖CONFIG_TRACEPOINTS && CONFIG_KUNIT,用以启动kfence的测试用例
CONFIG_KFENCE_KUNIT_TEST
1. kfence 原理
2. kfence中的重要数据结构
2.1 __kfence_pool
char *__kfence_pool __ro_after_init;
EXPORT_SYMBOL(__kfence_pool); /* Export for test modules. */
kfence 中有个专门的内存池,在 memblock移交 buddy之前从 memblock 中申请的一块内存。
内存的首地址保存在全局变量 __kfence_pool 中。
这里来看下内存池的大小:
include/linux/kfence.h#define KFENCE_POOL_SIZE ((CONFIG_KFENCE_NUM_OBJECTS + 1) * 2 * PAGE_SIZE)
每个 object 会占用 2 pages,一个page 用于 object 自身,另一个page 用作guard page
kfence pool 会在 CONFIG_KFENCE_NUM_OBJECTS 的基础上多申请 2 个pages,即kfence pool 的page0 和 page1。page0 大部分是没用的,仅仅作为一个扩展的guard page。多加上page1 方便简化metadata 索引地址映射。
下面弄一个图方便理解 kfence_pool
- page0 和 page1 就是上面所述的多出来的两个 pages;
- 其他的pages 是 CONFIG_KFENCE_NUM_OBJECTS * 2,每个object 拥有两个pages,第一个为object 本身,第二个为 fence page;
- kfence 定义一个全局变量 kfence_metadata 数组,数组的长度为CONFIG_KFENCE_NUM_OBJECT,里面管理所有的objects,包括obj当前状态,内存地址等信息;
- kfence pool 中可用的 metadata 会被存放在链表 kfence_freelist 中;
- 从图上可以看到,每一个object page 都会被两个 guard page 包裹了;
2.2 kfence_sample_interval
static unsigned long kfence_sample_interval __read_mostly = CONFIG_KFENCE_SAMPLE_INTERVAL;
该变量用以存储 kfence 的采样间隔,默认使用的是 CONFIG_KFENCE_SAMPLE_INTERVAL 的值。当然,内核中还提供内存参数的方式进行配置:
static const struct kernel_param_ops sample_interval_param_ops = {.set = param_set_sample_interval,.get = param_get_sample_interval,
};
module_param_cb(sample_interval, &sample_interval_param_ops, &kfence_sample_interval, 0600);
通过set、get 指定内核参数 kfence.sample_interval 的配置和获取,单位为毫秒。
可以通过设施 kfence.sample_interval=0 来禁用 kfence 功能。
2.3 kfence_enabled
该变量表示kfence pool 初始化成功,kfence 进入正常运行中。
kfence 中一共有两个地方会将 kfence_enables 设为false。
第一个地方:
mm/kfence/core.c#define KFENCE_WARN_ON(cond) \({ \const bool __cond = WARN_ON(cond); \if (unlikely(__cond)) \WRITE_ONCE(kfence_enabled, false); \__cond; \})
在kfence 中很多地方需要确定重要条件不能为 false,通过 KFENCE_WARN_ON() 进行check,如果condition 为false,则将 kfence_enabled 设为 false。
第二个地方:
param_set_sample_interval() 调用时,如果采样间隔设为0,则表示 kfence 功能关闭。
2.4 kfence_metadata
这是一个 struct kfence_metadata 的全局变量。用以管理所有 kfence objects:
mm/kfence/kfence.hstruct kfence_metadata {struct list_head list; //kfence_metadata为kfence_freelist中的一个节点struct rcu_head rcu_head; //delayed freeing 使用//每个kfence_metadata带有一个自旋锁,用以保护data一致性//我们不能将同一个metadata从freelist 中抓取两次,也不能对同一个metadata进行__kfence_alloc() 多次raw_spinlock_t lock;//object 的当前状态,默认为UNUSEDenum kfence_object_state state;//对象的基地址,都是按照页对齐的unsigned long addr;/** The size of the original allocation.*/size_t size;//最后一次从对象中分配内存的kmem_cache//如果没有申请或kmem_cache被销毁,则该值为NULLstruct kmem_cache *cache;//记录发生异常的地址unsigned long unprotected_page;/* 分配或释放的栈信息 */struct kfence_track alloc_track;struct kfence_track free_track;
};
2.5 kfence_freelist
/* Freelist with available objects. */
static struct list_head kfence_freelist = LIST_HEAD_INIT(kfence_freelist);
static DEFINE_RAW_SPINLOCK(kfence_freelist_lock); /* Lock protecting freelist. */
用以管理所有的可用的kfence objeces
2.6 kfence_allocation_gate
这是个 atomic_t 变量,是kfence 定时开放分配的闸门,0 表示允许分配,非0表示不允许分配。
正常情况下,在 kfence_alloc() 进行内存分配的时候,会通过atomic_read() 读取该变量的值,如果为0,则表示允许分配,kfence 会进一步调用 __kfence_alloc() 函数。
当考虑到性能问题,内核启动了 static key 功能,即变量 kfence_allocation_key,详见下一小节。
2.7 kfence_allocation_key
这个是kfence 分配的static key,需要 CONFIG_KFENCE_STATIC_KEYS 使能。
#ifdef CONFIG_KFENCE_STATIC_KEYS
/* The static key to set up a KFENCE allocation. */
DEFINE_STATIC_KEY_FALSE(kfence_allocation_key);
#endif
这是一个 static_key_false key。
如果 CONFIG_KFENCE_STATIC_KEYS 使能,在 kfence_alloc() 的时候将不再判断 kfence_allocation_gate 的值,而是判断该key 的值。
3. kfence 初始化
init/main.cstatic void __init mm_init(void)
{...kfence_alloc_pool();report_meminit();mem_init();...
}
从《buddy 初始化》一文中得知,mm_init() 函数开始将进行buddy 系统的内存初始化。而在函数 mem_init() 中会通过 free 操作,将内存一个页块一个页块的添加到 buddy 系统中。
而 kfence pool 是在 mem_init() 调用之前,从memblock 中分配出一段内存。
3.1 kfence_alloc_pool()
mm/kfence/core.cvoid __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");
}
代码比较简单:
- 确认 kfence_sample_interval 是否为0,如果为0 则表示kfence 为disabled;
- 通过 memblock_alloc() 申请 KFENCE_POOL_SIZE 的空间,PAGE_SIZE 对齐;
3.2 kfence_init()
该函数被放置在 start_kernel() 函数比较靠后的位置,此时buddy初始化、slab初始化、workqueue 初始化等已经完成。
mm/kfence/core.cvoid __init kfence_init(void)
{/* Setting kfence_sample_interval to 0 on boot disables KFENCE. */if (!kfence_sample_interval)return;if (!kfence_init_pool()) {pr_err("%s failed\n", __func__);return;}WRITE_ONCE(kfence_enabled, true);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));
}
- 同样,当采样间隔设为0,即 kfence_sample_interval 为0 时,关闭kfence;
- 调用 kfence_init_pool() 对kfence pool 进行初始化;
- 变量 kfence_enabled 设为 true,表示 kfence 功能正常,可以正常工作;
- 创建工作队列 kfence_timer,并添加到 system_unbound_wq 中,注意这里延迟为0,即立刻执行 kfence_timer;
注意最后打印的信息,在kfence pool 初始化结束,会从dmesg 中看到如下log:
<6>[ 0.000000] kfence: initialized - using 2097152 bytes for 255 objects at 0x(____ptrval____)-0x(____ptrval____)
系统中申请了 255 个 objects,共使用 2M 的内存空间。
3.2.1 kfence_init_pool()
mm/kfence/core.cstatic bool __init kfence_init_pool(void)
{unsigned long addr = (unsigned long)__kfence_pool;struct page *pages;int i;//确认 __kfence_pool已经申请成功,kfence_alloc_pool()会从memblock中申请if (!__kfence_pool)return false;//对于 arm64架构,该函数直接返回true//对于 x86架构,会通过lookup_address()检查__kfence_pool是否映射到物理地址了if (!arch_kfence_init_pool())goto err;//获取映射好的pages,从vmemmap 中查找pages = virt_to_page(addr);//配置kfence pool中的page,将其打上slab页的标记for (i = 0; i < KFENCE_POOL_SIZE / PAGE_SIZE; i++) {if (!i || (i % 2)) //第0页和奇数页跳过,即配置偶数页continue;//确认pages不是复合页if (WARN_ON(compound_head(&pages[i]) != &pages[i]))goto err;__SetPageSlab(&pages[i]);}//将kfence pool的前两个页面设为guard pages//主要是清除对应 pte项的present位,这样当CPU访问前两页就会触发缺页异常,就会进入kfence处理流程for (i = 0; i < 2; i++) {if (unlikely(!kfence_protect(addr)))goto err;addr += PAGE_SIZE;}//遍历所有的kfence objects页面,kfence_metadata数组是专门对CONFIG_KFENCE_NUM_OBJECTS个对象的状态进行管理for (i = 0; i < CONFIG_KFENCE_NUM_OBJECTS; i++) {struct kfence_metadata *meta = &kfence_metadata[i];/* 初始化kfence metadata */INIT_LIST_HEAD(&meta->list); //初始化kfence_metadata节点raw_spin_lock_init(&meta->lock); //初始化spi lockmeta->state = KFENCE_OBJECT_UNUSED; //所有的起始状态是UNUSEDmeta->addr = addr; //保存该对象的page地址list_add_tail(&meta->list, &kfence_freelist); //将可用的metadata添加到kfence_freelist尾部//保护每个object的右边区域的pageif (unlikely(!kfence_protect(addr + PAGE_SIZE)))goto err;addr += 2 * PAGE_SIZE; //跳到下一个对象}//kfence pool是一直活着的,从此时起永远不会被释放//之前在调用 memblock_alloc()时在 kmemleak中留有记录,这里要删除这部分记录,防止与后面调用// kfence_alloc()分配时出现冲突kmemleak_free(__kfence_pool);return true;err:/** Only release unprotected pages, and do not try to go back and change* page attributes due to risk of failing to do so as well. If changing* page attributes for some pages fails, it is very likely that it also* fails for the first page, and therefore expect addr==__kfence_pool in* most failure cases.*/memblock_free_late(__pa(addr), KFENCE_POOL_SIZE - (addr - (unsigned long)__kfence_pool));__kfence_pool = NULL;return false;
}
3.2.2 kfence_timer
在上面 kfence_init_pool() 成功完成之后,kfence_init() 会进入下一步:创建周期性的工作队列。
queue_delayed_work(system_unbound_wq, &kfence_timer, 0);
注意最后一个参数为0,因为这里是kfence_init(),第一次执行 kfence_timer 会立即执行,之后的 kfence_timer 会有个 kfence_sample_interval 的延迟。
来看下 kfence_timer 的创建:
mm/kfence/core.cstatic DECLARE_DELAYED_WORK(kfence_timer, toggle_allocation_gate);
通过调用 DECLARE_DELAYED_WORK() 初始化一个延迟队列,toggle_allocation_gate() 为时间到达后的处理函数。
下面来看下 toggle_allocation_gate():
mm/kfence/core.cstatic void toggle_allocation_gate(struct work_struct *work)
{//首先确定kfence功能正常if (!READ_ONCE(kfence_enabled))return;//将 kfence_allocation_gate 设为0// 这是kfence内存池开启分配的标志,0表示开启,非0表示关闭// 这样保证每隔一段时间,最多只允许从kfence内存池分配一次内存atomic_set(&kfence_allocation_gate, 0);#ifdef CONFIG_KFENCE_STATIC_KEYS//使能static key,等到分配的发生static_branch_enable(&kfence_allocation_key);//内核发出 hung task警告的时间最短时间长度,为CONFIG_DEFAULT_HUNG_TASK_TIMEOUT的值if (sysctl_hung_task_timeout_secs) {//如果内存分配没有那么频繁,就有可能出现等待时间过长的问题,// 这里将等待超过时间设置为hung task警告时间的一半,// 这样,内核就不会因为处于D状态过长导致内核出现警告wait_event_idle_timeout(allocation_wait, atomic_read(&kfence_allocation_gate),sysctl_hung_task_timeout_secs * HZ / 2);} else {//如果hungtask检测时间为0,表示时间无限长,那么可以放心等待下去,直到有人从kfence中// 分配了内存,会将kfence_allocation_gate设为1,然后唤醒阻塞在allocation_wait里的任务wait_event_idle(allocation_wait, atomic_read(&kfence_allocation_gate));}/* 将static key关闭,保证不会进入 __kfence_alloc() */static_branch_disable(&kfence_allocation_key);
#endif//等待kfence_sample_interval,单位是毫秒,然后再次开启kfence内存池分配queue_delayed_work(system_unbound_wq, &kfence_timer,msecs_to_jiffies(kfence_sample_interval));
}
注意 static key 需要 CONFIG_KFNECE_STATIC_KEYS 使能。
这里使用 static key,主要是来优化性能,每次读取 kfence_allocation_gate 的值是否为0来进行判断,这样的性能开销比较大。
另外,在此次 toggle 执行完成后,会再次调用 queue_delayed_work() 进入下一次work,只不过有个 delay——kfence_sample_interval。
4. kfence 申请
kfence 申请的核心接口是 __kfence_alloc() 函数,系统中调用该函数有两个地方:
- kmem_cache_alloc_bulk()
- slab_alloc_node()
第一个函数只有在 io_alloc_req() 函数中调用,详见 fs/io_uring.c
第二个函数如果只考虑 UMA 架构,起点只会是 slab_alloc() 函数,调用的地方有:
kmem_cache_alloc()
kmem_cache_alloc_trace()
__kmalloc()
函数的细节可以查看《slub 分配器之kmem_cache_alloc》和《slub 分配器之kmalloc详解》
5. kfence 释放