内核电围栏 (KFENCE)

内核电围栏 (KFENCE) 是一种低开销的基于采样的内存安全错误检测器。KFENCE 可以检测堆越界访问、释放后使用和无效释放错误。

KFENCE 设计为可在生产内核中启用,并且性能开销几乎为零。与 KASAN 相比,KFENCE 用性能换取精度。KFENCE 设计背后的主要动机是,在总运行时间足够长的情况下,KFENCE 将检测到非生产测试工作负载通常不会执行的代码路径中的错误。快速实现足够长的总运行时间的一种方法是在大量机器上部署该工具。

用法

要启用 KFENCE,请使用以下配置内核

CONFIG_KFENCE=y

要构建支持 KFENCE 但默认禁用的内核(要启用,请将 kfence.sample_interval 设置为非零值),请使用以下配置内核

CONFIG_KFENCE=y
CONFIG_KFENCE_SAMPLE_INTERVAL=0

KFENCE 提供了其他几个配置选项来自定义行为(有关更多信息,请参阅 lib/Kconfig.kfence 中的相应帮助文本)。

调整性能

最重要的参数是 KFENCE 的采样间隔,可以通过内核启动参数 kfence.sample_interval 以毫秒为单位进行设置。采样间隔确定堆分配将由 KFENCE 保护的频率。默认值可以通过 Kconfig 选项 CONFIG_KFENCE_SAMPLE_INTERVAL 进行配置。设置 kfence.sample_interval=0 会禁用 KFENCE。

采样间隔控制一个定时器,该定时器设置 KFENCE 分配。默认情况下,为了保持实际采样间隔的可预测性,当系统完全空闲时,普通定时器还会导致 CPU 唤醒。这在受功率限制的系统上可能是不希望的。启动参数 kfence.deferrable=1 而是切换到“可延迟”定时器,该定时器不会在空闲系统上强制 CPU 唤醒,但有采样间隔不可预测的风险。默认值可以通过 Kconfig 选项 CONFIG_KFENCE_DEFERRABLE 进行配置。

警告

由于它目前会导致非常不可预测的采样间隔,因此在使用可延迟定时器时,KUnit 测试套件很可能会失败。

默认情况下,KFENCE 在每个采样间隔内仅采样 1 个堆分配。突发模式 允许采样连续的堆分配,其中内核启动参数 kfence.burst 可以设置为一个非零值,表示采样间隔内的额外连续分配;设置 kfence.burst=N 表示在每个采样间隔内尝试通过 KFENCE 进行 1 + N 次连续分配。

KFENCE 内存池的大小是固定的,如果池耗尽,则不会发生进一步的 KFENCE 分配。通过 CONFIG_KFENCE_NUM_OBJECTS(默认值为 255),可以控制可用保护对象的数量。每个对象需要 2 个页面,一个用于对象本身,另一个用作保护页面;对象页面与保护页面交错,因此每个对象页面都被两个保护页面包围。

专用于 KFENCE 内存池的总内存可以计算为

