Rust 接棒 C 语言:盘点那些用 Rust 重写的知名 C 项目
“写本文的目的并不是为了吹捧 Rust ,而是为了让一些想采用 Rust 的公司,能看到更多的案例来参考。本系列文章第一篇参见:《Rust 接棒 C 语言 :Rust for Linux 中正在发生的技术变革》
前文介绍了 Rust for Linux 中正在发生的 Rust 接棒 C 语言的变革,本文继续来探索还有哪些知名的 C 项目是用 Rust 重写了。
sudo-rs
sudo-rs[1] 是 Prossimo 项目的一部分,由 ISRG 主导开发,并得到 NLNet 基金会的资助进行独立安全审核。开发团队是由 Ferrous Systems 和 Tweede Golf 团队组成的团队。
sudo 工具为类 Unix 系统(例如 Linux 和 FreeBSD)的特权用户提供了以 root 身份运行命令的方式。它存在一定的风险,因为低权限的恶意用户或软件可能会找到滥用它的方法,例如利用代码中的漏洞来提升他们的访问权限到 root 或超级用户级别。理想情况下,sudo 和 su 应该尽可能安全和无漏洞,因为它们作为控制系统的完全控制的入口。
““ sudo 命令算是一个典型的安全关键工具,它既普遍存在又不被重视。对于这类工具的安全改进将对整个行业产生巨大影响。” —— Chainguard (一家网络安全公司)的首席执行官兼联合创始人丹·洛伦克(Dan Lorenc)
根据 ISRG 的 Prossimo 项目执行主任 Josh Aas 的说法,原始 sudo 中三分之一的安全漏洞源于内存管理问题。
在 2023 年 8 月,`sudo-rs` 首次发布了 release 版本[2]。在用 Rust 重写 sudo-rs
时还带来了附加收益:sudo-rs
开发了一个测试套件,帮助发现了原始sudo
C 实现中的错误。因为 sudo
已经是一套非常成熟的软件了,用 Rust 对其重写,需要覆盖一份完整的功能测试套件。
在 2023 年 9月4日至9月15日,ROS(**Radically Open Security[3]**)对 sudo-rs 进行了水晶盒渗透测试,目的是验证在没有适当身份验证的情况下无法执行特权操作。此次审计是在 sudo-rs
代码库的 b5eb2c6
分支版本上于进行的,点此查看完整审计报告[4]。
ROS 团队发现了一个中等严重性问题和两个低严重性问题:
CLN-001:相对路径遍历漏洞(中等) CLN-003:Cargo 配置不会剥离(strip)符号(symbol)(低) CLN-004:对 chown
调用的默认权限设置不正确(低)
除了这些发现之外,ROS 还对 sudo-rs 代码库的不同组件进行了模糊测试,但没有发现任何问题。
pendulum
Pendulum[5] 同样是 Prossimo 项目的一部分,由 ISRG 主导开发,2023年7月,主权科技基金( Sovereign Tech Fund[6])投资了Pendulum,确保了2023年的开发和维护,并在2024年进行维护和采用工作。Pendulum 项目包含 Statime(PTP)和 ntpd-rs(NTP)两个项目,旨在构建现代化、开源的网络时间协议和精确时间协议的实现,以达成下面两个目标:
提供可靠的时间同步 易于扩展以适应未来的时间标准改进
“在数百万设备和服务器上运行的 NTP 和 PTP 是互联网和其他关键基础设施的重要组成部分:金融和广播领域,电网和电信领域以及安全协议中。
“网络时间协议(NTP)是一种用于在网络中与计算机时钟时间源进行同步的互联网协议。它属于TCP/IP套件的最古老的部分之一。术语NTP既适用于协议,也适用于在计算机上运行的客户端-服务器程序。David Mills 是特拉华(Delaware)大学的教授,于1981年开发了NTP。它被设计成高度容错和可扩展的,同时支持时间同步。
NTP
NTP时间同步过程涉及以下三个步骤:
NTP客户端与NTP服务器发起时间请求交换。 客户端随后能够计算链路延迟和本地偏移,并调整本地时钟以与服务器计算机上的时钟相匹配。 通常情况下,需要进行大约五到十分钟的六次交换来最初设置时钟。
一旦同步,客户端大约每10分钟更新一次时钟,通常只需要一次消息交换,除了客户端与服务器的同步。此事务通过用户数据报协议(UDP)在端口123上进行。NTP还支持对对等计算机时钟进行广播同步。世界各地有成千上万个NTP服务器。它们可以访问高精度的原子钟和全球定位系统钟。需要专门的接收器与NTP服务器直接通信以获取时间服务。
计算机网络上所有设备的准确时间对许多原因都很重要;即使是一秒钟的差异也可能引发问题。所以 NTP 的安全健壮和性能非常重要。
ntpd-rs[7] 是一个完全使用 Rust 编写的开源实现的网络时间协议,重点是暴露最小的攻击面。应该已经支持 NTPv5 协议。
2023 年 5 月,`ntpd-rs` 经过 ROS 的安全审计[8]。2023 年 5 月,`ntpd-rs` 经过 ROS 的安全审计[9]。
审计发现了该项目的首个 CVE[10] : ntpd-rs在接收到的NTP数据包中不验证NTS cookie的长度,攻击者可以通过发送一个特制的NTP数据包来使服务器崩溃,该数据包中包含的cookie长度小于服务器预期的长度。当服务器未配置处理NTS数据包时,也会导致服务器崩溃。这个 CVE 不算是内存安全问题,但它会有 DDos 风险。
此外,NTP 的NTS(网络时间安全)扩展使用 TLS 在 NTP 服务器和客户端之间建立可信链接。这意味着一些敏感的安全密钥被保存在内存中,可能会被攻击者提取出来。虽然这种攻击很难实施,但审计建议进一步增加其难度。
当密钥被丢弃时, zeroize
crate 确保存储密钥的内存被设置为零。然而,这并不能完全保证密钥不再存在于内存中,因为 Rust 允许移动内存。密钥字节将保留在原始位置。所以要确保敏感数据不被不必要地复制或移动,以及及时清理所有可能的副本,是提高安全性的关键考虑因素。
PTP
NTP(Network Time Protocol)是用于不同计算机之间同步时钟的网络协议。 它的设计目标是使所有的互连的机器之间的时钟与UTC时间只相差若干毫秒。
PTP是英文 “Precision Timing Protocol” 的缩写,即“精确时间协议”。PTP 的设计目标是使机器之间的时钟偏差在亚微秒级(sub-microsecond)范围。
PTP 管理的时钟遵循主从层次结构。从属时钟将同步到其主时钟。层次结构由每个时钟上运行的最佳主时钟 (BMC) 算法更新。只有一个端口的时钟可以是主时钟也可以是从属时钟。此类时钟称为普通时钟 (OC)。具有多个端口的时钟可以是一个端口上的主时钟以及另一个端口上的从属时钟。此类时钟称为边界时钟(BC)。顶层主时钟称为_超级主时钟_。超级主时钟可与全球定位系统 (GPS) 同步。这样,不同的网络便能够以高准确度实现同步。
PTP实现主要分为硬件和软件两种方式。硬件支持是 PTP 的主要优势,通常在网络性能和安全性要求高的场合下,采用硬件实现方式,其他采用软件方式。各种网络交换机和网络接口控制器 (NIC) 都支持 PTP。虽然可以在网络内部使用启用非 PTP 的硬件,但为所有 PTP 时钟之间的网络组件启用 PTP 硬件可实现最大的准确性。
通过 PTP,我们可以实现很多高精度的实时应用,如音视频传输、金融交易、无人驾驶车辆等。同时,PTP还广泛应用于工业自动化领域中,通过对工业控制系统进行精确的时间同步,能够实现高效、高质量的自动化生产。此外,PTP 还可以帮助网络管理员检测和处理网络延迟,通过确定延迟或时钟偏移,对网络进行实时监测和通信优化。
Statime[11] 是Rust 实现的精确时间协议(PTP)的开源实现。高精度定时是关键的网络基础设施的一部分。通过 Statime 为现有实现提供了一个内存安全的替代方案。PTP 用 Rust 实现的优势在于,软件和硬件支持模块都是 Rust 实现,均可利用 Rust 优势。
Binder
2023 年 11 月,Google Android 团队宣布,已经将 Android 的 Binder 代码重新用 Rust 实现,并已提交到 Linux 内核中。
Binder 负责 Android 上的进程间通信(IPC)和其他任务,用内存安全的Rust代码替换它应该是系统安全性的重要提升。
在 Google 工程师发到 Linux 内核邮件列表的 RFC[12] 中写道:“我们通常不赞同重写,但是... ...”。为什么用 Rust 重写?
Binder 在过去的 15 年中不断发展,以满足 Android 不断变化的需求。在这段时间里,它的责任、期望和复杂性都大大增加。虽然团队期望 Binder 能够与 Android 一起不断发展,但目前有一些因素限制了开发和维护它的能力。简要来说,这些因素包括:
复杂性:Binder 处于 Android 的各个交叉点上,并承担了许多超出 IPC 之外的责任。它对于不同的人来说有着不同的用途,由于其众多的功能及其相互作用,其复杂性相当高。仅仅在6kLOC 中,它必须将事务传递到正确的线程。它必须正确解析和转换事务的内容,其中可能包含多个不同类型的对象(例如指针、文件描述符),这些对象可以相互交互。它控制用户空间中线程池的大小,并确保事务以避免线程池耗尽线程的方式分配给线程。它必须正确地在多个进程之间转发共享对象的引用计数更改。它必须处理众多的错误场景,并结合/嵌套了13个不同的锁、7个引用计数器和原子变量。最后,它必须以尽可能快速和高效的方式完成所有这些工作。即使是轻微的性能回退也会导致用户体验明显下降。 改进的事项:随着代码库的有机增长,可能会出现千行函数、容易出错的错误处理和混乱的结构。经过十多年的开发,这个代码库需要进行全面改进。 安全关键:Binder 是 Android 沙盒策略的关键部分。即使是 Android 最低权限的沙盒(例如Chrome 渲染器或 SW 编解码器),也可以直接访问 Binder。与其他任何组件相比,Binder 提供强大的安全性并且本身对安全漏洞具有鲁棒性非常重要。 使用Rust,因为它直接解决了Google Android 团队在过去几年中在 Binder 中遇到的一些挑战。它可以防止引用计数、锁、边界检查等方面的错误,并且在错误处理方面也做了很多工作来降低复杂性。此外,还能够使用更具表达力的类型系统来编码各种结构体和指针的所有权语义,这将管理对象生命周期的复杂性从程序员手中解放出来,减少了使用后释放和类似问题的风险。
Rust 在类型系统中使用许多不同的指针类型来编码所有权语义,这可能是它在Binder中帮助的最重要的方面之一。Binder 驱动程序有许多具有复杂所有权语义的不同对象;一些指针拥有引用计数,一些指针具有独占所有权,而一些指针只是引用对象,并以其他方式保持其活动状态。使用 Rust,可以为每种指针使用不同的指针类型,这使得编译器能够强制执行正确实现所有权语义。
另一个有用的功能是 Rust 的错误处理。Rust 允许使用诸如析构函数之类的功能来简化错误处理,并且如果错误没有得到正确处理,编译将失败。这意味着尽管 Rust 要求你编写比 C 更多的代码行,例如写下在 C 中隐含的不变量,但 Rust 驱动程序仍然比 C 绑定器稍微小一些:Rust 为5.5kLOC
,C 为5.8kLOC
。(这些数字不包括空行、注释、binderfs 以及 Rust 驱动程序中尚未实现的任何 C 调试工具。这些数字包括rust/kernel/
中的抽象,这些抽象不太可能被除 Binder 以外的其他驱动程序使用)。
尽管这次重写完全重新思考了代码的结构和强制执行的假设方式,但我们并没有从根本上改变驱动程序执行任务的方式。对现有设计进行了很多仔细的思考。重写的目的是改善代码的健康性、结构性、可读性、健壮性、安全性、可维护性和可扩展性。我们还增加了更多的内联文档,并改善了代码中对假设的强制执行方式。此外,所有 Unsafe 的代码都用一个“SAFETY”注释进行了标注,解释了其正确性。
PubNub
PubNub[13] 致力于打造一种先进的边缘网络消息系统,用于构建任何实时功能的组合,包括聊天、实时观众参与、多用户协作、设备控制、数据流传输和地理位置/调度等。PubNub 的规模巨大,每个月设备连接数超过 8 亿,每月 API 调度超过 3万亿次,5个9的服务级别( 99.999%正常运行时间服务)。
PubNub 之前是用 C 写的,他们花费很多时间和精力做到了服务的稳定和高性能。但为什么要转向 Rust 呢?在最新的一期访谈[14]中,PubNub 的 CTO 畅谈了这个问题。
大约五年前 PubNub 内部就开始探索 Rust,通过逐步的探索他们发现,Rust 在内存安全性和接近 C 语言速度的性能方面非常吸引人,尤其在 PubNub 如此巨量规模下更加吸引人,基本上这也是为什么 PubNub 引入 Rust 的主要原因。
在之前使用 C 的过程中,PubNub 团队经常遇到“段错误”。出现这种情况通常意味着可能会有数据损坏或丢失,这是个大问题。C 语言性能强劲,节省硬件成本,但是 C 却没有节省工程成本。对于 PubNub 这样大规模的系统,工程成本远远大于硬件成本。另外,PubNub 也是利用的所有招聘方式想找到 C 高手,因为即使是十年前,找到 C 语言专家就已经是一个挑战了。即便是找到了一个 C 专家,他也可能不想再写 C 了。在 PubNub 中,必须编写超级稳定的 C 代码,然而,作为一个 C 开发者,遇到段错误或其他类似的东西,这是一个必经之路,这是一定会发生的事情。是的,问题不是会不会发生,而是什么时候发生。
PubNub 也尝试过使用 Go 语言来重写 PubSub(发布/订阅)总线的一部分,但性能远远比不上 C。即使在低负荷下,延迟也立即慢了 10 倍。然后还有 GC 暂停,所以延迟会周期性地突然增加。所以后面换为了 Rust 。
现在,Rust 是 PubNub 最受欢迎的语言,到目前为止,PubNub 所有的新服务通常都选择用 Rust 编写,未来所有的服务都将是 Rust,这是因为 PubNub 的规模,他们已经从中看到了出色的结果。
ockam
Ockam[15] 是一套开源编程库和命令行工具,用于在大规模环境中协调端到端加密、相互认证、密钥管理、凭证管理和授权策略执行。
“Ockam 与InfluxData的首席技术官Paul Dix一起讨论了InfluxDB和Ockam 为什么用 Rust 重写的视频[16] ,对,InfluxDB 也用 Rust 重写了,只不过它是从 Go 转向 Rust。
在 Ockam 的早期阶段使用 C 语言开发,然后在几个月后决定放弃那数万行 C 代码并改用Rust 重写[17]。下面是 Ockam 用 Rust 重写 C 的故事。
2019年,Ockam 开始用C语言构建,并希望 Ockam 能够在各种设备上运行,从受限的边缘设备到强大的云服务器。他们还希望 Ockam 能够在任何类型的应用程序中使用,不论该应用程序使用的是哪种语言构建。
基于这个目标,在当初 C 语言成为该团队构建这个系统的首选。因为 C 可以编译为99%的计算机,并且几乎可以在任何地方运行(一旦你弄清楚如何处理所有特定目标的工具链)。而且,所有其他流行的语言都可以通过某种本地函数接口调用 C 库。
在 Ockam 的核心是一组分层的加密和基于消息的协议,如 Ockam 安全通道和 Ockam 路由。这些是异步的、多步骤的、有状态的通信协议,他们希望将这些协议的所有细节从应用程序开发人员中抽象出来。他们想象中的用户体验是一个单行函数调用,用于创建端到端的身份验证和加密的安全通道。
然而,与加密相关的代码也往往存在很多隐患,一点小错误就会导致系统不安全。因此,简洁对 Ockam 来说不仅仅是一种审美理念,还是确保能够赋予每个人构建安全系统的关键要求。Ockam 希望将这些安全隐患隐藏起来,并以易于正确使用且难以损害应用程序的方式提供接口给开发者。
这就是 C 语言严重不足的地方。Ockam 团队在 C 语言中尝试暴露安全简单的接口并不成功。在每次迭代中,他们发现应用程序开发人员需要了解太多关于协议状态和状态转换的细节。
与此同时,他们也用 Elixir 语言创建了一个 Ockam 安全通道覆盖 Ockam 路由的原型。Elixir 程序运行在 BEAM 上,即 Erlang 虚拟机。BEAM 提供了 Erlang 进程。Erlang 进程是轻量级的有状态并发执行者。由于执行者可以在并发运行的同时保持内部状态,因此可以轻松运行一组有状态的协议堆栈 :Ockam传输 + Ockam路由 + Ockam安全通道。这样就做到了能够隐藏所有有状态的层,并创建一个简单的一行函数,任何人都可以调用它来创建一个端到端加密的安全通道,可以通过多跳、多协议的路由。应用程序开发人员将调用此简单函数,多个并发的执行者将运行底层的有状态协议。当通道建立或出现错误时,函数将返回。这正是 Ockam 团队想要的接口。
但是 Elixir 不像 C 语言。它在小型/受限制的计算机上(嵌入式)运行效果不好,也不适合用特定语言的习惯进行包装。
所以,上述问题促使该团队开始尝试探索 Rust 语言。于是,Rust 的一些特性很快吸引了他们:
与C-ABI 调用约定的兼容性。Rust 库可以导出与 C 的调用约定兼容的接口。这意味着任何能够静态或动态链接并调用 C 库中函数的语言或运行时环境也可以以完全相同的方式链接并调用Rust 库中的函数。由于大多数语言支持 C 中的本地函数,它们也已经支持 Rust 中的本地函数。这使得从我们需要在核心库周围具有特定于语言的包装器的角度来看,Rust 与 C 是等价的。 跨平台支持。Rust 使用 LLVM 进行编译,这意味着它可以针对非常多的计算机进行目标编译。这个集合可能没有 C 语言使用 GCC 和各种专有 GCC 分支所能覆盖的那么大,但仍然是一个非常大的子集,并且正在进行工作,使 Rust 能够与 GCC 一起编译。随着对新的 LLVM 目标的不断支持和 Rust 在 GCC 上的潜在支持,从我们能够在任何地方运行的需求来看,这似乎是一个不错的选择。 强类型和强大的类型系统。Rust 的内存安全特性消除了使用后释放、双重释放、溢出、越界访问(非编译时)、数据竞争和许多其他常见错误的可能性,这些错误已知会导致大型 C 或 C++ 代码库中 60-70% 的高严重性漏洞。Rust 在编译时提供了这种安全性,而不需要使用垃圾回收器在运行时安全地管理内存,从而避免了性能开销。这使得 Rust 在编写需要高性能、在受限环境中运行和高度安全的代码方面具有重要优势。 async/await
异步编程和可插拔的异步运行时。最后一个让该团队相信 Rust 非常适合 Ockam 的特性是async/await
。Ockam 已经确定需要轻量级的 Actor 来创建Ockam协议栈的简单和安全接口。基于 Rust 生态的 tokio 和 async-std ,可以轻松构建 Ockam 的 Actor 实现。另一个显著的重要方面是,在Rust中,async/await
与其他语言(如Javascript)中的async/await
有一个重要的区别,就是它的异步运行时(tokio/async-std)是可插拔的。当在嵌入式环境下,也可以选择更轻量的异步运行时。这就意味着,无论它在哪里运行(大型计算机或小型计算机),都可以向用户呈现完全相同的接口。Ockam 所有基于Ockam Workers的协议接口也可以呈现完全相同的简单接口,无论它们在哪里运行。
基于以上的优点,Ockam 决定用 Rust 重写整个项目。
后记
写本文的目的并不是为了吹捧 Rust ,而是为了让一些想采用 Rust 的公司,能看到更多的案例来参考。
本文记录的只是冰山一角。相信,将来大家会看到越来越多的案例。
感谢阅读!祝 2024 年元旦快乐!
参考资料
sudo-rs: https://github.com/memorysafety/sudo-rs
[2]在 2023 年 8 月,sudo-rs
首次发布了 release 版本: https://www.memorysafety.org/blog/sudo-first-stable-release/
Radically Open Security: https://www.radicallyopensecurity.com/
[4]点此查看完整审计报告: https://github.com/memorysafety/sudo-rs/blob/audit-report/docs/audit/audit-report-sudo-rs.pdf
[5]Pendulum: https://tweedegolf.nl/en/pendulum
[6]Sovereign Tech Fund: https://www.sovereigntechfund.de/
[7]ntpd-rs: https://github.com/pendulum-project/ntpd-rs
[8]ntpd-rs
经过 ROS 的安全审计: https://tweedegolf.nl/en/blog/94/report-ntp-security-audit
ntpd-rs
经过 ROS 的安全审计: https://tweedegolf.nl/en/blog/94/report-ntp-security-audit
首个 CVE: https://github.com/pendulum-project/ntpd-rs/security/advisories/GHSA-qwhm-h7v3-mrjx
[11]Statime: https://github.com/pendulum-project/statime
[12]RFC: https://lore.kernel.org/lkml/20231101-rust-binder-v1-0-08ba9197f637@google.com/
[13]PubNub: https://www.pubnub.com/
[14]最新的一期访谈: https://corrode.dev/podcast/s01e02-pubnub/
[15]Ockam: https://github.com/build-trust/ockam
[16]Ockam 与InfluxData的首席技术官Paul Dix一起讨论了InfluxDB和Ockam 为什么用 Rust 重写的视频: https://www.influxdata.com/resources/meet-the-founders-an-open-discussion-about-rewriting-using-rust/
[17]然后在几个月后决定放弃那数万行 C 代码并改用Rust 重写: https://www.ockam.io/blog/rewriting_in_rust