Skip to content

20.4 不安全的边界

Vec 有一个成员方法叫作set_len,可以用于改变动态数组的大小。它的源码如下:


rust
pub unsafe fn set_len(&mut self, len: usize) {
    self.len = len;
}

关于这个方法,需要请大家注意的是,它有 unsafe 标记。它的内部只是一个 usize 类型的赋值而已,怎么会是 unsafe 呢?

因为,我们的 Vec 内部实现非常依赖于 self.len 这个值的合法性。如果说这个方法不是 unsafe,外部的使用者可以随意设置动态数组的大小,那么用户可以将其大小突然变很大,然后就可以通过这个 Vec 访问本不该属于它的内容,这就造成了“内存不安全”的情况。

所以,我们一定要注意的是:判断一个函数是否应该是一个 unsafe 函数,不该看它表面的逻辑,而应该判断用户使用它的时候造成的影响。

当我们使用 unsafe 代码块的时候,很可能需要一些对 safe 代码的隐含的假设和依赖,这些依赖关系既不能通过类型系统向编译器清楚表达,也未必能在代码中明显地表现出来。safe 代码有义务维持 unsafe 代码相对应的假设,unsafe 代码中也要注意保持一致性。如果这些假设一旦被破坏,那这个库的安全性也就功亏一篑了。只要你在某个函数内部使用了 unsafe 代码块,你需要关注的就不只是这个函数的正确性,还有这个类型中,甚至是这个模块中,其他所有函数的正确性,它们是互相影响互相搭配的。

对于 Vec 类型,我们需要保证任何时候 self.len 这个成员都应该准确地反映它内部的成员个数。这个要求,我们无法利用类型系统或者别的什么语言特性表达出来。因为这个成员赋值,可能是安全的,也可能是不安全的,取决于上下文逻辑。所以,这个方法必须用 unsafe 标记。而用户在使用的时候,如果已经在逻辑上保证了这个赋值是安全的,那么就可以在那个地方利用 unsafe 代码块调用这个方法,否则就不该调用。

再举个例子,我们使用 unsafe 代码来访问数组内部的数据:


rust
fn index(arr: &[u8], idx: usize) -> Option<u8> {
    if idx < arr.len() {
        unsafe {
            let p = arr.as_ptr().offset(idx as isize);
            Some(*p)
        }
    } else {
        None
    }
}

fn main() {
    let arr = [1,2,3,4,5];
    println!("{:?}", index(&arr, 3));
}

基本逻辑很简单,通过裸指针的算术运算,指向我们需要的目标,然后将数据读出来。这段代码将不安全的内部实现和安全的外部 API 良好地结合在了一起,是符合 Rust 的设计思路的。

在这个例子中,如果我们把 if 条件稍作改动,变成:


rust
if idx <= arr.len()

那么这个函数就变成了“不安全”的代码,外部用户有机会通过安全代码读取不属于这个数组的内容,而且编译器检查不出来。需要注意的是,我们这里只修改了安全代码,但它制造了不安全现象。这是因为我们内部的 unsafe 语句块中,已经假定了 idx 的数值是合法的。我们在 unsafe 块的外部,就需要通过逻辑来保证这一点,否则就是 bug。

所以,基于 unsafe 代码写库很难,难就难在你不仅要测试正常情况下功能的正确性,还必须考虑用户可能的各种行为是否有可能在 safe 代码中利用你这个库制造内存不安全。在 C/C++中,如果用户可以通过一个库制造内存不安全,那是用户的问题,作为库只需要提供功能就足够了,无须保证安全性,当然你也保证不了,顶多也就“防君子不防小人”。跟 C/C++相比,Rust 提供的保证要严格得多,如果用户有机会利用你的库在不使用 unsafe 关键字的情况下制造出“内存不安全”,那这个库就有严重 bug,是低质量的、不可接受的。

Released under the MIT License