( #objects + 1 ) * 2 * PAGE_SIZE

使用默认配置并假设页面大小为 4 KiB,则会将 2 MiB 专用于 KFENCE 内存池。

注意:在支持大页面的架构上,KFENCE 将确保池使用大小为 PAGE_SIZE 的页面。这将导致分配额外的页表。

错误报告

典型的越界访问如下所示

==================================================================
BUG: KFENCE: out-of-bounds read in test_out_of_bounds_read+0xa6/0x234

Out-of-bounds read at 0xffff8c3f2e291fff (1B left of kfence-#72):
 test_out_of_bounds_read+0xa6/0x234
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

kfence-#72: 0xffff8c3f2e292000-0xffff8c3f2e29201f, size=32, cache=kmalloc-32

allocated by task 484 on cpu 0 at 32.919330s:
 test_alloc+0xfe/0x738
 test_out_of_bounds_read+0x9b/0x234
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

CPU: 0 PID: 484 Comm: kunit_try_catch Not tainted 5.13.0-rc3+ #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

报告的标题提供了访问中涉及的函数的简短摘要。紧随其后的是有关访问及其来源的更详细信息。请注意,仅在使用内核命令行选项 no_hash_pointers 时才会显示真实的内核地址。

释放后使用访问报告为

==================================================================
BUG: KFENCE: use-after-free read in test_use_after_free_read+0xb3/0x143

Use-after-free read at 0xffff8c3f2e2a0000 (in kfence-#79):
 test_use_after_free_read+0xb3/0x143
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

kfence-#79: 0xffff8c3f2e2a0000-0xffff8c3f2e2a001f, size=32, cache=kmalloc-32

allocated by task 488 on cpu 2 at 33.871326s:
 test_alloc+0xfe/0x738
 test_use_after_free_read+0x76/0x143
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

freed by task 488 on cpu 2 at 33.871358s:
 test_use_after_free_read+0xa8/0x143
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

CPU: 2 PID: 488 Comm: kunit_try_catch Tainted: G    B             5.13.0-rc3+ #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

KFENCE 还会报告无效释放,例如双重释放

==================================================================
BUG: KFENCE: invalid free in test_double_free+0xdc/0x171

Invalid free of 0xffff8c3f2e2a4000 (in kfence-#81):
 test_double_free+0xdc/0x171
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

kfence-#81: 0xffff8c3f2e2a4000-0xffff8c3f2e2a401f, size=32, cache=kmalloc-32

allocated by task 490 on cpu 1 at 34.175321s:
 test_alloc+0xfe/0x738
 test_double_free+0x76/0x171
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

freed by task 490 on cpu 1 at 34.175348s:
 test_double_free+0xa8/0x171
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

CPU: 1 PID: 490 Comm: kunit_try_catch Tainted: G    B             5.13.0-rc3+ #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

KFENCE 还在对象的保护页的另一侧使用基于模式的红色区域,以检测对象非保护侧的越界写入。这些报告在释放时

==================================================================
BUG: KFENCE: memory corruption in test_kmalloc_aligned_oob_write+0xef/0x184

Corrupted memory at 0xffff8c3f2e33aff9 [ 0xac . . . . . . ] (in kfence-#156):
 test_kmalloc_aligned_oob_write+0xef/0x184
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

kfence-#156: 0xffff8c3f2e33afb0-0xffff8c3f2e33aff8, size=73, cache=kmalloc-96

allocated by task 502 on cpu 7 at 42.159302s:
 test_alloc+0xfe/0x738
 test_kmalloc_aligned_oob_write+0x57/0x184
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

CPU: 7 PID: 502 Comm: kunit_try_catch Tainted: G    B             5.13.0-rc3+ #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

对于此类错误,会显示发生损坏的地址以及无效写入的字节(相对于地址的偏移量);在此表示中,“.”表示未触及的字节。在上面的示例中,0xac 是写入偏移量为 0 的无效地址的值,其余的“.”表示未触及任何后续字节。请注意,只有在内核使用 no_hash_pointers 启动时才会显示真实值;为了避免信息泄露,否则会使用“!”来表示无效写入的字节。

最后,KFENCE 还可能报告对任何受保护页面的无效访问,其中无法确定关联的对象,例如,如果尚未分配相邻的对象页面

==================================================================
BUG: KFENCE: invalid read in test_invalid_access+0x26/0xe0

Invalid read at 0xffffffffb670b00a:
 test_invalid_access+0x26/0xe0
 kunit_try_run_case+0x51/0x85
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x137/0x160
 ret_from_fork+0x22/0x30

CPU: 4 PID: 124 Comm: kunit_try_catch Tainted: G        W         5.8.0-rc6+ #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1 04/01/2014
==================================================================

DebugFS 接口

一些调试信息通过 debugfs 公开

  • 文件 /sys/kernel/debug/kfence/stats 提供运行时统计信息。

  • 文件 /sys/kernel/debug/kfence/objects 提供通过 KFENCE 分配的对象的列表,包括那些已释放但受保护的对象。

实现细节

受保护的分配是根据采样间隔设置的。在采样间隔过期后,通过主分配器(SLAB 或 SLUB)的下一个分配将从 KFENCE 对象池返回一个受保护的分配(支持的最大分配大小为 PAGE_SIZE)。此时,定时器将重置,并且在间隔过期后设置下一个分配。

当使用 CONFIG_KFENCE_STATIC_KEYS=y 时,通过静态分支,KFENCE 分配通过依赖于静态键基础架构的主分配器的快速路径进行“门控”。切换静态分支以将分配重定向到 KFENCE。根据采样间隔、目标工作负载和系统架构,这可能比简单的动态分支执行得更好。建议进行仔细的基准测试。

每个 KFENCE 对象都驻留在专用页面上,在随机选择的左侧或右侧页面边界。对象页面左侧和右侧的页面是“保护页面”,其属性更改为受保护状态,并导致任何尝试访问时发生页面错误。然后,KFENCE 会拦截这些页面错误,通过报告越界访问来优雅地处理错误,并将页面标记为可访问,以便错误的代码可以(错误地)继续执行(设置 panic_on_warn 来代替崩溃)。

为了检测对象页面本身内部的内存越界写入,KFENCE 还使用基于模式的红色区域。对于每个对象页面,都为所有非对象内存设置一个红色区域。对于典型的对齐方式,红色区域仅在对象的非保护侧需要。由于 KFENCE 必须遵守缓存请求的对齐方式,因此特殊的对齐方式可能会导致对象两侧出现未保护的间隙,所有这些间隙都将被红色区域化。

下图说明了页面布局

---+-----------+-----------+-----------+-----------+-----------+---
   | xxxxxxxxx | O :       | xxxxxxxxx |       : O | xxxxxxxxx |
   | xxxxxxxxx | B :       | xxxxxxxxx |       : B | xxxxxxxxx |
   | x GUARD x | J : RED-  | x GUARD x | RED-  : J | x GUARD x |
   | xxxxxxxxx | E :  ZONE | xxxxxxxxx |  ZONE : E | xxxxxxxxx |
   | xxxxxxxxx | C :       | xxxxxxxxx |       : C | xxxxxxxxx |
   | xxxxxxxxx | T :       | xxxxxxxxx |       : T | xxxxxxxxx |
---+-----------+-----------+-----------+-----------+-----------+---

在释放 KFENCE 对象时,对象的页面再次受到保护,并且该对象被标记为已释放。任何进一步访问该对象都会导致错误,并且 KFENCE 会报告释放后使用访问。释放的对象会插入到 KFENCE 的空闲列表的尾部,以便首先重用最近释放的对象,从而增加检测最近释放的对象的释放后使用的机会。

如果池利用率达到 75%(默认值)或更高,为了降低池最终被已分配对象完全占用的风险,同时确保分配的覆盖范围多样化,KFENCE 会限制当前相同来源的已覆盖分配进一步填充池。分配的“来源”基于其部分分配堆栈跟踪。副作用是,这也限制了同一来源的频繁长期分配(例如页面缓存)永久填充池,这是池被填满且采样分配率降至零的最常见风险。开始限制当前覆盖的分配的阈值可以通过启动参数 kfence.skip_covered_thresh(池使用率%)进行配置。

接口

以下描述了分配器以及页面处理代码用来设置和处理 KFENCE 分配的函数。

bool is_kfence_address(const void *addr)

检查地址是否属于 KFENCE 池

参数

const void *addr

要检查的地址

返回值

根据地址是否在 KFENCE 对象范围内返回 true 或 false。

描述

KFENCE 对象存在于单独的页面范围内,不应与常规堆对象混合(例如,KFENCE 对象绝不能添加到分配器的空闲列表中)。如果这样做,可能会并且将会导致堆损坏,因此必须使用 is_kfence_address() 来检查对象是否需要特殊处理。

注意

此函数可能在快速路径中使用,并且对性能至关重要。未来的更改应考虑这一点;例如,我们希望避免引入额外的加载,因此需要保持 KFENCE_POOL_SIZE 为常量(直到内核中添加即时修补支持)。

void kfence_shutdown_cache(struct kmem_cache *s)

处理 KFENCE 对象的 shutdown_cache()

参数

struct kmem_cache *s

正在关闭的缓存

描述

在关闭缓存之前,必须确保没有从该缓存分配的剩余对象。由于 KFENCE 对象不是直接从缓存引用的,因此我们需要在此处检查它们。

请注意,shutdown_cache() 是 SL*B 的内部函数,如果仍然存在已分配的对象,则 kmem_cache_destroy() 不会返回:它会打印错误消息并直接中止缓存的销毁,从而导致内存泄漏。

如果唯一的此类对象是 KFENCE 对象,我们将不会泄漏整个缓存,而是尝试通过使已分配的对象成为“僵尸分配”来提供更有用的调试信息。然后,对象仍然可以被使用或释放(这将得到优雅的处理),但是使用将导致显示 KFENCE 错误报告,其中包括指向对象用户、原始分配站点以及 shutdown_cache() 调用者的堆栈跟踪。

void *kfence_alloc(struct kmem_cache *s, size_t size, gfp_t flags)

以较低的概率分配一个 KFENCE 对象

参数

struct kmem_cache *s

具有对象要求的 struct kmem_cache

size_t size

要分配的对象的精确大小(可以小于 s->size,例如,对于 kmalloc 缓存)

gfp_t flags

GFP 标志

返回值

  • NULL - 必须像往常一样继续分配,

  • 非 NULL - 指向 KFENCE 对象的指针。

描述

kfence_alloc() 应该插入到堆分配快速路径中,允许它使用静态分支以较低的概率透明地返回 KFENCE 分配的对象(该概率由 kfence.sample_interval 启动参数控制)。

size_t kfence_ksize(const void *addr)

获取为 KFENCE 对象分配的实际内存量

参数

const void *addr

指向堆对象的指针

返回值

  • 0 - 不是 KFENCE 对象,必须改为调用 __ksize(),

  • 非 0 - 可以访问这么多字节而不会导致内存错误。

描述

kfence_ksize() 返回在分配时为 KFENCE 对象请求的字节数。此数字可能小于相应的 struct kmem_cache 的对象大小。

void *kfence_object_start(const void *addr)

查找 KFENCE 对象的开头

参数

const void *addr

KFENCE 分配的对象内的地址

返回值

对象的开头的地址。

描述

SL[AU]B 分配的对象在页面内逐个布局,因此,给定对象内部的指针和对象大小,很容易计算出对象的开头。对于 KFENCE 来说,情况并非如此,它将单个对象放置在页面的任一端。此辅助函数用于查找 KFENCE 分配的对象的开头。

void __kfence_free(void *addr)

将 KFENCE 堆对象释放到 KFENCE 池

参数

void *addr

要释放的对象

描述

要求:is_kfence_address(addr)

释放 KFENCE 对象并将其标记为已释放。

bool kfence_free(void *addr)

尝试将任意堆对象释放到 KFENCE 池

参数

void *addr

要释放的对象

返回值

  • false - 对象不属于 KFENCE 池,已被忽略,

  • true - 对象已释放到 KFENCE 池。

描述

释放 KFENCE 对象并将其标记为已释放。可以在任何对象(甚至是非 KFENCE 对象)上调用,以简化钩子与分配器的释放代码路径的集成。分配器必须检查返回值以确定它是否是 KFENCE 对象。

bool kfence_handle_page_fault(unsigned long addr, bool is_write, struct pt_regs *regs)

执行 KFENCE 页面的页面错误处理

参数

unsigned long addr

发生错误的地址

bool is_write

是否为写入访问

struct pt_regs *regs

当前的 struct pt_regs(可以为 NULL,但会显示完整的堆栈跟踪)

返回值

  • false - 地址在 KFENCE 池外部,

  • true - 页面错误由 KFENCE 处理,无需额外处理。

描述

KFENCE 池内部的页面错误表示内存错误,例如越界访问、使用后释放或无效内存访问。在这些情况下,KFENCE 会打印错误消息,并将有问题的页面标记为存在,以便内核可以继续运行。