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_bytes、allocated_bytes和max_allocated_bytes,不再需要线性扫描来获取剩余空间。 - Heap Listener: 引入了回调机制。当内存分配、释放或 Heap 扩容时,可以通知外部模块(如内存分析工具)。
新增加固机制 链接到标题
Zephyr 4.4 引入 Heap 分级加固 (Hardening Levels),通过 CONFIG_SYS_HEAP_HARDENING_LEVEL 定义了五个等级:
CONFIG_SYS_HEAP_HARDENING_NONELevel 0: 无检查,原本的 Heap 管理。CONFIG_SYS_HEAP_HARDENING_BASICLevel 1: 基础检查。能识别简单的双重释放(Double Free)和通过相邻 Chunk 校验捕获的缓冲区溢出。CONFIG_SYS_HEAP_HARDENING_MODERATELevel 2: 链表一致性检查。在操作空闲链表前,验证prev->next == current防止链表被非法篡改。CONFIG_SYS_HEAP_HARDENING_FULLLevel 3: 金丝雀 (Canaries) 机制。这是 Chunk 结构上最大的变化,可以较准确地检测 Heap 溢出。CONFIG_SYS_HEAP_HARDENING_EXTREMELevel 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 链接到标题
在 free(sys_heap_free())和 realloc(inplace_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)。

值得注意的是,双重释放(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_PREV 和 FREE_NEXT 字段验证链表节点的双向链接关系,例如 prev->next == current 和 current->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 c 的 LEFT_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 之间的链接关系,一旦存储链接关系的部位被破坏,就会触发检查失败。

FULL Hardening 链接到标题
MODERATE Hardening 的基础上,在每个 Chunk 的尾部添加金丝雀(Canaries)机制。在每个已分配的 Chunk 的末尾添加一个 CHUNK_TRAILER,其中包含一个 64 位的金丝雀值。这个金丝雀值是根据 Chunk 的地址和大小计算得出的。在 sys_heap_free 或 sys_heap_usable_size 时,会重新计算并验证金丝雀值。如果值不匹配,则表明用户数据区发生了溢出,破坏了金丝雀。分为 4 步:
- 生成:
compute_canary使用(地址 ^ 尺寸) ^ MAGIC_64BIT生成一个与位置相关的 64 位校验值。 - 植入:在已分配内存的末尾(Trailer)写入该值。
- 校验:在
free或realloc时,验证该值是否被篡改来判定是否溢出。 - 毒化(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;
}
下图示意了 alloc 和 free Chunk 的金丝雀(Canary)机制。

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…)。Canary为POISON: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=y和CONFIG_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=y和CONFIG_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 根因。