P.UNS.SAS.01 代码中要注意是否会因为 Panic 发生而导致内存安全问题

【描述】

Panic 一般在程序达到不可恢复的状态才用,当然在 Rust 中也可以对一些实现了UnwindSafe trait 的类型捕获Panic。

当 Panic 发生时,会引发栈回退(stack unwind),调用栈分配对象的析构函数,并将控制流转移给 Panic 处理程序中。所以,当 Panic 发生的时候,当前存活变量的析构函数将会被调用,从而导致一些内存安全问题,比如释放已经释放过的内存。

通常, 封装的 Unsafe 代码可能会暂时绕过所有权检查,而且,安全封装的 API 在内部 unsafe 代码的值返回之前,会根据安全边界条件确保它不会违反安全规则。但是,假如封装的 Unsafe 代码发生了Panic,则其外部安全检查可能不会执行。这很可能导致类似 C/C++ 中 未初始化(Uninitialized )或双重释放(Double Free)的内存不安全问题。

想要正确的推理在 Unsafe 代码中的恐慌安全,是非常困难且易于出错的。即便如此,在编写代码的时候也要刻意注意此类问题发生的可能性。

【正例】

// 标准库 `String::retain()` 曝出的 CVE-2020-36317 Panic safety bug

pub fn retain<F>(&mut self, mut f: F)
where 
    F: FnMut(char) -> bool
{
    let len = self.len();
    let mut del_bytes = 0;
 	let mut idx = 0;
 
    unsafe { self.vec.set_len(0); }    // + 修复bug 的代码
 	while idx < len {
 		let ch = unsafe {
  			self.get_unchecked(idx..len).chars().next().unwrap()
 		};
 		let ch_len = ch.len_utf8();
 
 		// self is left in an inconsistent state if f() panics
        // 此处如果 f() 发生了 Panic,self 的长度就会不一致
 		if !f(ch) {
 			del_bytes += ch_len;
 		} else if del_bytes > 0 {
 			unsafe {
 				ptr::copy(self.vec.as_ptr().add(idx),
 				self.vec.as_mut_ptr().add(idx - del_bytes),
 				ch_len);
 			}
 		}
 		idx += ch_len; // point idx to the next char
 	}
 	unsafe { self.vec.set_len(len - del_bytes); } // + 修复bug 的代码 ,如果 while 里发生 Panic,则将返回长度设置为 0 
}

fn main(){
    // PoC: creates a non-utf-8 string in the unwinding path
    // 此处传入一个 非 UTF-8 编码字符串引发 Panic
    "0è0".to_string().retain(|_| {
        match the_number_of_invocation() {
            1 => false,
            2 => true,
            _ => panic!(),
        }
    });
}