Zephyr内存管理之Heap进阶--安全加固

在五年前的文章Zephyr内存管理之Heap对 Zephyr 的 Heap 实现基本原理——如 Chunk 单元、双向循环链表、Bucket 桶分配进行了详细分析。随着 Zephyr 在安全关键型(Safety-critical)领域的应用,Zephyr 4.4 后 sys_heap 在原有基础上引入了加固(Hardening)机制,alloc/free 管理算法不变,但在每一个转换节点都埋下了“陷阱”来捕捉 Heap 损坏。

修改概览 链接到标题

与五年前的 Heap 管理相比,Heap 的 alloc/free 管理机制并没有发生变化,主要的变动有 3 点:

  • 代码目录调整
  • 新增运行时统计和监听
  • 新增加固机制

本文重点分析加固机制。

代码目录调整 链接到标题

原来的代码放在 lib/os 下,现在为 Heap 单独建立了目录 lib/heap,其中 Heap 管理和加固机制都实现在 heap.c 中。

.
├── CMakeLists.txt
├── heap_array.c        // Heap 信息收集
├── heap.c              // Heap 核心管理算法
├── heap_constants.c    // Heap 常量定义
├── heap.h
├── heap_info.c         // 打印 Heap 信息
├── heap_listener.c     // Heap 监听器
├── heap_stats.c        // 获取 Heap 统计状态
├── heap_stress.c       // Heap 压力测试
├── heap_validate.c     // Heap 验证
├── Kconfig
├── multi_heap.c            // 多 Heap 管理
├── multi_heap.h
└── shared_multi_heap.c     // 共享多 Heap 管理

多 Heap 管理和共享多 Heap 管理是构建在 Heap 管理之上的一层,是基于 Heap 之上的一层管理,详细内容可以查看Zephyr内存管理之共享Heap

新增运行时统计和监听 链接到标题

涉及的文件是 heap_array.c, heap_stats.c, heap_listener.c

  • Runtime Stats: 实时维护 free_bytesallocated_bytesmax_allocated_bytes,不再需要线性扫描来获取剩余空间。
  • Heap Listener: 引入了回调机制。当内存分配、释放或 Heap 扩容时,可以通知外部模块(如内存分析工具)。

新增加固机制 链接到标题

Zephyr 4.4 引入 Heap 分级加固 (Hardening Levels),通过 CONFIG_SYS_HEAP_HARDENING_LEVEL 定义了五个等级:

  • CONFIG_SYS_HEAP_HARDENING_NONE Level 0: 无检查,原本的 Heap 管理。
  • CONFIG_SYS_HEAP_HARDENING_BASIC Level 1: 基础检查。能识别简单的双重释放(Double Free)和通过相邻 Chunk 校验捕获的缓冲区溢出。
  • CONFIG_SYS_HEAP_HARDENING_MODERATE Level 2: 链表一致性检查。在操作空闲链表前,验证 prev->next == current 防止链表被非法篡改。
  • CONFIG_SYS_HEAP_HARDENING_FULL Level 3: 金丝雀 (Canaries) 机制。这是 Chunk 结构上最大的变化,可以较准确地检测 Heap 溢出。
  • CONFIG_SYS_HEAP_HARDENING_EXTREME Level 4: 全量自检。每次 alloc/free 都会调用 z_heap_full_check 线性扫描整个Heap。
加固级别 主要检查内容 Chunk 结构变化 性能/内存开销
BASIC 双重释放、简单缓冲区溢出(右邻居 LEFT_SIZE
MODERATE 空闲链表完整性、左邻居头部损坏(当前 LEFT_SIZE 较低
FULL 金丝雀检测缓冲区溢出、双重释放、空闲 Chunk 头部保护。 引入 CHUNK_TRAILER(金丝雀),增大 Chunk 最小尺寸 中等 (内存)
EXTREME 每次操作全Heap扫描,验证所有 Chunk 和链表完整性。 无额外结构变化 高 (性能)

BASIC Hardening 链接到标题

freesys_heap_free())和 reallocinplace_realloc())中启用低成本的双重释放(Double Free)和缓冲区溢出检测。在释放路径上增加少量字段读取,运行时开销可忽略不计,适用于需要安全保障的生产环境构建。

Double Free 链接到标题

