
1. 项目概述CHERI是什么它想解决什么先聊点大家都有共鸣的。写C/C这么多年谁没被内存安全坑过缓冲区溢出、空指针解引用、释放后使用use-after-free、内存泄漏这些词几乎是C/C程序员的日常梦魇。我们习惯性地开ASan、开UBSan、开静态分析工具试图在开发阶段抓出那些潜伏很深的内存问题。但问题是这些工具都是运行时检测只能在问题发生之后报错并不能真正阻止恶意程序利用这些问题突破系统防线。而且一旦代码发布到生产环境ASan就不可能再开启——性能开销太大生产环境跑不动。CHERICapability Hardware Enhanced RISC Instructions就是一个从硬件层面重新定义“指针”的项目目标是从根源上让C/C的内存安全漏洞变得难以利用。它不像ASan那样是软件补丁也不像传统的ASLR/NX那样只是提高攻击难度而是直接在CPU的指令集架构里加入了一种叫做“能力”capability的硬件对象把原来单纯的指针升级成带边界的、带权限的、不可伪造的硬件对象。简单来说CHERI做的事情可以类比为过去的指针是一把不带锁的钥匙谁捡到就能开任何门CHERI下的指针是一张有照片、有有效期、有门禁权限的工牌不但要验证你本人的身份指针完整性还要看你有权进入哪个房间边界和权限而且这张工牌本身不可复制、不可篡改。这篇文章不是学术界那种论文复述而是从一个普通C/C开发者的角度把CHERI拆开来看它怎么工作、能防什么不能防什么、怎么跑起来、在真实项目里怎么用。对系统编程、嵌入式、网络服务、内核开发感兴趣的朋友这篇文章应该能帮你省下啃论文的时间和精力。2. 核心思路拆解为什么C/C的内存安全问题这么难治2.1 问题的根源在语言“太自由”很多人觉得内存安全问题写代码时小心点能避免但实际做过大型项目的都知道这是不可能完成的任务。C/C的指针本质上就是一个整数——它存放的是某个内存地址。CPU在执行*ptr value这类指令时只关心这个地址是否对齐、是否可写完全不关心这个地址是不是“属于”这个指针的对象。这就导致了一个根本性的不对称程序员在语义层面认为指针是对某个对象的引用但硬件在物理层面只看到一个裸地址。这种语义和物理层面的断裂就是C/C内存安全漏洞的根源。缓冲区溢出、越界读写、悬垂指针本质上都是“指针无法被验证是否仍然有效”导致的问题。用一个通俗的比喻你在停车场停车时保安给你一张写有“车位A3”的小纸条。纸条本身没有防伪标识也没有有效期。攻击者完全可以把这个纸条改成“车位B7”或者捡起别人丢掉的纸条照着描一张。停车场系统无法辨别这个纸条是否真的属于你、现在是否仍然有效。传统安全方案的本质都是在事后弥补这个漏洞——ASan是在每次读写前检查地址是否落在合适的范围内ASLR是试图让攻击者猜不准地址指针加密如ARM的PAC是试图让指针被改后程序崩溃。但这些都是“打补丁”的思路没有改变一个核心事实CPU仍然把指针当裸地址来处理。2.2 CHERI的核心启示把指针从“地址”升级为“能力”CHERI的思路和传统方案完全不是一回事。它不尝试去修补那些漏洞而是从CPU层直接把指针的语义升级了。在CHERI的架构下每一个指针不再是一个裸的64位整数而是一个128位的capability能力。多出来的64位携带了丰富的元数据包括边界bounds这个指针能够合法访问的内存范围下限和上限。权限permissions这个指针允许执行的操作读、写、执行、加载/存储能力等。tag标记一个1位的硬件标志标识这个能力对象是否有效、是否被篡改过。对象类型object type用于软件定义的隔离域比如把不同子系统的能力区分开。当程序执行*ptr value时CPU会自动检查这个pointer的capability是否仍然合法访问的地址是否在bounds内是否拥有写权限。如果检查不通过CPU会触发一个硬件异常程序直接崩溃而不是像过去那样继续执行下去或者更糟——被攻击者利用来改写内存。这个机制第一次让CPU具备了“验证指针语义合法性”的能力。你会发现它天然就防了下面这些攻击缓冲区溢出ptr[10]越界了CPU检查bounds后直接报错。use-after-free释放内存后capability中记录的边界和权限被失效再访问就触发异常。指针伪造tag位防止了把普通数据伪装成capability没有合法的tag即使你恶意构造了一个看起来合理的128位能力CPU也不认。2.3 和传统防御技术做个直观对比为了把CHERI的位置放清楚我整理了一张对比表把当前常见的内存安全防御机制和CHERI放在一起看方案机制部署形态能阻止越界读写能阻止UAF性能开销生产可用性ASan编译器插桩运行时检查开发测试阶段能部分2x-4x不适合生产ASLR/NX地址随机化、不可执行页操作系统/编译器不能只提高难度不能极低一直开启Pointer Authentication (PAC)对指针签名防篡改硬件编译器部分防改指针本身不能较低现代Arm CPUMemory Tagging (MTE)内存地址附带随机标签访问时匹配硬件编译器能概率性能下代较低正在落地CHERI指针升级为能力对象硬件强制执行CPU架构OS编译器能确定性能下代中等纯cap模式实验到商用过渡期CHERI最大的优势是它的检查是确定性的不是概率性的。MTE的标签是随机分配的访问时理论上存在标签碰撞的可能性虽然概率极低但不是数学意义上的不可绕过。CHERI的能力模型则是直接承载了完整的边界和权限信息硬件层面不允许任何绕过路径。3. 核心细节解析CHERI的能力模型是怎么运作的3.1 地址、指针和能力从裸地址到带保险的引用要想真正理解CHERI你得先把“指针”这个概念做一个认知重载。在我们熟悉的64位平台上一个普通的指针占8字节CPU直接拿它的值去寻址。在CHERI架构中指针仍然是8字节的地址部分但当代码在编译时开启了CHERI支持即编译为capability-aware代码编译器会把每个指针变成128位的capability。前64位是原有的地址值后64位存储的是元数据包括边界、权限、类型和其他标志位。但关键在于CHERI不只是简单地把指针变宽它还给每个capability配了一位tag位。tag位存储在物理内存的某处实际实现中在DRAM中占用额外空间或者在cache中直接携带它标志这个capability是否“有效”。举例来说当你从内存中加载一个capability到寄存器时如果这个capability的tag位是1那么它是可以被信任的CPU会把它当作一个合法的能力对象使用。如果tag位是0那么这个数据只是普通的数据不具备能力语义。这意味着什么意味着无法通过写内存的方式“伪造”一个capability。攻击者无法通过溢出把内存中的capability数据改掉再指望CPU继续执行因为改掉的数据的tag位会变成0硬件直接不认。这是CHERI防攻击的核心逻辑能力对象只能在创建时由合法指令生成比如编译器生成的指令不能由普通的数据写操作伪造出来。3.2 边界、权限和tag三个最核心的属性为了让你在实操中能准确理解CHERI的报错信息我把三个属性的作用分别展开说一下它们对应着非常具体的安全检查。边界bounds是capability的“活动范围”。每个capability都携带一个下界和上界硬件在每次内存访问时都会检查地址是否落在 [base, baselength) 区间内。如果有任何访问越过了这个区间CPU就会触发capability异常。这个检查是在一个CPU周期内完成的不存在软件开销。权限permissions告诉CPU这个capability允许执行哪些操作。常见权限位包括加载权限、存储权限、执行权限、加载capability权限、存储capability权限等。比如一个指向只读字符串的capability其存储权限位是0那么任何写入操作都会被硬件拒绝即使通过强制类型转换也没用——因为转换后的capability仍然携带原来的权限位。tag位则是capability的“防伪标识”。前面提到过tag位存储在物理内存中任何非能力对象的写入操作都会清除目标位置已经存储的capability的tag。举个例子如果你把一个有效的capability存入内存然后对这个内存区域执行一次普通的字节写操作哪怕这个写操作只改变了1个字节capability的tag也会被清零从此这个对象不再是合法能力。这就保证了“不能通过改写内存来篡改capability”因为任何改写都会让它失效。3.3 纯capability模式与混合模式两种工作方式CHERI有两种主要的工作模式理解这两种模式是上手的第一个关键点。纯capabilitypurecap模式在这种模式下整个程序的所有指针都升级为capability。这包括栈指针、全局变量指针、函数指针、结构体成员指针甚至malloc返回的指针。编译器在生成代码时会适配新的ABI所有指针操作都使用capability指令。这种模式的防护最全面但要求代码和依赖库都要按CHERI ABI重新编译。混合hybrid模式在这种模式下CPU仍然支持传统的非capability指令但允许程序员通过编译器扩展在关键位置显式地创建和使用capability。比如你可以在某个模块中创建一个带权限约束的capability只把它传递给那些应该被允许访问某个内存区域的代码。这为渐进式迁移提供了可能不需要一次性把整个应用的重重链路全部改造。实际操作中纯capability模式的保护效果最好但对生态的要求也最高。如果你的项目依赖某个闭源库或者没有源码的旧库那这个库无法按CHERI ABI重编你就会被迫使用混合模式或者直接放弃。而混合模式则相当于在原有的内存模型之上叠加了可选的防护层风险控制更灵活但相应地也增加了心智负担和代码复杂度。3.4 为什么这么设计从“信任地址”转向“信任能力”以前的安全模型建立在“信任地址、不信任内容”的假设之上只要你拿到了一个地址你就默认可以访问它只要权限位允许CPU不区分这个地址是你合法获得的还是攻击者猜出来的。CHERI把这个模型翻转了只有拥有合法能力对象才能访问对应的内存区域。能力对象本身是一个“句柄”而不是一个可以随意计算的数值。虽然capability的地址部分仍然是一个数值但它的有效性和范围约束由硬件通过tag位来保证你无法脱离capability环境单独构造一个能力。这有点像“饭店会员卡”过去的做菜流程是所有桌位都用同一个开放式后厨谁都能直接走到灶台前动手模拟传统指针的强语义而CHERI是给每张餐桌配一个传菜通道只有持有对应餐桌号卡片的传菜员才能进入后厨而且这张卡伪造不了。这种设计带来的最直接收益是即使攻击者成功造成了内存破坏例如缓冲区溢出他得到的权限也只是该缓冲区capability的权限而不是进程的整个内存空间。威胁模型从“攻破一个漏洞获得全部控制权”变成了“攻破一个漏洞突破一个隔离域需要继续寻找下一个漏洞”。4. 实操过程在QEMU模拟器上跑起CHERI环境纸上谈兵没有意义我来带你把CHERI环境真正跑起来。目前最方便的上手路径不是买一块支持CHERI的开发板因为市面上几乎没有现货民用的而是用CHERI的QEMU模拟器跑CheriBSD——这是剑桥大学和SRI International开发的一个基于FreeBSD的CHERI操作系统参考实现。前提是你的CPU支持虚拟化最好有16GB以上的内存CHERI模拟器跑起来还是比较吃资源的。4.1 环境准备与工具链安装第一步是拿到CheriBSD的镜像。CheriBSD官方发布页提供了基于QEMU的虚拟机镜像直接通过qemu启动即可。但你还需要一套专门编译CHERI程序的工具链目前最成熟的是CHERI Clang/LLVM。如果你用的是Ubuntu等Linux发行版可以直接用官方仓库里的基于LLVM的CHERI工具链。这里以源码方式给一个最通用的思路因为预编译包在不同发行版的可用性略有差异。你需要安装QEMUapt install qemu-system-mips64elCHERI QEMU目前主要支持MIPS和RISC-V的模拟器也有Arm Morello相关的模拟器拉取CheriBSD镜像从官方发布页下载cheribsd相关的qcow2镜像准备CHERI工具链从CHERI SDK或者CheriBSD的源码构建工具链工具链构建完成之后你会得到clang --targetriscv64-unknown-freebsd这类带target的交叉编译器。它的用法和普通clang几乎一样只是所有指针都变成了128位的capability。4.2 编译一个最简单的CHERI程序我们用一个最简单的示例来验证环境和理解编译行为。代码长得和普通C语言完全一样#include stdio.h #include stdlib.h #include string.h int main() { char *buf (char *)malloc(16); if (buf NULL) { return 1; } strcpy(buf, hello, cheri); printf(%s\n, buf); free(buf); return 0; }编译命令大致是clang --targetriscv64-unknown-freebsd --sysroot$CHERI_SYSROOT \ -marchmorelloc64 -mabipurecap -o hello_cheri hello.c这里解释几个关键参数的含义-marchmorelloc64这里的“morello”是Arm的一个CHERI实验处理器原型架构c64表示64位capability模式。-mabipurecap让编译器使用纯capability ABI即所有指针都变成capability。--sysroot指定CHERI系统的根文件系统路径这样编译时才能正确找到头文件和系统库。编译之后你可以用file命令查看生成的可执行文件你会发现它的格式里标明了pure-capability相关的标识。如果没有这个标识说明编译参数没生效后续在模拟器上运行大概率会直接报非法指令。4.3 在QEMU里运行并验证防护效果启动CheriBSD QEMU后把编译好的可执行文件通过scp或虚拟磁盘挂载的方式传进去执行它如果一切正常会输出hello, cheri。接下来我们来做一个可能让不少C/C老手后背发凉的测试故意写一个越界访问的程序。#include stdio.h #include stdlib.h #include string.h int main() { char *buf (char *)malloc(16); if (buf NULL) { return 1; } // 故意的越界写 for (int i 0; i 32; i) { buf[i] A; } free(buf); return 0; }如果你的编译和运行环境没问题程序在循环到buf[16]或附近的越界位置时会直接被CPU硬件以SIGPROT信号终止而不是像传统环境下那样“幸运地”继续运行然后留下一堆未定义行为。这个体验和你在普通Linux上开着ASan跑这个代码的体验是完全不同的ASan会在你的代码里插桩用软件逻辑做检查CHERI则是硬件自动检查。你不需要改代码、不需要重新配置工具链除了编译加flag、不需要在生产环境关掉它——因为CPU本身就执行检查。注意QEMU模拟环境下CHERI的性能开销并不能真实反映真实硬件的性能因为模拟本身就是几十倍到上百倍的开销。真实硬件如Arm Morello的公开benchmark数据表明纯capability模式的平均性能开销大约是10%-30%根据工作负载不同有所浮动。4.4 项目落地实操从零开始切一个模块对于大型C/C项目不太可能一次全量切换CHERI所以比较务实的路径是从单个模块开始验证。我在自己的测试项目里选的是一个网络协议解析库因为这类库对内存安全的要求高、对外部依赖少、接口边界清晰。步骤大致如下列出模块的所有外部依赖确认它们是否支持用CHERI ABI重新编译。把模块的CMakeLists.txt或Makefile改成交叉编译最主要的工作是替换编译器、加-march和-mabi参数、指向CHERI sysroot。编译并处理所有编译报错。你会发现大部分报错源于下面三类隐式指针转换把capability转换成了整数值再转换回来利用指针的低位比特做标记的代码比如对齐标志位这在CHERI下不再合法手工序列化/反序列化指针的代码把指针保存到文件或网络包在CheriBSD跑测试套件观察是否有capability异常出现。这一步你会发现CHERI对代码的约束其实和语言规范ISO C/C标准的“正式未定义行为清单”高度重合。很多项目能编译过完全是因为编译器太宽容、硬件太配合。CHERI只是把这些模糊地带用硬件手段彻底“显形”了。5. 常见问题与排查技巧实录5.1 为什么我的代码在普通环境下正常在CHERI下就报SIGPROT这是新手最常遇到的情况。十有八九是代码里存在未定义行为——最常见的就是越界访问、在数组外层访问、或者在较小的对象上执行了超出其生命周期的访问。传统环境下CPU没有能力检查这些所以代码一直能“正常”运行。CHERI把这些未定义行为变成显式崩溃不是CHERI的问题而是你的代码本来就有问题。排查方法用--cheri-traps之类的调试选项让子进程在崩溃处输出更多上下文或者配合gdb的CHERI支持查看当前capability的bounds信息。gdb的info capability可以直接打印当前寄存器里的capability和其边界一目了然。从实际调试经验看CHERI的报错信息比ASan的精确度更高因为它指出的就是硬件检测到的那条指令而不是插桩后栈回溯的“检测点”。5.2 和ASan同时开启会冲突吗有冲突。纯capability模式下程序的所有指针已经是capability了ASan的shadow memory机制和capability的bounds检查会产生重复和干扰。在实践中CHERI的硬件检查已经覆盖且严格于ASan能查到的绝大部分内存类问题因此建议在CHERI环境下不需要再额外开启ASan。你在CHERI下做调试只需要开编译器的-Wcheri相关警告、把编译器的诊断开关打开就能得到比ASan更可靠的反馈。不过有个细节要注意ASan检测不到的内存错误比如某些延迟的堆越界读CHERI也可能检测不到因为CHERI的bounds是在malloc时分配的。所以CHERI不能完全替代ASan但它查到的错误比ASan“干净”——没有shadow memory的干扰定位更快。5.3 性能开销到底有多大是不是不能用于生产这是一个应该严肃面对的问题。真实硬件Arm Morello的基准数据显示纯capability模式下的SPEC CPU 2017等标准负载平均性能开销在10%-30%具体取决于代码风格频繁做小内存分配的程序开销更高因为capability的创建、绑定、释放本身有额外成本。混合模式的性能开销要小得多甚至可以控制在5%以内因为它只保护你显式标记的关键区域。从实际部署可行性角度说这个开销完全可以接受——相比ASan在生产环境不可用的窘境CHERI的开销是常驻的但它能直接防住整类漏洞。这相当于为每个进程装了一个永远不关的“硬件级ASan”。对安全敏感的基础设施DNS服务器、TLS库、容器运行时、数据库引擎来说这是质变级的提升多花的性能换的是“不需要依赖程序员在开发阶段就发现所有漏洞”的系统级安全保障。5.4 我的第三方库不支持CHERI ABI怎么办这确实是当前最大的落地痛点。方案有三个按优先级排序联系上游库维护者请求增加CHERI CI/构建支持。这个领域目前属于早期阶段很多项目维护者其实有接受patch的意愿因为CHERI对库的修改通常很小消除少数未定义行为即可。用混合模式把这些库排除在capability保护之外。代价是跨边界传参时要适配ABI略微增加开发成本。对库代码做小范围patch。多数库的未定义行为都集中在特定几个文件里改起来并不像想象中那么难。我patch过一个老版本zlib只是把几处内部指针运算重写成标准操作改动不到100行。5.5 常用排查思路整理现象可能原因确认方法解决途径程序启动即SIGPROTABI编译参数不对或依赖库混合编译用file检查ELF用ldd看链接库全部按purecap ABI重编译或者切换hybrid模式某些指针操作报错代码里用了指针低位比特做标记在报错处查看capability的bounds和权限位改用显式的位域/布尔字段存储标记指针在不同结构体间转换丢tag通过数据序列化/反序列化指针检查数据结构里是否直接保存了指针值改用句柄/索引间接引用不要直接序列化指针栈越界但堆越界能查栈对象在释放后仍被使用栈上UAF检查栈指针capability的bounds用编译器栈保护或闭环检查代码路径性能开销超出预期大量热路径做了capability创建/销毁用perf分析capability相关指令占比改hybrid模式只保护关键边界6. 部署策略与现实参考从实验项目走到真实基础设施6.1 混合模式优先渐进式覆盖真实项目落地时我强烈不建议第一步就全面切纯capability模式。一个比较稳妥的路径是这样第一阶段用混合模式把系统启动和内存管理路径跑通。这个阶段重点是验证工具链、调试器、基础库的可用性不追求全面防护。第二阶段把最易受攻击的边界模块网络协议解析、不可信输入处理、加密密钥管理切换到纯capability模式。这些模块往往是攻击者最先接触的入口防护收益最高。第三阶段根据性能收益评估把热路径模块从混合模式迁到纯capability模式或者保持混合模式并用细粒度capability做隔离。这样做的好处是风险可控。你在每个阶段都能验证性能和兼容性而不是在最后整合时面对一堆“不知道为什么就崩了”的叠加问题。6.2 对语言层面和工程文化的影响CHERI对C/C代码的约束尤其是纯capability模式带来的指针完整性要求实际上在推动整个生态往更安全、更规范的方向走。你会发现它把ISO C标准里的“未定义行为”真正变成了“硬件可见、编译期可查、运行时可报”的行为而不是任由编译器做各种不透明优化。不少我测试过的老项目在迁移到CHERI的过程中顺带把潜伏多年的边界问题和栈错误给揪了出来。迁移CHERI的过程本质上就是一次整个项目内存安全的全面体检这种隐性价值往往比防护效果本身更让人惊喜。6.3 值得关注的方向目前CHERI生态里最活跃的几个方向我给读者朋友们提一下Arm Morello评估板这是ARM和剑桥大学合作的真实硬件平台基于Neoverse N1核心做了capability扩展。如果你能在学校或公司申请到访问权限上面跑CheriBSD的体验会比QEMU真实得多。RISC-V上的CHERI实现RISC-V对CHERI的支持相对年轻但因为它CPU核是可扩展的所以学术界和工业界都在往这个方向投入。未来很多开源SoC可能直接集成CHERI扩展。CheriBSD在生产环境的试点FreeBSD基金会已经有一些边缘计算和网络基础设施方向的试点。如果你在做FreeBSD相关产品值得盯着CheriBSD的发布节奏。CHERI与WebAssembly、Rust的交叉CHERI能解决C/C的问题而Rust通过所有权模型在软件层面实现了类似的安全保证两者思路完全不同但目标一致。未来可能会在运行时比如ExpoKit、Wasmtime里看到CHERI作为底层安全原语被调用。6.4 个人实操后的经验总结聊了这么多技术细节最后分享几个我实操折腾CHERI时攒下的经验第一务必先熟悉QEMUCheriBSD的组合再上真硬件。QEMU的问题排查环境比真板子友好太多而且很多问题ABI不对、sysroot路径错误、编译参数错漏在模拟器上排查一次就记住了。真板子的串口和JTAG调试对新手来说就是灾难别一上来就挑战高难度副本。第二编译参数务必统一管理。CHERI项目有几个互相牵连的编译参数target三重奏、march/mabi、sysroot路径、链接器标志。任何一个不对都可能导致运行期崩溃而这类问题往往不会在编译期报错。我把这套上下文收敛到一个CMake toolchain文件里所有的子模块都无条件include它才彻底摆脱了“这边加一个flag那边漏一个flag”的混乱。第三别把CHERI当成万能药。它解决的是内存安全类别里的很大一部分问题但不能防逻辑层漏洞、不能防竞态条件除非你配合锁和原子操作、不能直接防侧信道攻击。CHERI的目标是把攻击者利用内存破坏漏洞的难度提到“近乎不可行”的水平但它不是程序正确性的银弹。该写的单元测试、该做的fuzzing、该做的代码评审一个都不能省。CHERI的意义在于就算你的防御层出了纰漏它为系统兜住的底线比传统方案高出一个量级让“一处漏洞血洗全盘”变成了“突破一层的攻击者还要继续面对下一层的防护”。