说明:本篇文章不是论文的翻译,而是本人对该论文的梳理和总结。
引子
美国佐治亚理工学院的系统软件安全实验室[1]开源了`Rudra`[2] ,用于分析和报告 Unsafe Rust 代码中潜在的内存安全和漏洞,为此他们也将在 2021 年第 28 届 ACM 操作系统原则研讨会论文集上发表相关论文,该论文目前在 Rudra 源码仓库中提供下载[3]。
概要
Rust 语言关注内存安全和性能,Rust 目前已经在传统的系统软件中得到了广泛的应用,如操作系统、嵌入式系统、网络框架、浏览器等,在这些领域,安全和性能都是不可或缺的。
Rust 内存安全的思想是在编译时验证内存的所有权,具体而言是验证内存分配对象的访问和生存期。Rust 编译器对值的共享和独占引用通过借用检查提供两个保证:
- 引用的生存期不能长于其拥有者变量的生存期。为了避免 use-after-free (UAF) 。
- 共享和独占引用不能同时存在,排除了并发读写同一个值的风险。
不幸的是,这些安全规则太过限制。在某些需要调用底层硬件系统,或需要获得更好性能时,需要暂时绕过安全规则。这些需求无法被 Safe Rust 解决,但是对于系统开发却是必不可少的,所以 Unsafe Rust 被引入。Unsafe Rust 意味着,编译器的安全检查职责被暂时委托给了程序员。
Unsafe Rust代码的健全性(soundness )对于整个程序的内存安全是至关重要的,因为大多数系统软件,如操作系统或标准库,都离不开它。
有些人可能比较天真地以为,Unsafe Rust 只要在审查源码的时候就可以排除它的风险。然而,问题的关键在于,健全性的推理是非常微妙的,且很容易出错,原因有三:
- 健全性的错误会顺道破坏Rust的安全边界,这意味着所有的外部代码,包括标准库都应该是健全的。
- Safe 和 Unsafe 的代码是相互依赖的。
- 编译器插入的所有不可见的代码路径都需要由程序员正确推理。
为了让 Rust 有一个健全性的基础,已经有了很多研究型项目,比如形式化类型系统和操作语义,验证其正确性,并且建立模型用于检查。这些都是非常重要的,但还不够实用,因为它没有覆盖到整个生态系统。另外还有一些动态方法,比如 Miri 和 Fuzz 模糊测试,但是这些方法不太容易被大规模使用,因为它需要大量的计算资源。
当前,Rust 语言正在变得流行,Unsafe Rust 的包也逐渐变多。因此,设计一个实用的检测内存安全的算法就很重要了。
这篇论文介绍了三种重要的Bug模式,并介绍了 Unsafe 代码,以及提供 Rudra 这样的工具。该论文作者的工作一共有三个贡献:
- 确定了三种 Unsafe Rust 中的 Bug 模式,并且设计了两种新的算法可以发现它们。
- 使用 Rudra 在Rust 生态系统中发现263个新的内存安全漏洞。这代表了自2016年以来RustSec中所有bug的41.4%。
- 开源。Rudra 是开源的,我们计划 贡献其核心算法到官方的Rust linter中。
Rudra
`Rudra`[4] 用于分析和报告Unsafe Rust 代码中潜在的内存安全漏洞。由于Unsafe 代码中的错误威胁到 Rust 安全保证的基础,Rudra
的主要重点是将我们的分析扩展到 Rust 包注册仓库(比如 crates.io
)中托管的所有程序和库。Rudra
可以在 6.5 小时内扫描整个注册仓库(43k
包)并识别出 263 个以前未知的内存安全漏洞,提交 98 个 RustSec
公告和 74 个CVE
,占自 2016 年以来报告给 RustSec
的所有漏洞的 41.4%。
Rudra
发现的新漏洞很微妙,它们存在于Rust 专家的库中:两个在 std
库中,一个在官方 futures
库中,一个在 Rust 编译器 rustc
中。 Rudra
已经开源, 并计划将其算法集成到官方 Rust linter 中。
“Rudra, 这个名称来自于 梵文,译为鲁特罗(或楼陀罗),印度神话中司风暴、狩猎、死亡和自然界之神。他在暴怒时会滥伤人畜;他又擅长以草药来给人治病。其名意为“狂吼”或“咆哮”(可能是飓风或暴风雨)。
Rudra
和 Miri
的区别 :
“
Rudra
是静态分析,无需执行即可分析源码。Miri
是解释器,需要执行代码。 两者可以结合使用。
关于 Unsafe Rust
因为 unsafe
关键字的存在,引出了一个有趣的 API 设计领域:如何交流 API 的安全性。
通常有两种方法:
- 内部 Unsafe API 直接暴露给 API 用户,但是使用 unsafe 关键字来声明该 API 是不安全的,也需要添加安全边界的注释。
- 对 API 进行安全封装(安全抽象),即在内部使用断言来保证在越过安全边界时可以Panic,从而避免 UB 的产生。
第二种方法,即将 Unsafe 因素隐藏在安全 API 之下的安全抽象,已经成为 Rust 社区的一种约定俗成。
Safe 和 Unsafe 的分离,可以让我们区分出谁为安全漏洞负责。Safe Rust 意味着,无论如何都不可能导致未定义行为。换句话说,Safe API 的职责是,确保任何有效的输入不会破坏内部封装的 Unsafe 代码的行为预期。
这与C或C 形成了鲜明的对比,在C或C 中,用户的责任是正确遵守 API 的预期用法。
比如,在 libc
中的printf()
,当它调用一个错误的指针而导致段错误的时候,没有人会指责它。然而这个问题却导致了一系列的内存安全问题:格式字符串漏洞(format-string vulnerability)。还记得前段时间 苹果手机因为加入一个经过特别构造名字的Wifi就变砖的漏洞否?
而在 Rust 中,println!()
就不应该也不可能导致一个段错误。此外,如果一个输入确实导致了段错误,那么它会被认为是 API 开发者的错误。
Rust 中内存安全Bug 的定义
在 Rust 中有两类 Unsafe 定义:Unsafe 函数 和 Unsafe 特质(trait)。
Unsafe 函数希望调用者在调用该函数时,可以确保其安全性。
Unsafe 特质则希望实现该 trait 的时候提供额外的语义保证。比如标准库里的 pub unsafe trait TrustedLen: Iterator { }
,该 trait 要求必须检查 Iterator::size_hint()
的上界,才能保证 TrustedLen
所表达的“可信的长度”语义。
该论文对 内存安全 Bug 提供了一个清晰的一致性的定义,而非 Rust 操作语义:
定义 1:类型(Type)和值(Value)是以常规方式定义的。类型是值的集合。
定义2:对于 类型 T, safe-value(T)
被定义为可以安全创建的值。例如 Rust 里的字符串是内部表示为字节的数组,但它在通过 安全 API 创建的时候只能包含 UTF-8
编码的值。
定义3:函数 F 是接收类型为 arg(F)
的值,并返回一个类型为 ret(F)
的值。对于多个参数,我们将其看作元组。
定义4:如果 在 safe-value(arg(F))
集合中存在v
(记为:∃