sys_heap_free 函数中,利用 SIZE_AND_USED 字段的 used 位,检查待释放的 Chunk 是否已经被标记为空闲(!chunk_used(h, c))。如果 Chunk 已经被标记为空闲,则认为发生了双重释放(Double Free),系统将触发 k_panic()

	if (SYS_HEAP_HARDENING_BASIC && !chunk_used(h, c)) {
		LOG_ERR("heap corruption (double free?) at %p", mem);
		k_panic();
	}

Buffer Overflow 链接到标题

检查当前 Chunk c 的右侧邻居 right_chunk(h, c)LEFT_SIZE 字段是否正确地指向 c 的大小。如果 c 发生溢出,很可能会破坏 right_chunk(h, c)LEFT_SIZE 字段,导致 left_chunk(h, right_chunk(h, c)) != c 检查失败。

if (SYS_HEAP_HARDENING_BASIC &&
	    left_chunk(h, right_chunk(h, c)) != c) {
		LOG_ERR("heap corruption (buffer overflow?) at %p", ptr);
		k_panic();
	}

下图示意了如何检查双重释放(Double Free)和缓冲区溢出(Buffer Overflow)。 alt text

值得注意的是,双重释放(Double Free)只是检查 USE 标记,如果是前面的 Chunk 溢出(overflow)写到后面的 USE 标记,双重释放(Double Free)就会发生误判。对于溢出(Overflow)的检查,如果当前 Chunk 的 LEFT_SIZE 不能正确计算指向,说明当前 Chunk 被破坏(虚线箭头表示正确指向,实线箭头表示被破坏后的错误指向),但无法确认是前面的 Chunk 操作溢出导致。BASIC 的检查能确认存在问题,但问题原因具有怀疑性,而非确切性。

MODERATE Hardening 链接到标题

在 BASIC Hardening 的基础上,增加了对 Free list 完整性和左侧邻居 Chunk 头部完整性的检查。在分配和释放路径上增加少量字段读取。能有效检测大多数意外损坏,适用于需要健壮性的开发和生产系统的默认设置。

Free List 完整性检查 链接到标题

在从 Free list 移除 Chunk 或向其中添加 Chunk 时,利用空闲 Chunk 中的 FREE_PREVFREE_NEXT 字段验证链表节点的双向链接关系,例如 prev->next == currentcurrent->prev == prev。如果链表被篡改,这些检查将失败。

//添加 Chunk 时检查
        chunkid_t second = b->next;
		chunkid_t first = prev_free_chunk(h, second);

		if (SYS_HEAP_HARDENING_MODERATE &&
		    next_free_chunk(h, first) != second) {
			LOG_ERR("heap corruption (free list linkage)");
			k_panic();
		}

//移除 Chunk 时检查
        chunkid_t first = prev_free_chunk(h, c),
		second = next_free_chunk(h, c);

		if (SYS_HEAP_HARDENING_MODERATE &&
		    (next_free_chunk(h, first) != c ||
		     prev_free_chunk(h, second) != c)) {
			LOG_ERR("heap corruption (free list linkage)");
			k_panic();
		}

邻居 Chunk 头部完整性检测 链接到标题

sys_heap_free 中,检查当前 Chunk cLEFT_SIZE 字段是否正确。通过 right_chunk(h, left_chunk(h, c)) != c 来验证。这可以检测到由左侧 Chunk 溢出或当前 Chunk 下溢导致的 LEFT_SIZE 字段损坏。

	if (SYS_HEAP_HARDENING_MODERATE &&
	    right_chunk(h, left_chunk(h, c)) != c) {
		LOG_ERR("heap corruption (left neighbor?) at %p", mem);
		k_panic();
	}

    if (SYS_HEAP_HARDENING_MODERATE &&
	    (chunk_used(h, c) ||
	     left_chunk(h, right_chunk(h, c)) != c ||
	     right_chunk(h, left_chunk(h, c)) != c)) {
		LOG_ERR("heap corruption (free chunk linkage)");
		k_panic();
	}

下图示意了如何进行 Free list 和 Chunk 头部完整性检查,主要还是利用 Chunk 之间的链接关系,一旦存储链接关系的部位被破坏,就会触发检查失败。 alt text

