cve-rs
通过利用 Rust 生命周期参数的安全漏洞,精心构造了在 Safe Rust 下的三类攻击手段,真是让人大开眼界。
一个类型的良构性(well-formedness),实际上就是检查这个类型和它的所有部分是否遵守 Rust 的类型规则和约束。
良构性的概念确保了类型的使用是有意义的。例如,如果你定义了一个泛型结构体,这个结构体对泛型参数有特定的约束(如需要实现某个 trait),那么在使用这个结构体时提供的具体类型必须满足这些约束。良构性检查正是保证了这一点。
`struct MyStruct<T: Copy> { value: T, } `
在这个例子中,MyStruct<T>
要求 T
必须实现了 Copy
trait。如果尝试使用不满足这个约束的类型参数来实例化 MyStruct
,编译器会报错,因为这样的类型实例不是良构的。
良构性检查在 Rust 中非常重要,因为它直接关系到 Rust 的核心安全保障。只有确保了类型的良构性,才能保证类型系统的正确性和程序的内存安全。
GitHub 上的 Rust 语言问题 #25860[1] 揭示了一个类型系统中的问题,破坏了 Rust 类型系统的良构性。
`static UNIT: &'static &'static () = &&(); // 未指定 'a 和 'b 的关系, // 这里编译器会假设 foo 函数有 'b: 'a fn foo<'a, 'b, T>(_: &'a &'b (), v: &'b T) -> &'a T { v } fn bad<'a, T>(x: &'a T) -> &'static T { let f: fn(&'static &'a (), &'a T) -> &'static T = foo; // 然而在这里调用 foo 时并未按上面的假设检查生命周期参数 // 这里是 'static : 'a,违反了 'b : 'a 的假设 // 编译通过 f(UNIT, x) } `
这个问题涉及到嵌套引用的隐含边界(implied bounds)和型变(variance,协变/逆变/不变)的组合。
问题的本质在于,对于嵌套引用类型(例如 &'a &'b T
),Rust 类型系统允许通过型变在没有实际提供证据的情况下,交换生命周期参数 'a
和 'b
的相对长短。在一定情况下,这会导致一种情况,即可以创建出一个假设 'b: 'a
(意即 'b
生命周期至少和 'a
一样长)的函数 foo
,因为只有这样,才不会出现悬垂指针(二级指针 &'b ()
必须在 &'a ()
整个生命周期内有效 ),但在实际使用这个函数时,并不检查这个假设是否成立。这就打开了一个可以绕过 Rust 安全保障的漏洞。
再看另外一个示例,Miri 可以检查出 UB。
``fn foo<'a, 'b, T>(_false_witness: Option<&'a &'b ()>, v: &'b T) -> &'a T { v } fn bad<'c, 'd, T>(x: &'c T) -> &'d T { // below is using contravariance to assign `foo` to `f`, // side-stepping the obligation to prove `'c: 'd` // implicit in the original `fn foo`. let f: fn(Option<&'d &'d ()>, &'c T) -> &'d T = foo; f(None, x) } fn main() { fn inner() -> &'static String { bad(&format!("hello")) } // miri 检查出问题: // error: Undefined Behavior: constructing invalid value: encountered a dangling reference (use-after-free) let x = inner(); println!("x: {}", x); } ``
这里就违反了 Rust 类型系统的良构性,而这个问题也被记录在官方的 **#RFC 1214**[2] 中,包括其他之前未明确规定或存在错误实现的类型系统的各个方面问题。
Rust 类型系统实现并非完美,截止今天 Rust 官方团队依然在重构 Rust 语言内部的类型系统,包括 trait 系统 和 借用检查规则等。
所以这类问题需要开发者自己去注意,显式化指定类型或生命周期关系:
`static UNIT: &'static &'static () = &&(); // 这里显式指定 'b: 'a fn foo<'a, 'b: 'a, T>(_: &'a &'b (), v: &'b T) -> &'a T { v } fn bad<'a, T>(x: &'a T) -> &'static T { let f: fn(&'static &'a (), &'a T) -> &'static T = foo; // 这里是 'static : 'a,违反了 'b : 'a 的假设 // 编译报错:error: lifetime may not live long enough f(UNIT, x) } `
官方目前的解决思路是:从函数参数中移除逆变性是解决问题的一种实用方法,至少在短期内是这样。
因为 Rust 中只有生命周期参数包括类型父子关系,'static : 'b
是 Rust 编译器认同的父子类型。而前面示例中 允许'b
参数(父类型)传入,而实际传入了 'static
(子类型),就是一种逆变。在上面的示例中会导致悬垂指针,而编译器没有发现。
如果编译器禁止函数参数逆变,编译时就能消除这类问题。当然官方也在讨论其他方法,目前看来没什么进展。
前几周,Rust 生态中突然冒出一个库 ,cve-rs[3] ,号称可以不用一行 Unsafe 代码而百分百地用 Safe Rust 就能写出包含内存安全问题的代码。
一石激起千层浪,本来 Rust 程序员是没有什么机会去进行 “防御性编码” 的。现在有了这个库。。。(我不是教你坏)。
“
防御性编程,又被玩坏的词。在早期是为了开发可靠的软件,我们在设计系统中每个组件的时候,都需要使其尽可能的 "保护" 自己。 而现在,这个词被重启,已经改变了原意:从保护代码 变为 保护程序员群体。说白了,就是写烂代码,写 Bug,让别人看不懂,出了问题不好修,从而提升程序员的不可替代性。这算是程序员的悲哀,也是这个时代的悲哀。
这个库公开的真实目的是为了警示 Rust 开发者,在代码审查时需要注意这类问题。
cve-rs 可以在代码中实现下面内存安全问题:
使用后释放(Use after free )
缓冲区溢出(Buffer overflow )
段错误(Segmentation fault )
cve-rs 还支持 WebAssembly。这意味着,这些漏洞也可以随着 Rust 代码编译为 WebAssembly 而传播出去。
比如:
`// With cve-rs, you can crash prod in a 🔥 blazingly fast manner! pub fn segfault() { let null: &mut u8 = cve_rs::null_mut::<u8>(); *null = 42; } `
执行以后:
cve-rs
源码解读