关注了就能看到更多这么棒的文章哦~
Supporting CHERI capabilities in GCC and glibc
By Jonathan Corbet
September 26, 2022
Cauldron
DeepL assisted translation
https://lwn.net/Articles/909265/
CHERI 架构来自于一个研究计划,希望能对普通的 CPU 架构进行扩展,来阻止多种类型的内存相关 bug(和漏洞)。在 2022 年的 GNU Tools Cauldron 会议上,Alex Coplan 和 Szabolcs Nagy 介绍了将 GCC 和 GNU C Library(glibc)引入该架构的工作。CHERI 从根本上改变了内存访问方式,要想能正确地支持这个架构,可不是一个小任务。
CHERI
Coplan 首先介绍了 CHERI,这是一个已经运行了十年左右的研究项目,来自于剑桥大学。它引入了 "capabilities" 的概念,这个术语在这里的含义跟平常不太一样。Coplan 说,capability 是访问一系列内存的一个 authority token,不能被伪造(forged)。capability 可以传送给别人,它们可以从其他 capability 派生(derive)出来,但这样派生之后获得的 capability 拥有的 permission 不能增加,只能减少。
在 CHERI 架构的概述中可以看到更多描述找到。简而言之,一个 capability 可以被认为是一个特殊类型的指针,占据了 129 bits。其中最低位的 64 bit 是传统的虚拟地址,而更高的 64 位(通常被称为 "provenance (出处)")描述了相关的访问权限。其中包括了用来指示允许的操作(读、写、执行)的 bitmask, 以及该 capability 适用的内存区域。这 128 位加在一起,就可以像一个指针一样在允许的内存范围内进行它所允许的访问。
第 129 位存储在其他地方,由 CPU 管理;只有在这个 bit 置 1 的时候,这个 capability 才是有效的。一些 CPU 指令可以从一个旧的 capability 衍生出一个新的 capability,这些指令将保持这个 bit 的设置;对这个 capability 的直接修改,会导致这个生效 bit 被清零。其他不被允许的操作,如试图使用一个 capability 在允许的内存范围之外写入,也会使该 capability 失效。如果硬件设计完好无误,那么每一个 capability 都允许以某些方式访问内存范围,仅此而已。
在系统启动时,firmware 为内核提供了一个对所有内存都允许访问的 capability。在系统的生命周期中使用的每一个其他 capability 最终都来自于这个 "root capability"。设计良好的系统里,每个 capability 可能经过几个层次的衍生之后,就被缩小到一小块内存区域了。
Morello GCC
CHERI 是定义成附加在现有架构上的一个组件的;Morello 项目就是在向 ARMv8-A 架构添加 CHERI 功能。现在已有原型实现了这种组合。基于 LLVM 的成熟 toolchain 也有了,但 ARM 也希望有 GCC 的版本,因此 Morello GCC 项目正在努力在 GCC 中增加支持;该项目还在移植 binutils、GDB 和 glibc。
已经实现了两种 capability model,称为 "pure-cap "和 "hybrid";前者将 capability 施加在所有指针上,而后者只对系统中的一部分指针使用 capability。hybrid 模式可以允许把那些支持 capability 的代码跟不支持 capability 的代码混合在一起运行;Linux 内核已经利用 hybrid 模式获得了对 Morello 架构的支持。
在尝试这个移植时,面临了很多挑战。第一步是重新映射 intptr_t type(这是描述一个用来容纳指针值的整数)。当然,这里要解决的核心问题就是普通的 long 形不够用来放置 capability;如果试图使用从 long 中提取的指针,都会在运行时被捕获。因此,在按这种方式使用此类型的代码在 CHERI 系统中就无法运行了。对指针进行比较,也比较棘手;两个指向同一位置的 capability 可能有不同的访问权限,因此两者逐 bit 比较的话并不完全相等,但在指针比较的时候应该判定它们是相等的。
Coplan 说,尽管有这些陷阱,但大多数代码在 pure-cap 模式下编译就可以正常工作。不过,那些非常底层的软件代码则可能需要大量的修改,尤其是那些把玩指针的代码可能会有问题。例如,在 GCC 中有一个排序功能,它会利用指针完成一些技巧,这就会导致编译器报错。
要教会 GCC 能了解 capability,就是一个很大的挑战,需要对整个代码进行许多修改。它们打破了 GCC 内部的两个基本假设:pointer 指针和 integer 整数是完全可以直接互相替换的,以及假设地址和 offset 本质上是同一种性质的。编译器必须确保所有指针都有正确的出处,以及确保指针比较要给出正确的结果,等等。
GCC 的工作最开始是增加了一个 -mfake-capability 选项,这使得编译器在其内部表示中可以一直使用 capability 方式,但仍像以前一样利用 backend 来生成标准代码。这使得 capability 的概念与 Morello 架构的具体细节可以隔离开了。开发人员首先努力使所有的地方都能使用这个 flag,然后才着手生成能在真实硬件上使用 capability 的代码。
剩下的一个问题是,capability provenance 中的地址范围的边界是用压缩浮点表示法来存储的;否则,这些信息就没法塞在那些 bits 里了。但是这就导致,无法把所有的地址、base 和 limit 的组合都被表示出来。Coplan 说,这个问题主要对内存分配器有影响,必须要能对更大范围的内存进行操作。
Porting glibc
Nagy 接着谈到了为了把 glibc 移植到 Morello 架构上所做的工作。这项工作目前是在一个单独的分支中进行的,没有计划将其提交到上游,需要等到可以创建一个更干净的 patch set 再说了。他说,需要对 glibc 进行大量修改;CHERI C 是一种完全不同的语言。比如说需要把所有操作指针的代码都修改掉。
更细节的问题是,必须对 memcpy()等基本函数进行特殊处理。除非特别小心,否则存储在要复制的内存中的任何 capability 在复制目标那里都会完全无效。syscall()必须返回 intptr_t 类型(这是一个 ABI 变动)才能成功返回一个指针,其他许多系统调用也需要类似的改变。
然后是 capability derivation (衍生)的问题。当内核启动一个进程时,该进程获得了一个能够访问整个用户空间的一个 capability。当然,有可能可以继续使用这个 capability 来完成所有一切工作,这是第一步,但它并没有真正利用上 capability 机制。所以下一步是在不需要访问所有内存的特定情况下来缩小 capability。
还有一个更棘手的问题,就是动态链接器 dynamic linker,它对 64 位地址进行了大量操作。不过,它是与 ELF 二进制文件配合工作的,所以它的大部分工作是以 "base 加 offset" 的形式进行计算的。如果 base 值是一个正确的 capability,那么一切都可以正常工作。随着最初的问题被克服,下一阶段是开始将允许写入的 capability 与允许执行的 capability 区分开;这将需要在 pointer derivation 中更加谨慎地处理,他说。
然后是 malloc() 的问题。理想情况下,这个函数将返回一个可以访问到被分配对象的指针(不可以访问到其他任何东西)。在第一阶段,并没有实现缩小访问范围的工作;工作重心是希望让基本功能可以正常工作起来。第二阶段则要把返回的指针的范围缩小到只有相关的对象的范围。但这给 free() 带来了挑战,它必须能够访问存储在这个对象本身之外的 metadata;这要给 free() 单独准备一个特殊的 capability 来使用。他说,要使所有这些工作顺利进行的话,还有其他各种挑战;他将为 CHERI 实际上设计了专用的 malloc()接口。
他说,目前 test suites 可以运行,但只有在 GCC 中不使用 stack bound 的情况下才能运行。此外还有一些缺失的功能,包括 profiling、支持可执行的 stack、对 LD_AUDIT 的支持,以及对存储在共享内存或通过文件描述符发送的指针的支持。他总结说,后面这些情况可能永远都无法支持起来。
[感谢 LWN 订户支持编者参加这次活动]。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~