Rust 语言因其并发安全性而深受众多开发者的喜爱,曾在多个榜单上获评最受欢迎编程语言。然而,现在有人花费大量时间编写 10 万行 Rust 代码之后,撰写博客阐明 Rust 语言的一系列缺点,以下是博客的主要内容。
我深入研究 Rust 是为了改进由 Xobs 编写的 Xous 操作系统。Xous 是一个用纯 Rust 编写的微内核消息传递操作系统,是为了轻量级 (IoT / 嵌入式规模) 的安全优先平台(例如 Precursor)而编写的,用于 MMU 的硬件强制型页面级内存保护。
一年来,我们为 Xous 操作系统添加了许多功能,包括网络 (TCP/UDP/DNS)、用于模态和多语言文本的中间件图形抽象、存储(以加密的形式)、PDDB、可信启动(trusted boot)以及密钥管理库等。
我们决定编写自己的操作系统而不是使用 SeL4、Tock、QNX 或 Linux 等现有实现,是因为我们想真正了设备中每一行代码都在做什么。特别是对于 Linux,它的源代码库非常庞大且动态,即使开源,也不可能搞清其内核中的每一行代码。因此,Xous 仅支持我们的平台,以尽可能避免内核不必要的复杂性。
这样减少应用范围还意味着我们还可以充分利用 CPU 在 FPGA 中运行的优势 。因此,Xous 以一种不寻常的 RV32-IMAC 配置为目标:具有 MMU + AES 扩展的配置。
FPGA 意味着我们有能力在硬件级别上修复 API 错误,从而使内核更加精简。这对于从 RAM 中处理诸如挂起和恢复之类的抽象破坏(abstraction-busting)进程尤其重要。
我们创建 Xous 时研究了大量的系统编程语言,最终 Rust 脱颖而出。当时它刚刚开始支持 `no-std`,它的特点是强类型、内存安全,具有良好的工具和新型生态系统。我个人是强类型语言的忠实拥护者,而内存安全性不仅有利于系统编程,还能使优化器更好地生成代码,并且 Rust 适用于并发。
实际上,我希望 Precursor 有一个支持标记指针和内存功能的 CPU,类似于 CHERI。于是我们和 CHERI 研发团队进行了一些讨论,但显然他们非常专注于 C 语言,也没有足够的带宽来支持 Rust。总体而言,C 比 Rust 需要 CHERI 多得多,他们的选择是符合资源优先原则的。我们不使用 C 语言,但出于安全性考虑,我希望有一天 Rust 中会存在硬件强制型胖指针(fat pointer)。
然而,Rust 语言绝不是完美的,甚至给我们的开发带来了很多问题。下面我列举一下 Rust 的缺点。
我发现 Rust 语法密集、繁重且难以阅读,例如:
Trying::to_read::<&'a heavy>(syntax, |like| { this. can_be( maddening ) }).map(|_| ())?;
简单来说,上面的代码类似于在对象(实际上是 `struct`)上调用一个名为「to_read」的方法。
还有一种不遵循 Rust 语法规则的宏和指令也能运行:
上面的语句中最令我困惑的是使用‘=’来表示等价而不是赋值,因为配置指令中的内容不是 Rust 代码,它就像一个完全独立的元语言。
再比如,Rust 宏的可读性也存在问题——即使是我自己编写的一些 Rust 宏也「只是勉强工作」。
Rust 的确很强大,它的标准库中包含 HashMaps、Vecs 和 Threads 等数据结构,丰富且可用性高。然而,Rust 的「std」库并没有为我们构建可审计的代码库带来任何好处。
我们编写 Xous 的代码时,引入了一个叫作「const generic」的新类型。在此之前,Rust 没有原生能力来处理多于 32 个元素的数组,这个限制令人抓狂。
在编写 Xous 的过程中,Rust 的内联汇编、工作空间等功能逐渐成熟,这意味着我们需要重新审视已经写好的代码,以使关键的初始启动代码集成进我们构建的系统。
Xous 开发的第一年都是使用’no-std’完成的,代价是占用大量内存空间且复杂性高。尽管可以编写一个只有预先分配的、静态大小的数据结构的操作系统,但为了适应最坏情况下的元素数量,因此我们不得不推出一些自己的数据结构。
大约一年前,Xobs 将 Rust 的 `std` 库移植到 Xous,这意味着我们可以在稳定的 Rust 中访问堆,现在 Xous 与特定版本的 Rust 绑定。
`std` 库从根本上将内存分配、线程创建等「不安全」的硬件结构转变成了「安全」的 Rust 结构。
然而,我必须不断提醒自己,拥有 `std` 库并不能消除关键代码中的安全漏洞风险——它只是将许多关键代码移动到标准库中。
Rust 有固定的更新周期,这意味着我们也必须定期更新 Xous ,以保持与语言的兼容性。
但这可能是不可持续的。最终,我们需要锁定代码库,但我没有明确的退出策略。也许我们可以考虑仍然使用 `no-std` 以获得稳定的 `alloc` 功能来访问堆。但这样我们就还需要使用 Vec、HashMap、Thread 和 Arc/Mutex/Rc/RefCell/Box 构造等,以使 Xous 能够被有效编码。
`curl --proto '=https' --tlsv1.2 -sSf https:
用户可以下载脚本并在运行之前对其进行检查,这似乎比 vscode 的 Windows .MSI 安装程序好得多。但是,这种做法遍及整个构建生态系统,让我对通过 crates.io 生态系统发起的软件供应链攻击的可能性感到不安。
Crates.io 也存在一种拼写错误,很难确定哪些 crate 是好或坏;一些完全按照用户想要的名称命名的 crate 放弃提供所需功能,而积极维护的 crate 必须采用不太直观的名称。当然,这不是 Rust 独有的问题。
还有一个事实是,依赖项是链式的。也就是说当你从 crates.io 拉入一个东西时,你也会拉入该 crate 的所有从属依赖项,以及它们所有的 build.rs (http://build.rs/) 脚本,这些最终都将在你的机器上运行。因此,仅审核 Cargo.toml 文件中明确指定的 crate 是不够的——您还必须审核所有相关 crate 是否存在潜在的供应链攻击。
幸运的是,Rust 确实允许您使用 Cargo.lock 文件将 crate 固定在特定版本,并且可以完全指定依赖 crate 。我们试图在 Xous 中通过发布 Cargo.lock 文件并将我们所有的一阶相关 crate 指定为次要修订的策略来缓解这个问题。
然而,我们的大部分调试和测试框架都依赖于一些相当花哨和复杂的 crate,这些 crate 引入了大量的依赖项,即使我尝试为我们的目标硬件运行构建,在主机上运行的依赖 crate 和 build.rs 脚本还是被构建。
针对这个问题,我编写了一个名为「crate-scraper」的小工具,它为我们的 Cargo.toml 文件中指定的每个源下载源包,并且将它们存储在本地,这样我们就可以获得用于构建 Xous 版本的代码快照。
它还运行一个快速的「分析」程序——搜索名为 build.rs 的文件并将它们整理到一个文件中,这样我就可以更快地通过 grep 查找明显的问题。当然,手动审查并不是检测嵌入在 build.rs (http://build.rs/) 文件中巧妙伪装的恶意软件的实用方法,但它至少让我了解了我们正在处理的攻击面的规模。令人惊讶的是,我们审查出来自各种第三方的大约 5700 行代码,用于操作文件、目录和环境变量,并在我的计算机上运行其他程序。
我不确定这个问题是否有更好的解决方案,但是,如果你的目标是构建可信赖的固件,请警惕 Rust 广泛的软件供应链攻击面。
我对 Rust 的最后一点看法是,一台计算机上的构建无法在另一台上复现。
我认为这主要是因为 Rust 将源代码的完整路径作为内置到二进制文件中调试字符串的一部分。这导致了一些糟糕的情况,例如我们在 Windows 上构建的工作成功了,但在 Linux 下却失败了,因为二者的路径名非常不同,这会导致一些内存对象在目标内存中被转移。
公平地讲,这些失败是由于 Xous 中存在错误,这些错误已经得到修复。但是,最终仍会有用户向我们报告我们无法复现,因为他们在构建系统上的路径与我们的不同。
最后,我想说尽管这里列出了所有的怨言,但如果能重来,Rust 仍然是我们用于构建 Xous 所用语言的有力竞争者。我用 C、Python 和 Java 完成了很多大型项目,所有这些项目最终都背负着「不断增加的技术债务」,而 Rust 可以规避这些问题。
© THE END
转载请联系本公众号获得授权
投稿或寻求报道:content@jiqizhixin.com