FlawOpen

flawopen.com/Incidents/rust-unsafe-smallvec-overflow-january-2021

● servo/rust-smallvec · CVE-2021-25900 · January 2021 · CVSS 9.8 · 严重
安全研究 · FlawOpen

CVE-2021-25900:Rust smallvec 堆缓冲区溢出

SmallVec::insert_many 信任 size_hint 的下限;迭代器若产生更多元素,就会写出堆缓冲区边界。已在 0.6.14 和 1.6.1 修复。

💡 通俗易懂的原理解析 (ELI5)

一座仓库以安全著称,因为机器人会在放东西前检查每个货架。有几项工作要关掉机器人,由人手工完成,并保证会小心。其中一人问司机:“你大概带了多少箱?”,照这个数搭了货架就开始码放。可那个估计只是猜测,不是保证;到货比说好的多时,箱子就堆出了货架边缘。

核心概念与专有名词

SmallVec
一种 Rust 集合,少量元素直接内联存放,增长时转用堆上的缓冲区。它的高速来自手动管理这块缓冲区的 unsafe 代码。
size_hint
Iterator 的一个方法,报告剩余元素数量的估计下界和上界。这只是提示:安全代码可以返回任何值,哪怕是错的。
unsafe Rust
unsafe 块中的代码可以做编译器无法检查的事,比如通过裸指针写入;作者必须对任何可能的安全调用方保证内存安全。
可靠性(soundness)
如果任何安全代码,无论多么反常,都无法通过某个安全 API 引发未定义行为,这个 API 就是可靠的。insert_many 并不可靠,因为一个合法的 size_hint 就可能破坏内存。

框架映射

事件演进时间线

2021-01-08

Yechan Bae 与佐治亚理工学院 SSLab 的 Rust 小组报告了该缺陷;RustSec 发布 RUSTSEC-2021-0003,smallvec 0.6.14 和 1.6.1 包含修复。

2021-01

CVE-2021-25900 被分配;cargo audit 开始在依赖树中标出受影响的版本。

攻击执行流程分解

步骤 1

迭代器少报了长度

调用 insert_many 时传入的迭代器,其 size_hint 下界小于它实际产出的元素数量。安全代码这样做是允许的。

步骤 2

按提示预留空间

insert_many 只按下界预留空间,并把已有元素移动相应的位置。

步骤 3

所有元素照样写入

在 unsafe 块中,循环通过裸指针写入每个元素,多出来的元素就落在了分配区域的末尾之外。

步骤 4

堆被破坏

溢出覆盖了相邻的堆内存。凡是把受不可信输入影响的迭代器传给 insert_many 的程序,都会受到影响。

源代码对比:漏洞与安全实现

存在漏洞的实现
// 根据一个*提示值*分配空间……
let (lower, _) = iter.size_hint();
self.reserve(lower);

unsafe {
    let ptr = self.as_mut_ptr().add(index);
    for (i, item) in iter.enumerate() {
        // ……但实际写入
        // 所有到达的元素。
        ptr::write(ptr.add(i), item);
    }
}
加固后的安全修复
// 对每个元素都重新检查容量。
// 绝不能让安全代码中的值
// 决定 unsafe 代码使用的边界。
for (i, item) in iter.enumerate() {
    self.reserve(1);
    unsafe {
        let ptr = self.as_mut_ptr()
                      .add(index + i);
        ptr::write(ptr, item);
    }
}

// 实际修复还大幅缩小了
// unsafe 代码的范围。

工程与系统安全加固清单

参考来源