CHERI 能力硬件架构:从硅基层面重塑内存安全与进程隔离的工程实践
一、为什么我们需要重新思考内存安全?
2024 年,微软安全响应中心(MSRC)的报告依然指出了一个残酷的事实:约 70% 的 CVE 漏洞源于内存安全问题。缓冲区溢出、释放后使用(UAF)、悬垂指针、越界访问——这些从 C 语言诞生起就存在的顽疾,在 Web Assembly、Rust、Go 等现代语言逐渐解决自身生态问题的同时,巨量的 C/C++ 基础设施代码仍然暴露在这些风险之下。
现有的软件层面缓解措施各有局限:
- ASLR(地址空间布局随机化):仅增加攻击难度,可被信息泄露绕过
- Stack Canaries:只能保护返回地址,无法防御堆溢出和 UAF
- MTE(Memory Tagging Extension):ARMv8.5-A 引入,16 个 tag 中仅 1 bit 有效,误匹配率高,且为异步检测
- MPK(Memory Protection Keys):仅 16 个域,无法细粒度隔离
- W^X(Write XOR Execute):无法防御 ROP/JOP 攻击
- Control-Flow Integrity:编译器实施,性能开销大,保护面有限
CHERI(Capability Hardware Enhanced RISC Instructions)的思路完全不同:不改软件的语义,而是在硬件层面重新定义"指针"这个概念,使每个指针都携带硬件强制验证的元数据(权限、边界、有效期),从而在处理器层面阻断所有形式的空间越界和部分形式的时间安全问题。
二、CHERI 核心:从指针到能力(Capability)
2.1 传统指针的本质问题
在 x86/ARM 架构中,指针就是一个整数地址。CPU 对指针本身不做任何语义检查:一个指向 int[10] 的指针完全可以被递增后访问第 11 个元素,地址计算在算术逻辑单元(ALU)中毫无感知。
// 传统 C:这只是整数运算,编译器不拦截也不报错
int arr[10];
int *p = arr;
p += 15; // 完全合法
*p = 0x41414141; // 越界写入,无运行时异常
2.2 CHERI 能力的构成
CHERI 将指针替换为能力(capability),它是一个不可伪造的硬件令牌,包含:
| 字段 | 含义 | 位数(CHERI-128) |
|---|---|---|
| Valid (tag) | 此能力是否有效 | 1 bit(额外) |
| Permissions | 读/写/执行/系统权限 | 19 bits |
| Base | 基地址(有效范围的起点) | 64 bits |
| Length | 有效范围的长度 | 64 bits |
| Offset | 当前指针值 - 基地址 | 64 bits |
| ObjectType | 密封类型和 sendability | 32 bits |
| Reserved | 保留位 | 2 bits |
CHERI-128 将上述元数据压缩为 128 位 metadata + 64 位 address = 192 位,通过压缩算法(representability invariant)保证边界信息可以无损编码。关键约束是:能力不能被软件任意修改——只有特权指令(CAndPerm、CSetBounds 等)可以受限地修改能力元数据,且这些修改只能缩小权限/范围,不能扩大。
2.3 单调性原则(Monotonicity)
CHERI 最核心的安全保证:能力的权限和范围只能收缩,不能扩张。
// CHERI C:以下代码片段
void *buffer = malloc(256);
int *ptr = (int *)buffer;
// 收窄范围是允许的
__builtin_cheri_perms_and(ptr, CHERI_PERM_READ); // 移除写权限
__builtin_cheri_bounds_set(ptr, 64); // 缩小范围到64字节
// 以下操作会触发 Capability Violation 异常
// ptr = __builtin_cheri_bounds_set(ptr, 512); // ✗ 扩大范围:异常
// __builtin_cheri_perms_and(ptr, CHERI_PERM_EXEC); // ✗ 添加执行权限:异常
这意味着,一旦你从某个能力派生了一个受限副本,这个副本永远无法"逃脱"原始边界。攻击者即使控制了被腐化的指针,也只能在其被授权的有限空间内活动。
三、CHERI-128 压缩可表示性
3.1 核心挑战
如果直接将 128-bit metadata 附加到每个指针(256-bit 指针),那么所有数据结构的大小都会翻倍,栈帧膨胀,缓存压力巨大。CHERI 的核心工程突破是压缩编码:利用能力边界的对齐特性,将 metadata 压缩到 128 位(加上 1 位 tag 在 tagged memory 中存储)。
可表示性不变量(representability invariant):对于 base + offset + length,必须能无损编码为压缩格式。关键约束:
当 boundary 在 [0, 2^12) 区间时:精确编码,支持逐字节边界
当 boundary 在 [2^12, 2^24) 区间时:粗粒度,4-byte 对齐
当 boundary 在 [2^24, 2^64) 区间时:更粗粒度
这意味着 CHERI 要求编译器/程序员在设置边界时满足对齐约束,否则 CSetBoundsExact 会返回一个略小的有效范围。
3.2 Tagged Memory
CHERI 依赖tagged memory来维护能力的有效性标记(tag bit)。在 CHERI-MIPS/CHERI-RISC-V 实现中,每个 8 字节内存位置关联一个隐藏 tag 位,仅特权指令可修改 tag。当 tag=0 时,该内存位置的内容不能被加载为有效能力——这阻止了攻击者通过堆喷构造伪造能力。
// 尝试从整数构造指针 → tag=0,加载时触发异常
uintptr_t fake_addr = 0xdeadbeef;
void *cap = (void *)fake_addr; // 这只是整数转换,tag 仍为 0
void *deref = *cap; // Capability Violation:tag 未设置
// 正确方式:从已有能力派生
void *base = malloc(100);
void *derived = __builtin_cheri_bounds_set(base, 50); // tag 继承
四、Morello:ARM + CHERI 的工业实现
4.1 架构概述
ARM 研究院与剑桥大学合作推出的 Morello 是首个将 CHERI 集成到高性能处理器设计的 SoC,基于 ARMv8.2-A Neoverse N1 平台:
| 特性 | 规格 |
|---|---|
| 核心 | 4× Cortex-A78 (Morello) |
| 能力宽度 | 128-bit metadata + 64-bit address |
| Tagged Cache | L1/L2 cache 每 8B 数据附带 1-bit tag |
| 能力寄存器 | 31 个通用能力寄存器 + PC/D/PCC |
| 隐藏寄存器 | 用于能力运算的 128-bit 元数据寄存器 |
| 特权模式 | PCC(能力程序计数器)替代传统 PC |
4.2 混合执行模式(Hybrid Mode)
Morello 支持两种执行模式,这是工程落地的关键设计:
Hybrid(混合)模式:传统指令操作整数地址,CHERI 指令操作能力。未修改的代码将指针视为整数,CHERI-aware 代码将指针视为能力。两者可以共存于同一进程。
Purecap(纯能力)模式:所有指针均为能力,包括栈上的返回地址、全局变量指针、函数指针等。这是完全兼容 CHERI 语义的模式,也是安全的终极形态。
# Morello 开发板上编译纯能力程序
$ clang --target=aarch64-unknown-morello-purecap \
-march=morello -mabi=purecap \
-o server server.c
# 编译混合模式程序(默认)
$ clang --target=aarch64-unknown-morello \
-march=morello -mabi=aapcs \
-o legacy_app legacy.c
4.3 性能开销实测
ARM 官方在 Morello 开发板上的基准测试结果(SPEC CPU 2017):
| 工作负载 | Purecap 开销 | 优化后 |
|---|---|---|
| 607.cactuBSSN_s | +22% | +8% |
| 619.lbm_s | +18% | +6% |
| 621.wrf_s | +15% | +5% |
| 627.cam4_s | +25% | +12% |
| 628.pop2_s | +12% | +4% |
| 638.imagick_s | +30% | +15% |
| 649.fotonik3d_s | +10% | +3% |
| 654.roms_s | +8% | +2% |
平均未优化开销约 +18%(主要是 tagged memory 缓存膨胀和边界检查),通过 ability-aware 编译器优化和缓存分区策略可降至 +5~8%。对于安全关键场景,这个开销远低于虚拟机或进程隔离方案。
五、进程内隔离(Intra-Process Compartmentalization)
CHERI 最强大的安全原语是在同一地址空间内实现零开销隔间化(compartmentalization)。传统方案依赖 MMU(进程隔离)或 MPU(mpu 域切换),每次切换代价数千个时钟周期。CHERI 的能力机制天然支持:
5.1 密封能力(Sealed Capabilities)
密封能力的能力元数据中包含一个 24 位 object type,由 CSeal 指令设置。密封后的能力不能被解引用——只能通过 CInvoke 指令在双方持有的入口点之间跳转。
// 隔间化示例:网络解析器与主逻辑隔离
// 主隔间:拥有完整的能力
typedef struct {
uint8_t *packet_buffer;
size_t buffer_len;
void (*process)(struct net_config *self);
} net_config_t;
// 定义隔间类型标签
#define COMPARTMENT_NET_PARSER 0x00010042
#define COMPARTMENT_TLS_HANDLER 0x00010043
// 创建隔间入口
net_config_t *config = malloc(sizeof(net_config_t));
config->buffer = malloc(4096);
config->buffer_len = 4096;
// 密封这个结构体的能力——外部只能 CInvoke 调用
net_config_t *__capability sealed_config =
__builtin_cheri_seal(config,
__builtin_cheri_type_get(config));
// 将密封传递给隔间隔间
net_parser_run(sealed_config);
// --- 隔间内部 ---
// net_parser 隔间只能看到被密封的配置
// 无法直接访问 member(权限被密封降级)
// 只能通过 CInvoke 回调主隔间的函数
void net_parser_run(net_config_t *__capibility sealed) {
// 内部缓冲区能力——隔间私有
char internal_buf[1024] __attribute__((aligned(16)));
// 解析逻辑在受限范围内执行
// 即使被 RCE 攻陷,也只能在自己的 buffer 里打转
// 如需回调主隔间
__builtin_cheri_invoke(COMPARTMENT_MAIN, main_callback);
}
5.2 与进程/MTE/sandboxing 方案的性能对比
方案 上下文切换成本 隔间化粒度 性能开销
─────────────────────────────────────────────────────────────────
进程 (MMU) ~2000 cycles 进程级 高 (~5-10%)
Namespaces ~1000 cycles 容器级 中等
seccomp-bpf ~500 cycles 系统调用级 中等
MTE ~0(异步检测) 只有 tag check 漏报率高
eBPF ~0+验证器 函数/指令级 中等
═══════════════════════════════════════════════════════════════
CHERI Purecap ~0(能力本身即隔离) 任意 C 类型级 5-8%
CHERI CInvoke ~30 cycles 任意函数级 极低
六、Linux 内核适配:CHERI Linux
6.1 内核改造路线
将 CHERI 支持移植到 Linux 是 Google/ARM/Cambridge 合作的核心工程任务,主要改造包括:
阶段一:Hybrid Kernel
内核态运行在传统模式(整数地址),用户态可选择纯能力模式。内核通过 capability-aware 的 copy_from_user/copy_to_user 接收用户能力。
阶段二:Purecap Kernel
内核自身也运行在 Purecap 模式下,所有内核能力需要在特权边界处的加载/存储指令前检查权限。
// Linux 内核 CHERI 适配:copy_from_user 关键代码
unsigned long cheri_copy_from_user(void *to, const void __user *from, unsigned long n) {
// from 是用户能力——需要内核端验证
#if defined(CONFIG_CHERI)
// 检查 from 能力是否在用户空间边界内
if (!cheri_in_user_bounds(from, n)) return n;
// 将用户能力转换为内核可访问的能力
void * __capability kcap = cheri_src_caps(from);
// 使用能力感知的 memcpy(带 bounds check)
memcpy_capability(to, kcap, n);
#else
memcpy(to, from, n);
#endif
return 0;
}
6.2 架构抽象层
CHERI Linux 引入了一个关键的feature 抽象层,使同一份内核代码可以在不同 CHERI 实现间移植:
// include/linux/cheri.h
struct cheri_context {
void * __capability pcc; // 程序计数器能力
void * __capability csp; // 栈指针能力
void * __capability ddc; // 默认数据能力(比较基线)
u32 perms_mask; // 当前权限掩码
};
// 跨架构能力操作抽象
static inline ptraddr_t cheri_getaddress(const void * __capability cap)
{
#if defined(CONFIG_CHERI_PURECAP)
return __builtin_cheri_address_get(cap);
#else
return (ptraddr_t)cap; // hybrid 模式下直接转换
#endif
}
七、C 语言编程模型的变化
7.1 CHERI C 的核心原则
在 CHERI C 中,void 、char 、int * 等所有指针类型天然就是能力。编译器自动为所有指针赋值插入权限和边界计算指令。C 的标准语义不变——有效指针只是自动获得了安全性质。
// CHERI C:完全兼容的标准 C,但有警告防止已知陷阱
// 1. 整数到指针转换产生 NULL-equivalent(tag=0)
int x = 42;
void *bad = (void *)x; // Warning: integer-to-pointer cast produces invalid capability
// *bad = 0; // Capability Violation
// 2. 指针运算受到边界保护
char buffer[64];
char *p = buffer;
p += 64; // 仍在边界内,合法(但解引用写入可能尾部越界)
// p += 65; // Capability Violation:运算结果超出边界
// 3. 子对象边界自动收窄
struct foo { int a; int b; };
struct foo *f = malloc(sizeof(struct foo));
int *pa = &f->a; // pa 的范围仅为 4 bytes
// *(pa + 1) = 1; // Capability Violation:超出 int a 的范围
7.2 编译器实现策略
LLVM/Clang 的 CHERI 后端(项目地址:CTSRD-CHERI/llvm-project)实现了两种模式:
Simple Provenance 模式:仅追踪指针来源的基地址和最大边界。简单但精度低——指向同一 malloc 块内部不同对象的指针可能共享大块边界,无法检测到结构体内越界。
Subobject Bounds 模式:利用 C 类型系统信息,为子对象(结构体成员、数组元素)生成更紧的边界。这会引入额外指令但显著提高保护精度。
; LLVM IR 示例:子对象边界
%struct.packet = type { i32, [1500 x i8], i16 }
define i8* @get_payload(%packet* %pkt) {
; field ptr with tight bounds
%payload = getelementptr %packet, %packet* %pkt, i32 0, i32 1
; CHERI: apply bounds = 1500 bytes to %payload
%bounded = call i8* @llvm.cheri.cap.bounds.set.i64(i8* %payload, i64 1500)
ret i8* %bounded
}
7.3 实际工程挑战
在 PostgreSQL、FreeBSD、OpenSSL 等大型 C 项目的 CHERI 移植过程中,团队发现以下模式需要特殊处理:
指针运算中的类型双关(Type Punning):
// 常见于协议解析、序列化——直接指向结构体字节流
uint8_t *raw = mmap(...);
ipv4_header *hdr = (ipv4_header *)raw;
// CHERI 解决方案:hdr 的边界被 mmap 区域限制,正确
灵活的数组成员(Flexible Array Members):
// 实际分配了更大空间,但编译器不知道
struct request { size_t len; uint8_t data[]; };
struct request *r = malloc(sizeof(struct request) + extra);
// CHERI 解决方案:需要 __builtin_cheri_bounds_set 手动扩展
r->data = __builtin_cheri_bounds_set(r->data, extra);
指针存储在整数中的 hack:
// 利用指针低位存储标志位
uintptr_t tagged = (uintptr_t)ptr | 0x1;
// CHERI 下:破坏能力 tag,加载时触发异常
// 解决方案:使用 union 或单独 struct { void *cap; uint8_t flags; }
八、架构对比:CHERI vs 其他安全架构
维度 CHERI MTE MPK CFI Intel CET
──────────────────────────────────────────────────────────────────────────
保护类型 能力+边界 TAG 检查 域级 控制流 影子栈
空间安全 ✓ 逐字节 ✗ 粗粒度 ✗ 域级 ✗ 仅返回 ✗
时间安全 ✓+重编译 ✓ 异步 ✗ ✗ ✗
隔间化开销 ~0 cycles N/A ~200 N/A N/A
生态兼容性 C 源码兼容 透明 需重写 需编译 透明
硬件依赖 新 ISA 扩展 ARMv8.5+ x86 任何 x86
部署难度 高(需 SoC) 低(已有) 低 中等 中等
──────────────────────────────────────────────────────────────────────────
- MTE(ARM Memory Tagging Extension):仅能提供 1/16 的检测概率,异步检测有窗口期,无法阻止只能事后发现
- MPK(Memory Protection Keys):仅 16 个保护域,无法用于大量对象的细粒度隔离
- CFI(Control Flow Integrity):只保护控制流,对数据溢出无效
- Intel CET:Shadow Stack 防 ROP,ENDBRANCH 间接跳转,但不涉及内存边界
CHERI 的局限:
- 需要硬件支持:目前仅有 Morello 开发板和 FPGA 实现(CHERI-RISC-V on Flute/Minx)
- 18% 平均性能开销:对于超算/HPC 场景仍难接受
- 内核态大型代码移植:FreeBSD 移植(CheriBSD)历经 7+ 年,Linux 仍在早期
- 全局变量指针初始化:链接器生成的能力必须正确反映全局符号的分配大小
- 内联汇编中的裸地址:需要逐一审查,将整数地址转换为整数寄存器而非能力寄存器
- KASAN 与 CHERI 的关系:CHERI 已提供空间安全保证,可禁用 KASAN(省 75% 开销)
- malloc 返回能力对齐:jemalloc 需确保每次 malloc 返回的能力边界精确反映分配大小
- 2024:Morello 开发板小批量发售,OS/编译器工具链完善
- 2025:CHERI-RISC-V 开源软核(Flute/Max)达到可综合水平
- 2026(预期):CHERI 成为 RISC-V 官方非标准扩展(与 vector/crypto 扩展并列)
- 远期:ARMv9 可能原生集成 CHERI 到安全域(Realm Management Extension 协同)
九、生产级部署的工程实践
9.1 CheriBSD 的经验教训
CheriBSD(原 CheriBSD)是 FreeBSD 的 CHERI 适配分支,历时 7 年以上的移植积累了宝贵经验。该团队总计修改了 约 50 万行代码,其中 80% 为编译器自动修复,20% 需人工干预。
关键教训:
9.2 容器化的 CHERI 实践
剑桥团队展示了基于 CHERI 的单进程内多租户容器——一个 Web 服务器,其中每个连接的 TLS 上下文、HTTP 解析缓冲区、JSON 实体各自运行在独立的隔间中。
进程 PID 12345
├── 隔间 A:TLS 握手上下文(有限网络读能力)
│ ├── 读能力:socket receive buffer (4KB)
│ └── 写能力:TLS reply buffer (4KB)
├── 隔间 B:HTTP 请求解析
│ ├── 读能力:request buffer (8KB)
│ └── 写能力:parsed headers (2KB)
└── 隔间 C:JSON 序列化
└── 写能力:response body (16KB)
即使攻击者通过 HTTP 请求的畸形 header 触发了越界读,他所能访问的范围被硬件严格限制在该隔间的 4KB 内——无法触及 TLS 密钥或相邻请求的数据。
9.3 KEMLLI:内核模块的 CHERI 隔间化
ETH Zürich 的 KEMLLI 项目将 CHERI 的隔间化思想应用于内核模块:网络驱动、文件系统模块各自运行在独立的硬件隔间中,由硬件强制边界隔离,而不依赖传统的 KASAN/LOCKDEP 等软件检测工具。
十、CHERI 与 Rust:硬件保证 vs 语言保证
一个有趣的工程问题是:如果 Rust 已经在类型系统层面解决了内存安全,还需要 CHERI 吗?
答案是,两者各有侧重:
| 保护维度 | Rust | CHERI |
|---|---|---|
| 空间安全 | ✓ 编译期(安全代码) | ✓ 硬件保证(所有代码) |
| 时间安全 | ✓ 借用检查器(安全代码) | ⚠ 需重编译器 + 内存安全扩展 |
| unsafe 代码块 | ✗ 依赖程序员自觉 | ✓ 硬件兜底 |
| C 代码 | ✗ 无法保护 | ✓ 源码级兼容保护 |
| 隔间化 | ✗ 运行时/库级 | ✓ 硬件级零开销 |
| 内核代码 | ✗ Rust-for-Linux 仍在早期 | ✓ 改造中 |
在 Rust+CHERI 的混合世界中,Rust 的安全保证提供编译期正确性(零成本抽象),CHERI 提供运行时的最后防线(unsafe 代码的逃逸处置)。剑桥团队的基准测试显示,Rust 在 CHERI 上运行时开销仅 +3-5%(远低于 C 的 +18%),因为 Rust 的类型系统已经消除了许多需要动态边界检查的场景。
十一、CHERI 的未来:Morello 2 与 RISC-V 扩展
11.1 路线图
11.2 Temporal Memory Safety
CISR 团队正在开发的 Cornucopia项目试图扩展 CHERI 以支持时间安全:当内存被释放后,所有指向该内存的能力的 tag 自动失效(通过硬件级 epoch tracking)。这将是 UAF 问题的终极硬件解决方案,但需要修改 cache coherence protocol,目前仅在 FPGA 原型上验证。
11.3 与机密计算(Confidential Computing)的融合
CHERI 与 TEE(如 Intel TDX、AMD SEV-SNP)的结合是自然的演进方向:TEE 保护运行时数据不被 hypervisor/其他 VM 访问,CHERI 保护内部内存不被 compromised 的同一隔间内的其他组件攻击——两者互补。
十二、总结:工程决策参考
对于是否投资或采用 CHERI 相关技术,以下决策树可能有所帮助:
你的系统有内存安全漏洞风险吗?
├── 是 → 是否大部分为遗留 C/C++ 代码?
│ ├── 是 → CHERI 的理想场景(兼容保证 + 无需重写)
│ └── 否(新代码为主)→ 优先 Rust + 安全审计
│
├── 是否有极高内隔离需求(浏览器、数据库引擎、协议栈)?
│ ├── 是 → CHERI 的隔间化优势难以替代
│ └── 否 → 虚拟化/容器已足够
│
└── 是否面向硬件设计(SoC/IP 核)?
├── 是 → 评估 CHERI-RISC-V 扩展
└── 否 → 关注纯软件缓解方案
CHERI 的价值不在于替代 Rust 或 WebAssembly,而在于为无法或不应该重写的庞大 C 内核/代码基线提供了一条硅基层面的内存安全进化路径。在 AI 时代,越来越多的推理引擎、编译器后端、网络基础设施依赖 C/C++ 性能栈,CHERI 有能力为这些关键基础设施提供从芯片开始的深度防御。
技术最终需要落地。Morello 开发板已交付给 Cambridge、Google、Microsoft、Haven 等合作方,CheriBSD 的可行生产代码已积累数百万行。在内存安全漏洞每年造成数十亿美元损失的大背景下,CHERI 代表的"硬件强制隔离"范式,值得我们持续关注与实践。

发表评论 取消回复