采用CHERI实现内存安全与细粒度隔离

Lobsters Hottest 事件

摘要

David Chisnall 介绍了 CHERI,一种硬件能力架构,通过 ARM Morello、RISC-V 和 CHERIoT 微控制器等 ISA 扩展实现内存安全和细粒度隔离。该演讲强调了实用编程模型对于安全隔离的重要性。

<p><a href="https://lobste.rs/s/lt89di/adopting_memory_safety_fine_grained">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/13 19:22

# 采用 CHERI 实现内存安全与细粒度隔离 来源:https://www.infoq.com/presentations/cheri-memory-safety-compartmentalization/ ## 演讲稿 **David Chisnall:** 我将会非常快地讲很多内容。如果你几乎什么都不记得,我希望你记住这一件事:隔离是我们知道怎么做的事情。这很容易。几十年来,我们一直用 MMU 隔离进程,虚拟机也同样用 MMU 来隔离。CPU 非常便宜。如果你想要两个完全隔离的工作负载,最便宜的方法就是把它们放在不同的计算机上运行。真正开始变得困难的地方,是当你拥有两个并非完全隔离、需要相互通信的工作负载时。这会影响你如何设计硬件,如何设计编程模型,而随之而来的一切才是困难的部分。这是我在过去十年左右一直试图向 CPU 厂商灌输的信息。每次他们过来说,我们有这种新的隔离技术,然后你说,很好,那么共享的编程模型是什么?他们会说,我们可以把页面放到两个信任域中。我会说,这实际上并不是一种编程模型。 ## CHERI:它是什么,以及它不是什么 我来这里是为了谈论 CHERI。当然,当你打算解释某样东西是什么时,第一件要做的事就是先说明它不是什么。你会听到很多人谈论 CHERI ISA。其实并不存在所谓的 CHERI ISA。你可以把 CHERI 想象成类似 SIMD 的东西。同样也不存在所谓的 SIMD ISA。ARM 上有 NEON 或 SVE,Intel 上有 SSE 或 AVX。这些都是同一组抽象思想在特定 ISA 上的具体实现,即你可以对数据向量而不是标量进行操作。CHERI 完全一样。我们最初构建的研究原型是扩展 MIPS,因为 MIPS 恰好有 20 年历史并且已经不受专利保护。ARM 为 Aarch64 做了一个 CHERI 扩展,叫做 Morello。我可能会花太多篇幅谈论的那个,主要因为它在很大程度上要归功于我,叫做 CHERIoT。 目前,我们很可能正处于标准化一个名为 RVY 的 RISC-V 基础版本的最终阶段,这是 CHERI 在特定 ISA 上的另一个实例化。对于很多 CHERI 工作来说,在软件方面,它很大程度上聚焦于两个极端。最初的研究是在 CheriBSD 上完成的,这是 FreeBSD 的一个分支。很多想法正在逐渐移植回 Linux。这确实侧重于增量采用。它关注应用核心,从手机级别扩展到数据中心级别,那些需要能够运行现有二进制文件的场景,因为人们的机器上有数 GB 的现有二进制文件。如果你说,是的,我们有一种新架构,它很棒,很安全。你只需要抛弃所有软件重新开始。人们看着你的眼神就像你是个疯子。而另一个极端是 CHERIoT,它是为微控制器设计的。 如果你对嵌入式系统的人说,你需要重新编译所有代码,他们会说,没问题,反正今天是星期二。CHERIoT 的设计,部分是为了证明我们可以把这些想法从手机级别一直缩小到微控制器级别,同时也是为了展示,如果从硬件和软件栈的整个设计开始就能假设你拥有 CHERI,那会给系统设计带来什么影响。它是源代码兼容的。因为再说一次,如果你对嵌入式系统的人说:“我知道你过去 20 年都在做这个 C 代码库。相信我,如果你从头重写,一切都会很棒。”那么你的销售人员很快就会被赶出门。 我现在真的要开始解释 CHERI 是什么,而不是它不是什么了。CHERI 始于将来自两个相当不同的研究文献领域的思想统一起来。一方面,能力(capability)系统可以追溯到 60 年代,作为一种表达安全概念的方式。能力是一种不可伪造的权威令牌,你可以将其委托给其他人。无论谁持有它,只要出示它,就被授权执行某个操作。它们通常带有权限,你可以减少这些权限。你可以拿一个能力,创建一个更简单的能力,然后进一步委托下去。 另一方面,还有整个胖指针(fat pointer)思想。传统上,指针只是一个地址。如果我们让指针不仅仅是地址呢?如果我们加上一些其他元数据呢?这些元数据可能是边界,可能是类型。它们仍然像指针一样被传递。它们可以嵌入到其他数据结构中。同样,它们必须被显式呈现才能被解引用。 CHERI 统一了这些思想。从非常高的层次来看,用 30 秒概述 CHERI,那就是:CHERI 让硬件认识到指针实际上是一种东西。在传统架构中,指针是语言层面的抽象。C 对指针有一些规则。Rust 对指针有更严格的规则。Java 对指针有略微不同但同样严格的规则,并且称之为引用,这特意是为了迷惑本科生。CHERI 认为,指针不能凭空产生。它们携带边界,携带权限。任何在传统 ISA 中会把地址作为基址操作数的指令(加载、存储和跳转)现在都使用这些 CHERI 能力之一,这是一种硬件强制的、硬件保护的指针类型。在传统系统中,指针只是一个数字。它标识内存中的一个点位。你可以对它进行整数操作。现在你创建了一个新指针。在 CHERI 系统中,这非常相似。你仍然有那个地址。它还随身携带边界。你只能在边界内使用它。 ## CHERI 能力 让我们看看其中一个内部结构。不出意料,其中一半是地址。在 32 位系统上,能力是 64 位,其中一半是地址。在 64 位系统上,你仍然有一个 64 位地址,然后还有 64 位用于所有元数据。它们有一些边界。你可能会注意到,你有一个对象顶部的地址、对象底部的地址,以及你实际指向的地址,这相当于地址大小的三倍。我们没有那么多空间。我们必须压缩它。事实证明,这些东西中有很多冗余。顶部的高位、底部的高位,以及对象中间某处的高位,几乎总是相同的。我们存储边界的方式是使用一种浮点表示,其中到顶部的位移和到底部的位移各有自己的尾数,但共享一个指数。 实际上,这意味着对象越大,其顶部和底部的对齐要求就越强。实际上,大多数时候你根本不需要关心这一点,因为内存分配器已经在为大型对象做更强的对齐了。它通常有一种大约 3 位尾数的表示,而我们在硬件中提供了大得多的尾数。此外还有一些权限。其中一些非常简单,比如加载和存储权限。有些则稍微复杂一些。我们分离出一个权限,它表示:如果你可以加载或可以存储,那么你也被允许加载和存储指针。我可以给你一个缓冲区,如果我碰巧在里面留下了任何指针,是的,你不能加载那些指针。你只能加载数据。还有一些更丰富的权限。 我们可以用这些能力来强制实现深度不可变性,这借助一个名字有点令人困惑的权限,叫做“允许加载可变”(permit load mutable)。如果你有该权限,当你加载一个能力时,你只是直接加载它。如果你没有该权限,当你通过一个不具有该权限的指针加载指针时,该指针的权限和存储权限会被剥离。这意味着我可以给你一个指向树根节点的指针,只去掉几个权限。现在你可以沿着那棵树中的所有指针走。你可以完整遍历那棵树。你加载的任何指针都不会有存储权限。这意味着我现在给了你对整棵树的只读视图。在 CHERIoT 中,我们还以大致相同的方式强制实现深度和浅层的“禁止捕获”(no-capture)属性。非常关键的是,有一个标签位(tag bit),它表明:这个东西真的是一个指针。 或者,如果它没有被设置,那么这个东西实际上不是指针。我稍后会回到这一点。还有一个对象类型,用来表示:这是一个密封能力(sealed capability)吗?同样,我稍后会回来解释这是什么意思。关键是,所有这些属性都是单调的。你可以取边界并缩小它们。你可以取权限并移除其中一些。你不能反向操作。如果你想从一个指向数组中某个字段的指针(其边界已被限制到该字段)出发,然后重建一个指向包含该字段的数组的指针,你必须从其他地方找到指向该数组的指针,然后重新推导。 这在内存中是什么样子?你有一些数据,也有一些能力。在它们旁边,并且不可寻址地,物理内存中有一个额外的位,那就是我们存储标签的地方。这在寄存器文件中也是复制的。这是一个非常简单的机器,有六个寄存器。寄存器 6 是一个能力,指向倒数第二个内存位置。让我们想象一个非常简单的 CHERI 指令集,其中有加载和存储指令,可以操作能力或数据。通常,数据指令会进一步细分,所以你会看到字节、半字、字等等。这只是一张幻灯片,所以我们不能在这里放完整的 ISA。第一条指令是从寄存器 6 的相对偏移 0 处加载一个能力到寄存器 2。当我们执行它时,它会复制整个能力和标签位。 这非常重要,因为正是这个属性让我们可以实现像 memcpy 这样的、希望进行类型无关复制的操作。你需要能够在不知道某数据是否为指针的情况下复制它。如果你阅读关于为 C 语言改造内存安全的系统研究文献,其中很大一部分会说:是的,我们有一个编译器过程,会遍历并标注哪些东西包含指针以及所有指针在哪里。你读到这些就会想,你肯定没有用你的工具编译过任何非平凡的 C 程序,对吧?现在,如果我们做相反的操作,将寄存器 1 中的一些数据存储到同一个位置,我们只存储了寄存器宽度的一半,但因为我们已经覆盖了该内存中能力大小字的任何部分,我们就隐式地清除了标签。如果你在一个大型应用级核心上,这将是缓存内的操作。 如果你在微控制器上,那只是总线消息中的另一个位,表示“请清除与此关联的标签”。同样,我们可以用加载能力指令将其加载回来,它同样会传播这个零位。这是你做 memcpy 所需的两半部分。如果我们然后尝试解引用它,即使它在一个能力寄存器中,当我们尝试解引用时,标签并没有被设置。硬件要做的第一个检查就会失败,它会说:你正试图把一个不是指针的东西当作指针使用。这确实能阻止很多指针注入。很多攻击原语的第一步或第二步都涉及用攻击者控制的内容覆盖指针。如果这是你从网络或文件中读取的内容,那么它现在不是指针,所以你不能解引用它。这给了我们一个 1 位动态类型系统,其中每个东西要么是指针,要么不是指针。如果它不是指针,尽管用剩下的所有位去做你想做的任何事情。如果它是指针,硬件会保证那些单调性属性。 我说过我会回到密封机制。密封在 CHERI 中是一个非常强大的东西。稍后我会展示一些 C 代码,说明如何从 C 中使用它。如果你的能力在其权限字段中有“允许密封”(permit seal)和“允许解除密封”(permit unseal),那么它引用的不是那个线性的地址空间。它引用的是一个抽象的类型空间。这意味着你可以把它用作密封令牌,配合密封操作使用。同样,这是一个寄存器到寄存器的 CPU 操作。它所做的只是把密封令牌中的地址复制到你用作指针的东西的对象类型字段中。你得到的结果是,硬件不会允许你以任何方式修改它。你试图修改它,标签位就会被清除。硬件不会允许你以任何方式解引用它。你试图将它用于跳转、加载或存储,硬件都会陷入异常。 你仍然可以把它存储在内存中。你可以传递它。当你把这个令牌交给某个不可信代码,而它把它还给你时,你可以执行相反的解除密封操作,把它变回原始指针。如果他们给你的完全是其他任何东西,即使它是一个有效指针,甚至是一个具有不同类型的有效密封指针,解除密封操作也会失败,你会得到一个未标记的值。这意味着你可以拥有类型安全的不透明指针。所有那些你会使用文件描述符或带有间接表的句柄的地方,在 CHERI 中,这些都可以直接是指向该事物的指针。当你想要开始从一个由内核管理该表的世界,转向一个完全特权分离、最小特权设计的世界,其中任何隔离区(compartment)都可以向其他隔离区提供不透明的句柄(比如套接字句柄、消息队列句柄等)时,这非常有用。 这在你的内存层次结构中是什么样子?在我们构建的那类东西(微控制器)中,实际上非常简单。你有一个能力。你的加载存储单元会做边界或权限检查。它们非常快。它们只是单周期操作,与流水线的其余部分并行发生。如果这些检查通过,你就直接操作物理地址,从那里往下就和在非 CHERI 系统上一样了。不过,如果你在应用核心系统上,这里会有一个 MMU,它正在进行一个或多个阶段的地址转换,把你从虚拟地址带到物理地址。这些发生在 CHERI 检查之后,并且在很大程度上是正交的,并且与 CHERI 非常干净地组合在一起。 类似地,CHERI 系统上的程序计数器被扩展为一个完整的能力。如果你正在运行一个未修改的遗留二进制文件,它可能有一些指令会跳转到特定地址。当这种情况发生时,你把能力的元数据半部分视为隐式的。这些跳转只会设置程序计数器的地址。所有的边界检查仍然适用。如果你试图跳出你被告知允许执行代码的区域的边界,那会是一个陷阱,但除此之外,它看起来就像遗留代码。同样,对于数据,有一个隐式的默认数据能力,加载和存储可以针对它进行间接操作。 在较大的系统上,这两者的结合让你很容易构建一个看起来非常像 WebAssembly 的编程模型,你可以说:“这是我的地址空间中的一个区域。它里面有一些代码和一些数据。你可以在里面运行任意代码。你只是不能访问它之外的内存。也许里面有一点点 CHERI 感知的代码,用于在你显式给它访问权时复制东西,但除此之外,它只是一个沙箱。” ## 采用 CHERI 不会改变 C 作为一个 C 程序员,关于采用

相似文章

内存安全是生死攸关的问题

Lobsters Hottest

作者认为,内存不安全的开源软件极易受到即将到来的人工智能漏洞查找代理的攻击,这使内存安全成为道德义务,并且Rust必须作为领先且零开销的内存安全语言取得成功。

Fil-C: Garbage In, Memory Safety Out

Lobsters Hottest

Fil-C 是一个完全内存安全的 C/C++ 实现,通过 LLVM IR 阶段的不可见能力(invisicaps)将指针值与边界信息捆绑,实现高兼容性,性能损失约 4 倍。

突破 RISC-V 模拟的极限

Hacker News Top

这篇博客文章探讨了如何通过在非 RISC-V 机器上使用提前重编译器来加速 RISC-V 执行,该重编译器通过尾调用连接基本块,并利用 Clang 的 preserve_none 调用约定,作为 Axiom 的 OpenVM 项目的一部分。