FULL Hardening 链接到标题

MODERATE Hardening 的基础上,在每个 Chunk 的尾部添加金丝雀(Canaries)机制。在每个已分配的 Chunk 的末尾添加一个 CHUNK_TRAILER,其中包含一个 64 位的金丝雀值。这个金丝雀值是根据 Chunk 的地址和大小计算得出的。在 sys_heap_freesys_heap_usable_size 时,会重新计算并验证金丝雀值。如果值不匹配,则表明用户数据区发生了溢出,破坏了金丝雀。分为 4 步:

  • 生成compute_canary 使用 (地址 ^ 尺寸) ^ MAGIC_64BIT 生成一个与位置相关的 64 位校验值。
  • 植入:在已分配内存的末尾(Trailer)写入该值。
  • 校验:在 freerealloc 时,验证该值是否被篡改来判定是否溢出。
  • 毒化(Poisoning):释放内存时,将金丝雀设为 0xDEADBEEFDEADBEEF。如果在 free 时发现已经是这个值,直接判定为双重释放(Double Free)。

分配时植入

static inline uint64_t compute_canary(struct z_heap *h, chunkid_t c)
{
	uintptr_t addr = (uintptr_t)&chunk_buf(h)[c];
	chunksz_t size = chunk_size(h, c);

	return (addr ^ size) ^ HEAP_CANARY_MAGIC;
}

检查方式

static inline void verify_chunk_canary(struct z_heap *h, chunkid_t c, void *mem)
{
	uint64_t expected = compute_canary(h, c);
	uint64_t found = chunk_trailer(h, c)->canary;

	if (found != expected) {
		if (found == HEAP_CANARY_POISON) {
			LOG_ERR("heap corruption (double free?) at %p", mem);
		} else {
			LOG_ERR("heap canary: corruption at %p", mem);
		}
		k_panic();
	}
}

释放时毒化

static inline void poison_chunk_canary(struct z_heap *h, chunkid_t c)
{
	chunk_trailer(h, c)->canary = HEAP_CANARY_POISON;
}

下图示意了 allocfree Chunk 的金丝雀(Canary)机制。 alt text

EXTREME Hardening 链接到标题

EXTREME 级别是最高级别的加固,它在每次内存分配(sys_heap_alloc)和释放(sys_heap_free)操作时,都会执行一次全Heap完整性检查。因此开销极大(与内存分配次数呈线性关系)。此级别的开销性价比极低,不适用于生产环境,仅用于调试。

调用 z_heap_full_check(h) 函数线性遍历整个Heap,对每一个 Chunk 进行 valid_chunk 检查(包括大小、边界、相邻 Chunk 链接),并遍历所有空闲链表桶,验证其链接关系和 Chunk 计数。

if (SYS_HEAP_HARDENING_EXTREME && !z_heap_full_check(h)) {
		LOG_ERR("heap validation failed");
		k_panic();
	}

差异:为什么要引入 FULL Hardening 链接到标题

BASIC 和 MODERATE 两种方式已经可以检测到双重释放(Double Free)和缓冲区溢出(Buffer Overflow),但为什么还需要 FULL Hardening?

为方便说明,假定两个相邻的 Chunk A 和 Chunk B,Chunk A 是 Chunk B 的左 Chunk,物理地址在 Chunk B 前面。

区分“逻辑错误”与“内容篡改”:

  • BASIC 模式:单维度盲猜。仅通过 used 位判断,无法确定是逻辑上多调了一次 free,还是内存被改写意外将 used 位抹零。
  • MODERATE 模式:增强型盲猜。报错前会检查基本的邻居拓扑,如果拓扑也是乱的,会报(left neighbor?)。
  • Canary 模式:双维度断言。引入 HEAP_CANARY_POISON(0xDEADBEEF…)。CanaryPOISON:100% 确定为逻辑错误(双重释放)。Canary 为乱码:100% 确定为内容篡改(溢出/野指针写入)。

定位凶手:

  • BASIC 模式:受害者视角。释放 Chunk B 时发现 Chunk B 坏了,只能报 Chunk B 的错。
  • MODERATE 模式:邻居关联视角。当释放 Chunk B 时,会回溯 Chunk A,看 Chunk A 的长度字段是否能指回 Chunk B。如果指不回来,问题出在 Chunk B 的头部,通常是左邻居 Chunk A 溢出的结果。
  • Canary 模式:证据确凿。直接校验左邻居 Chunk A 的 Canary。即便 Chunk B 的头部暂时没坏,只要 Chunk A 的末尾(Canary)坏了就会报错。
