在Sonata Board上体验CHERI:硬件级内存安全实战指南 📅 2026/8/26 20:50:42 收到这个题目很多人第一反应是CHERI 不是那个还在论文里、实验室里的东西吗Sonata Board 又是什么开发板我也是抱着“先看看能跑成什么样”的心态入手的。结果这块板子给我的冲击比想象中大得多——它把一套完整的内存安全硬件机制放到了低成本的 FPGA 平台上而且真的能跑 CheriBSD能交叉编译 C 程序能亲手触发一次“越界保护 fault”。这篇文章就记录一下我在 Sonata Board 上折腾 CHERI 的完整过程包括环境准备、第一个 capability 程序、内存安全检查实验以及一堆新手才会踩的坑。想接触硬件内存安全、RISC-V 扩展、C/C 指针安全的朋友这篇文章应该能帮你省下不少试错时间。1. CHERI 到底是什么以及为什么值得在 Sonata 上玩1.1 从“指针只是个地址”说起传统的 C/C 里指针就是一段内存的地址。它告诉你“在哪儿”但完全不告诉你“能往哪走”“能走多远”“能不能写”。这就相当于一张只写了门牌号的便条拿到它的人理论上可以打开这个门牌对应的整栋楼甚至顺着地址改几个字节就进了隔壁楼。CHERICapability Hardware Enhanced RISC Instructions的思路是给这个“便条”升级成一张“门禁卡”。每个指针相关的 capability 不再只是一个地址而是一段带有元数据的对象它包含当前地址、基址、上界、访问权限、对象类型以及一个硬件维护的 tag 位。这个 tag 位极其关键普通 store 指令往内存里写数据是不可能伪造出有效 tag 的只有硬件层面的 capability 加载指令才能把它置为有效。所以一个 capability 不能被内存漏洞伪造也不能被简单改几个字节来绕过。我一开始用另一个类比来理解传统指针像“房间号房卡”CHERI capability 像“门禁卡可以活动的区域允许做的操作防伪标识”。你可以在门禁卡允许的范围内到处走但一旦越过范围边界感应门会瞬间锁死。而且这个锁死不是靠软件检查——比如 AddressSanitizer 那种运行时插桩——而是在 CPU 流水线里每次内存访问发生时硬件自动检查的。这里有一个关键点CHERI 不是单纯加几个寄存器而是改变了 CPU 对指针和内存访问的底层语义。传统架构里一次 load 就是“取地址读内存”CHERI 架构里一次 load 会先确认这个 capability 的 tag 是否有效、地址是否落在 bounds 内、权限是否包含读。这一套检查在硬件流水线里完成不需要软件在每条指令前插桩所以性能开销比纯软件方案要小得多语义上也更彻底——因为它连恶意代码的操作都拦得住而不仅仅约束“好程序”的 UB 行为。1.2 Sonata Board 在 CHEI 生态里的位置CHERI 目前有几个落地平台Arm 的 Morello 是一块比较出名的实验板但门槛不低渠道也受限QEMU 模拟器也可以跑 CheriBSD但模拟器里你不会感受到“真实的硬件页面错误、真实的中断、真实的 DMA 和网卡交互”。Sonata Board 是 NewAE 和 lowRISC 等团队合作推出的开源硬件开发板基于 RISC-V板子上集成了支持 CHERI 扩展的处理器核可以直接跑 CheriBSD。它的定位就很适合“想用真硬件做实验但又不想等实验室渠道”的开发者。板子是 FPGA 方案整个 SoC 设计在 GitHub 上开源甚至可以自己改一改 CPU 或外设重新生成 bitstream。这意味着你不只是“用 CHERI”如果你愿意还可以“改 CHERI”。我这次虽然没改硬件但那种“自己烧固件、跑实验、看 fault 日志”的感觉比纯软件模拟扎实太多。我建议先想清楚自己的实验目标。如果只是想看 CHERI 的内存保护效果那 Sonata 足够如果想对比 CHERI、MPK、ASan 的性能差异那你需要更多测量工具Sonata 只是一个起点。我这次的目标很明确跑通交叉编译流程写几个能触发内存安全问题的程序观察硬件保护行为。2. 环境准备从开箱到进入 CheriBSD 控制台2.1 硬件和连接我手上的装备清单很简单一块 Sonata Board、一条 USB-C 数据线、一张 microSD 卡、一个 USB 转串口模块或者直接用板载的 USB 串口。Sonata 板上有调试串口插上 USB 后在 Linux 下一般会出现/dev/ttyUSB0或者/dev/ttyACM0这样的设备。我用串口工具picocom连接波特率是 1152008N1picocom -b 115200 /dev/ttyUSB0如果你用的是 Windows可以用 MobaXterm 或者 PuTTY 的串口会话参数一样。注意别接错线USB 数据线最好用质量好一点的有些线只能充电不能传数据我第一次就栽在这上面折腾了半小时发现设备根本枚举不出来。关于 SD 卡官方文档里会给出镜像和烧录方式。烧录步骤其实就是把官方发布或自己构建的镜像用dd写到 SD 卡上。注意 SD 卡容量不要太小至少 4GB 起步我用的 16GB 卡没有遇到问题。烧录前一定确认设备名别把自己的硬盘写没了# 以 /dev/sdX 为例务必确认这是 SD 卡 sudo dd ifcheribsd-sonata.img of/dev/sdX bs4M statusprogress convfsync烧完把卡插到板子上接好串口和电源上电后串口窗口里就会滚动启动日志。2.2 获取 bitstream 和固件Sonata 不是那种“插上就能用”的板子它需要先把 FPGA bitstream 配置进去。这里有两个路线一种是直接用官方 release 里编译好的 bitstream省事另一种是自己用 Vivado 和开源硬件流程生成耗时但可定制。我这次偷懒直接用官方提供的 bitstream后续如果要改 SoC 再走构建流程。写 bitstream 的方式和具体工具以当前 Sonata 仓库文档为准不同版本的工具链差异较大。我的建议是第一次玩直接下载预编译好的 bitstream 和 CheriBSD 镜像把重点放在软件侧等确认硬件能跑起来再回头研究 FuseSoC 和 Vivado 的构建细节。2.3 进入系统后的第一眼板子启动后会看到 CheriBSD 的 boot loader 日志然后进入登录提示。默认账号密码在官方文档里有我的实验环境里是以 root 身份登录的。登录后我做的第一件事是看系统信息uname -a dmesg | grep -i cheri如果内核消息里能看到 CHERI 相关的能力开启提示说明整个链路已经通了。此时你已经拥有一个运行在真实 CHERI 硬件上的操作系统接下来可以交叉编译自己的程序跑进去。3. 交叉编译第一个 CHERI 程序3.1 工具链怎么选要在 Sonata 上编译程序有两种做法。一种是在板子上直接编译但板子的性能和存储有限不适合跑大工程另一种是在 PC 上使用 CHERI 交叉编译器生成 RISC-V purecap 二进制再拷到板子上运行。我推荐第二种也是大多数人的选择。官方提供的 CHERI SDK 基于 Clang/LLVM支持riscv64cheri-unknown-freebsd这个 target triple。我一开始直接用普通 x86 Clang 编译结果生成的二进制在板子上根本跑不起来提示 Illegal instruction 或者直接段错误。原因很简单普通 Clang 生成的是标准 RISC-V 指令没有启用 CHERI capability 相关的指令和 ABI。正确做法是用 CHERI SDK 里的 clang或者自己用 cheribuild 构建一套工具链。构建工具链的命令大致是这样具体路径以你 clone 下来的脚本为准git clone https://github.com/CTSRD-CHERI/cheribuild cd cheribuild ./cheribuild --build cheribsd-riscv64-purecap这条命令会下载源码、构建编译器、库、内核等耗时比较长我建议在磁盘空间充足的 Linux 机器上跑至少预留几十 GB。构建完以后编译一个 hello world 就可以用/path/to/sdk/bin/clang --targetriscv64cheri-unknown-freebsd -O2 -static -o hello hello.c这里-static是建议加上的因为纯 capability 环境下动态链接的库路径比较麻烦静态链接能让二进制直接跑。3.2 第一次看到 capability 的样子第一个程序不要写太复杂先输出一个 malloc 指针的 capability 信息。代码如下#include cheri/cheric.h #include stdint.h #include stdio.h #include stdlib.h int main(void) { void * __capability p malloc(64); if (p NULL) { return 1; } printf(addr: %#lx\n, (unsigned long)cheri_address_get(p)); printf(base: %#lx\n, (unsigned long)cheri_base_get(p)); printf(len : %#lx\n, (unsigned long)cheri_length_get(p)); printf(perm: %#lx\n, (unsigned long)cheri_perms_get(p)); free(p); return 0; }编译后拷到板子上运行你会看到base和len并不是随意的malloc 返回的指针绑定了实际可访问区间perm里也明确了读写权限。这里有一点很值得体会同样的代码在传统 C 里你拿到的是一个裸地址在 CHERI 里你拿到的是一个“自带边界和权限的令牌”。如果代码越界硬件会直接拒绝访问。3.3 传输程序到板子最简单的传输方式有两种。一种是在板子的 CheriBSD 上启用网络然后用scp传文件另一种是把编译好的二进制放到一个 FAT 格式的 U 盘或额外分区里。我这次没有配网络直接用了后一种把二进制放在 SD 卡的 FAT 分区然后在板子上挂载并 cp 到/tmp里执行。这样一个来回下来交叉编译的整个流程就闭环了。4. 核心实验亲眼看到越界和 UAF 被硬件拦截4.1 越界访问实验CHERI 最出名的能力是空间内存安全也就是禁止越界访问。我写了一个非常简单的越界程序#include stdio.h #include stdlib.h int main(void) { char *buf malloc(8); for (int i 0; i 16; i) { buf[i] (char)i; } printf(done\n); return 0; }这段代码在普通 x86 Linux 上编译运行大概率不会立刻崩溃因为堆的 8 字节后面可能还有可写内存越界写会悄悄破坏堆元数据或者其他对象问题被推迟到后续某个不可预测的时刻。这正是传统内存安全漏洞最难排查的地方错误发生了但现场早就被破坏了。在 CHERI 环境下程序运行到buf[8]附近时硬件会检测到当前地址已经超过 capability 的 bounds于是产生一个 CHERI protection fault。如果你是 root 或者有足够权限内核会打印出类似 protection fault 的信息进程被信号终止。整个过程没有任何模糊地带错误精确发生在越界的那一条指令。我当时的感受是这太“不讲道理”了硬件直接把指针的边界给焊死了。你可以在循环里看到i8之后程序就断了而不会等到最后才输出“done”。对调试来说这种定位粒度比 ASan 报告还要精准一些因为它是 CPU 在访问时实时拦截的。4.2 释放后使用UAF实验空间边界只是一个维度CHERI 还处理时间上的安全也就是 use-after-free。原理是free()之后存放在内存里的 capability tag 会被标记为无效。如果你的代码还保留着原来的指针并且试图通过它访问内存硬件检查 tag 时会发现这个 capability 已经“报废”直接拒绝访问。我写的实验代码更短#include stdio.h #include stdlib.h int main(void) { char *p malloc(16); free(p); p[0] 1; printf(ok\n); return 0; }这段代码在传统环境里大多数时候也能“正常”跑完因为 free 只是把内存块还给了 allocator物理内存还在p[0] 1只是改了一块已经被释放的内存而已。但在 CHERI 上p这个 capability 的 tag 在free后已经失效任何 dereference 都会触发硬件异常。这个机制对漏洞利用的打击是致命的因为 UAF 是很多真实漏洞的根因攻击者通常需要反复利用悬挂指针来操控堆布局CHERI 让这一招从第一下访问就直接失败。4.3 为什么这些实验很有意义我在做这些实验之前其实对 CHERI 的“安全收益”多少有些将信将疑。通过这几个小例子我发现它不是在现有体系上打补丁而是在 ISA 层面重新定义了“指针到底是什么”。传统 C/C 的内存安全是“事后修补”——先出问题再想办法检测CHERI 是“事前约束”——CPU 根本不允许非法的指针操作被完成。当然CHERI 不是银弹。它解决的是内存安全中空间边界、指针完整性和释放后使用这一大类问题但逻辑漏洞、整数溢出导致的边界算错、类型混淆等依然需要其他手段配合。不过它至少把“内存破坏后程序行为不可预测”这个最让人头疼的问题变成了“访问非法时立刻终止”的可预期行为。对系统软件和嵌入式开发来说这种确定性是极大的安全感来源。4.4 实验过程中可能遇到的“假阳性”我在跑实验时一开始遇到一个奇怪现象一个看起来很正常的程序启动后立刻就 fault。后来发现是因为代码里用了不安全的指针类型转换把一个普通地址强制转成 capability导致 tag 无效。这提醒我CHERI 环境里不能随便做整数和指针之间的转换所有指针必须来自合法的 capability 派生操作。如果是从嵌入式裸机那边带过来的习惯——习惯用(uintptr_t)转来转去——到 CHERI 上第一个要改的就是这个。5. 往深走一步用 Capsicum 组合出进程沙箱5.1 Capsicum 和 CHERI 的关系Capsicum 是 FreeBSD/CheriBSD 里的一套沙箱框架它基于 capability 的思想来限制进程对文件描述符的操作。一个进程调用cap_enter()之后就无法再打开全局路径、创建网络 socket 等只能使用已经传入的 fd并且每个 fd 的权限还可以进一步通过cap_rights_limit()裁剪。这和 CHERI 的“能力模型”天然契合因为两者都在强调“把权力收窄到最小必要范围”。Sonata 上跑的是 CheriBSD天然支持 Capsicum。我想试试在 CHERI 的纯 capability 环境下Capsicum 是否会更严格。理论上fd 本身可以携带更强的信息编译器甚至能在编译期帮你检查某些非法用法。5.2 一个小demo限制文件描述符权限下面这个程序在进入 capability mode 之前打开一个文件然后只保留读权限#include sys/capsicum.h #include fcntl.h #include unistd.h #include stdio.h #include errno.h #include string.h int main(void) { cap_rights_t rights; int fd open(/etc/passwd, O_RDONLY); if (fd 0) { perror(open); return 1; } cap_rights_init(rights, CAP_READ); if (cap_rights_limit(fd, rights) 0) { perror(cap_rights_limit); return 1; } if (cap_enter() 0) { perror(cap_enter); return 1; } char c; if (read(fd, c, 1) 0) { perror(read); return 1; } int newfd open(/etc/passwd, O_RDONLY); if (newfd 0) { printf(open after cap_enter failed: %s\n, strerror(errno)); } return 0; }编译后放到板子上运行你会看到open在cap_enter()之后返回ECAPMODE或者类似错误而之前打开的 fd 依然可以读。这个实验和 CHERI 没有直接关系但在同一块板子上你能同时感受到“软件级能力模型”Capsicum和“硬件级能力模型”CHERI的配合。5.3 合起来的价值如果你一个人开发一套网络服务传统写法是进程启动后打开一堆文件、监听端口然后进入事件循环。如果被攻击者利用漏洞攻击者可以调用任意系统调用读取敏感文件。Capsicum 可以把服务在启动后立刻“囚禁”在已有 fd 的集合里攻击者即使拿到代码执行能力也无法用open(/etc/shadow)去读东西。而 CHERI 则进一步保证即使攻击者想通过内存破坏修改 fd 的状态或绕过检查也会因为 capability 的 tag 和 permissions 被限制而失败。两者不是一个层面的东西但可以组成纵深防御。我把这套 demo 跑通之后最大的体会是安全不只是一个“特性”而是一种贯穿硬件、操作系统、应用层的设计哲学。Sonata 和 CheriBSD 之所以让人兴奋是因为你可以在一个真实的板子上把这条链路完整地走一遍。6. 排错与实战技巧那些文档不会明说的坑6.1 常见问题速查我把这次实验过程中遇到的典型问题整理成了表格方便你快速定位。现象可能原因解决办法串口无输出USB线不支持数据传输、串口参数错误、未供电换数据线确认 115200 8N1检查电源指示灯SD卡无法启动镜像损坏、卡速太慢、分区未写对重新烧录换知名品牌卡用convfsync写完再拔程序提示 Illegal instruction用了普通编译器生成的非 CHERI 指令使用 CHERI SDK 的 clangtarget 设为 riscv64cheri程序启动即 protection fault指针被强行用整数转换伪造不要用(uintptr_t)到处转保持指针来自合法操作cross compile 找不到头文件缺少 sysroot编译时用--sysroot指向 SDK 里对应的 purecap sysroot在 cap_enter 之后 open 还能成功Capsicum 编译参数或内核配置问题确认 CheriBSD 内核包含 capsicum 支持检查错误码6.2 调试能力利用好 dmesg 和 core dump在 CHERI 环境下程序崩溃时得到的日志比传统环境要“啰嗦”得多。这其实是好事。遇到 protection fault 时先看dmesg末尾的内核消息里面通常会包含出错的虚拟地址、CPU 的 capability 状态甚至能看出来是在哪条指令触发检查。如果你编译时加了-g还可以用 CheriBSD 的调试工具把 core dump 的 capability 信息解析出来精确定位到源码行。我第一次跑越界实验时就是靠 dmesg 里的报告确认了触发点在循环内第 9 次迭代左右。6.3 新手最容易犯的三个错误第一个错误把 CHERI 和普通的 64 位指针混为一谈。在纯 capability ABI 下指针不是一个整数不能随意按整数运算。很多传统代码里(uintptr_t)p和(char *)addr的来回转换到这里都要重新设计。第二个错误忽略权限位。capability 不仅有边界还有权限集。比如从共享内存映射得到的 capability可能没有执行权限如果你试图用它调函数就会触发权限错误。你在设计数据缓冲区时应该最少化权限而不是拷贝一份“万能指针”。第三个错误没搞清楚静态链接和动态链接的差别。CHERI 的纯 capability 环境里动态库的加载和重定位涉及很多 capability 操作如果工具链版本和板子上的 CheriBSD 版本不一致很容易出现运行时找不到库或符号的问题。我建议初学者一律用-static先把功能跑通再回去研究动态链接的细节。7. 还能怎么继续玩这块板子后续值得玩的方向很多。一个方向是跑网络栈测试看看在真实 DMA 和网卡驱动下CHERI 是否真的能把驱动里的内存安全问题也拦住。另一个方向是研究 CHERI 和编译器的配合比如在 CHERI C/C 里开启更强健壮性模式让指针的 bounds 尽量贴近对象的实际大小从而把“绕一圈访问对象内越界”也拦下来。还可以深入研究 CHERI 在嵌入式场景里的变体比如 CHERIoT它把 capability 模型带入更小的 MCU。对我来说这次实验最有价值的地方不是记住了几个命令而是彻底改变了我对“指针安全”的理解。以前我总觉得内存安全是软件工程问题要靠程序员自觉加 sanitizer靠代码审查但看了 Sonata 上的表现之后我意识到硬件可以在指令级别把很多危险操作变得根本不可能完成。你不需要信任每个程序员都记得检查边界因为 CPU 会在越界的那一瞬间替你把门关上。如果让我给想入门的人一句实在的建议先别急着买板子先在 QEMU 模拟器上跑一遍 CheriBSD 的 hello world再决定要不要上 Sonata。因为工具链和交叉编译的熟悉程度决定了你拿到真板之后是“立刻开工”还是“一脸茫然”。一旦流程摸清了Sonata 带来的那种实打实跑在真实硬件上的感觉绝对是模拟器给不了的。