内核电子围栏 (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 个堆分配。 *Burst mode* 允许采样连续的堆分配,其中内核启动参数 kfence.burst 可以设置为非零值,该值表示采样间隔内的 *additional* 连续分配;设置 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 拦截,KFENCE 通过报告越界访问并标记页面为可访问来优雅地处理该错误,以便发生错误的的代码可以(错误地)继续执行(设置 panic_on_warn 以改为 panic)。

为了检测对象页面本身内的内存的越界写入,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

要检查的地址

返回

true 或 false,取决于地址是否在 KFENCE 对象范围内。

描述

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 会打印一条错误消息,并将有问题的页面标记为存在,以便内核可以继续运行。