诊断维度 BASIC (Level 1) MODERATE (Level 2) FULL (Canary/Level 3) 对调试的作用
真假判断 双重释放?(存疑) 双重释放?(存疑) 双重释放(确信) 区分是“逻辑重复调用”还是“内存被意外改写”。
凶手定位 指向受害者 Chunk B 指出“左邻居坏了” 明确指向施害者 Chunk A 从“知道 Chunk B 坏了”进步到“知道是 Chunk A 溢出踢到了 Chunk B”。
状态识别 块结构损坏 Heap管理链表损坏 块内容被篡改 区分是那种“致命的Heap系统崩溃”。
检查深度 单向(向前看) 双向(前后互查) 指纹校验(Magic) 降低因数据巧合导致的“漏报”概率。

不同示例在不同配置下的提示情况演示:

    void *p, *q;
    p = sys_heap_alloc(&heap, 15);
	q = sys_heap_alloc(&heap, 15);
	printf("p %p q %p\n", p, q);
	memset(p, 0, 13);
	sys_heap_free(&heap, q);
	sys_heap_free(&heap, p);
	sys_heap_free(&heap, p);
  • CONFIG_SYS_HEAP_HARDENING_MODERATE=yCONFIG_SYS_HEAP_HARDENING_BASIC=y

p 0x200000d4 q 0x200000ec [00:00:00.000,000] os_heap: heap corruption (double free?) at 0x200000d4

  • CONFIG_SYS_HEAP_HARDENING_FULL=y
    • 理论上应明确提示 <err> os_heap: heap canary: double free at 0x200000dc,但实际上是 <err> os_heap: heap corruption (double free?) at 0x200000dc。这表明当前的检查顺序存在问题,已提交 PR 修复(https://github.com/zephyrproject-rtos/zephyr/pull/108758),目前仍在审核中。
    void *p, *q;
    p = sys_heap_alloc(&heap, 15);
	q = sys_heap_alloc(&heap, 15);
	printf("p %p q %p\n", p, q);
	memset(p, 0, 21);
	sys_heap_free(&heap, q);
	sys_heap_free(&heap, p);
	sys_heap_free(&heap, p);
  • CONFIG_SYS_HEAP_HARDENING_MODERATE=y

p 0x200000d4 q 0x200000ec [00:00:00.000,000] os_heap: heap corruption (buffer overflow?) at 0x200000d4

  • CONFIG_SYS_HEAP_HARDENING_MODERATE=y

p 0x200000d4 q 0x200000ec [00:00:00.000,000] os_heap: heap corruption (left neighbor?) at 0x200000ec

  • CONFIG_SYS_HEAP_HARDENING_FULL=y

p 0x200000dc q 0x200000fc [00:00:00.000,000] os_heap: heap canary: corruption at 0x200000dc

    void *p, *q;
    p = sys_heap_alloc(&heap, 15);
	q = sys_heap_alloc(&heap, 15);
	printf("p %p q %p\n", p, q);
	memset(p, 0, 24);
	sys_heap_free(&heap, q);
	sys_heap_free(&heap, p);
	sys_heap_free(&heap, p);
  • CONFIG_SYS_HEAP_HARDENING_MODERATE=yCONFIG_SYS_HEAP_HARDENING_BASIC=y

p 0x200000d4 q 0x200000ec [00:00:00.000,000] os_heap: heap corruption (double free?) at 0x200000ec

可以看到溢出被推测为双重释放(Double Free),这就是不准确的表现。

  • CONFIG_SYS_HEAP_HARDENING_FULL=y

p 0x200000dc q 0x200000fc [00:00:00.000,000] os_heap: heap canary: corruption at 0x200000dc

作用范围 链接到标题

Zephyr heap 加固的作用是尽早检测并定位Memory corruption问题(如越界、非法释放),把“随机崩溃”变成“可小范围定位崩溃”,局限是它无法像ASAN那样及时且准确的发现 Memory corruption 根因。