参考自:
论文精要 | 真实世界中Rust程序的安全实践my.oschina.net本文是对世界顶级学术期刊的论文《理解真实Rust程序中的内存和线程安全实践》中的数据和观点的精要萃取,供学习参考和讨论。
论文地址:
《Understanding Memory and Thread Safety Practicesand Issues in Real-World Rust Programs》 https:// blog.mozilla.org/nnethe rcote/2020/04/24/how-to-speed-up-the-rust-compiler-in-2020/ 论文精要 | 真实世界中Rust程序的安全实践本文是对世界顶级学术期刊的论文《理解真实Rust程序中的内存和线程安全实践》中的数据和观点的精要萃取,供学习参考和讨论。
论文地址:
《Understanding Memory and Thread Safety Practicesand Issues in Real-World Rust Programs》 https:// blog.mozilla.org/nnethe rcote/2020/04/24/how-to-speed-up-the-rust-compiler-in-2020/
实际上,该论文团队成员在2019年也发过相似主题的论文:《Fearless Concurrency? Understanding Concurrent Programming Safety inReal-World Rust Software》
地址: https://www. semanticscholar.org/pap er/Fearless-Concurrency-Understanding-Concurrent-in-Yu-Song/225250e14d33ac91b319c1c0001af735d31e3d28
只不过2020的论文调研的面更加广泛。
Rust 虽然是安全语言,但是默认写的代码,尤其是用了unsafe或 写并发代码的时候,还会有安全风险。依赖于开发者对所有权、生命周期的理解,以及API设计的功力。
该论文的目的也是为了帮助更好地完善Rust及其社区,包括周边的工具,比如增强IDE的生命周期可视化、专属的bug检测工具等等。
真实Rust程序的调研范围
论文调研范围涉及:五个开源项目、五个广泛使用的库、两个在线安全漏洞数据库、Rust 标准库中的 850 个 Unsafe 代码的用法、分析了170个Bug。
五个开源项目分别是:
- Servo,Rust实现的浏览器
- Tock,一个实时嵌入式操作系统(tockOS)
- Ethereum,以太坊区块链Rust客户端(parity-ethereum)
- TiKV, Rust 实现的分布式KV数据库,是TiDB的存储底层。
- Redox,Rust实现的下一代桌面级操作系统。
五个广泛使用的库:rand、crossbeam、threadpool、rayon、lazy_static。
两个在线漏洞数据库: https://cve.mitre.org/ 和 https://rustsec.org/
170 个 Bug ,都是和安全相关的,包括 70 个内存 Bug 和 100 个并发 Bug。
为什么要使用 Unsafe
使用 Unsafe 的场景总结为三类:
- 42% 是通过 FFi 方式重用现有的 C/Cpp 库。
- 22% 是为了提高性能。
- 14% 是为了绕过 Rust 的安全规则在线程间共享全局变量。
使用 Unsafe 进行操作的类型主要是:
- 66% 用于内存操作。比如裸指针操作和类型转换。
- 29% 用于调用 unsafe 函数
也发现某些程序员为了代码看上去保持一致性,在不需要 Unsafe 的地方也加了 Unsafe 块(unsafe block),这大概占 Unsafe 使用总数的 5%。
某些程序员用 Unsafe 关键字来发起警告,在使用该函数时注意 UB 风险。这其实也是 Rust 官方团队对于 编写 Unsafe Rust 时的推荐做法,对于使用了 Unsafe 操作,但是没有做边界检查的函数必须使用 unsafe 关键字标识。
这个统计结果表明:
- 有些场景必须使用 Unsafe 代码,比如 FFi。
- 有些场景没必要使用 Unsafe,比如提高性能的时候,也不要太盲目,要对比 Safe 代码的性能看是否够用,就可以减少不必要的 Unsafe。
- 有些场景属于滥用 Unsafe,比如使用 Unsafe 绕过并发安全检查。
重构过程中为什么删除 Unsafe 代码
统计:
- 提高内存安全性:占 61%
- 提升代码结构:占 24%
- 提高线程安全性:10%
- 为了修复 Bug:3%
- 删除不必要的使用:2%
如何封装 Unsafe 代码
论文作者们对 Rust 标准库中 250 个 Unsafe 函数进行了统计,得出了三条建议:
- 如果一个函数的安全性取决于它的使用方式,那么请将其标记为 unsafe 函数。
- 尽量最小化 unsafe 接口
- 使用内部可变性的时候需要小心,可能会绕过 Rust 的安全检查
并且发现 Rust 标准库中使用 unsafe 函数的时候,都遵循同样模式:调用 unsafe 函数的时候检查使用条件。这其实就是 Rust 官方也建议的「安全抽象」。只不过论文作者本着从实践中求证的精神,得出的结论和官方的建议也是一致的。
标准库中稳定的 unsafe API 的安全使用条件大都满足下面两类:
- 69% 的内部 unsafe 代码都需要有效的内存空间或有效的 UTF-8 字符
- 15% 要求合法的生命周期和所有权条件
标准库中其实也有 5 例使用 unsafe 不恰当的地方,虽然未引起 Bug,但还有潜在的安全问题。
内存安全问题
论文团队调研了 70 个 Rust 内存安全问题及其详细的修复过程,从两个维度对 Bug 进行了分析:错误的传播性和影响力。
内存 Bug 分类及产生原因:
- 缓冲区溢出(Buffer Overflow)。在统计的 21 个内存 Bug 中,有 17 个遵循相同的模式: 在 Safe 代码中计算缓冲区大小或索引时发生了错误,然后在 Unsafe 代码中发生了越界访问。
- Null 。
- 解引用空指针。
- 未初始化。
- 访问未初始化内存。比如,在 Unsafe 代码里创建了未初始化内存区,而在 Safe 代码里去读取。
- 无效释放(Invalid Free)。这属于 Rust 特有,发生在 Unsafe 代码中。
- 释放后使用( UAF, use after free)。比如,在 Safe 代码中被释放内存,而在 Unsafe 代码中还使用其指针。
- 二度释放(Double Free)。这也属于 Rust 特有,是由 Unsafe 代码中的错误传播到 Safe 代码中发生的。比如, t2 = ptr::read:: <T>( & t1 ) , 该代码中,t1 的内容被绑定给了 t2,但是 t1 的所有权却没有被移出。
上述 Bug 经统计,一般存在三种修复策略:
- 可以通过设置检查条件(前置检查 + 后置检查)来跳过危险代码。(安全抽象)
- 调整生命周期。可通过这种策略修复的 Bug,多半是因为对生命周期认识不足引起的。
- 修正 Unsafe 的操作对象。比如,调用 Vec::from_raw_parts () 时将长度和容量更改为正确的顺序。
总体而言,这些内存 Bug 的主要原因还是因为开发者对 Rust 的生命周期认识不足。
线程安全问题
线程安全问题论文团队在 分析了 100 个并发安全问题之后,将其分为两大类:
- 阻塞类 Bug。比如死锁。
- 非阻塞类 Bug。这里指数据竞争。
引起阻塞类 Bug 的原因,又大体分为三种:
- 无法获取到锁。
- 二度锁定。
- 获取锁的顺序有关系。
本质原因,是还是因为开发者对生命周期理解不到位导致的。因为 Rust 是利用生命周期来隐式解锁( unlock)。
(下图是阻塞类 Bug 统计信息。)
阻塞类 Bug 的修复策略主要有四种方法:
- 改变 lock 相关方法的位置,从而调整其生命周期,以改变隐式解锁的时机。
- 调整线程同步机制。
- 修改为非阻塞代码(避免用锁)。
- 显式 drop 替代隐式解锁(这种方式不太 Rust)。
引起非阻塞类 Bug 的原因:
- 使用 Unsafe 进行线程间共享,跳过了安全检查。
- Safe 代码内共享,但是违反了程序语义(应该属于逻辑 Bug)。
使用 Unsafe 代码进行线程共享,还有几种方式:
- 直接传递裸指针
- 使用 Unsafe 代码访问系统级调用和硬件资源。比如,多个线程共享系统调用getmntent()的返回值,并且该返回值指向描述文件系统的结构体。
而在 Safe 代码里,虽然每个值都满足安全规范,但是它们组合在一起,却违反了程序语义,从而引发了数据竞争。
(下图是非阻塞类 Bug 统计信息。)
非阻塞类 Bug 的修复策略主要有两种方法:
- 强制对共享内存进行原子访问
- 强制对不同线程的共享内存访问排序
如何尽量避免非阻塞类 Bug:
- 在实现了 Sync 的结构体中,如有内部可变性的函数,必须检查其内部是否正确互斥。
- 开发者应该仔细设计接口(推敲可变与不可变借用),以避免非阻塞性 Bug。API 的设计能力深深影响 Rust 编译器检查 Bug 的能力。
小结
通过这类调研,我们可以对真实世界中存在的 Rust 程序的安全性有一个比较全面的认识,这些结论对社区开发者来说,是非常有借鉴意义的。并且对于开发 Rust 周边的工具指明了方向,比如 IDE中添加可视化生命周期功能、专属 Rust 的 Bug 检查工具等等。