查看原文
其他

Unsafe 随堂小测题解(一)

张汉东 觉学社 2022-07-24

本文节选自「Rust 生态蜜蜂」。Rust 生态蜜蜂是觉学社公众号开启的一个付费合集。生态蜜蜂,顾名思义,是从 Rust 生态的中,汲取养分,供我们成长。计划从2022年7月第二周开始,到2022年12月最后一周结束。预计至少二十篇周刊,外加一篇Rust年度报告总结抢读版。本专栏可以在公众号菜单「生态蜜蜂」中直接进入。欢迎大家订阅!如果有需要,每个订阅者都可以私信我你的电子邮件,我也会把 Markdown 文件发送给你。

在知乎发现了几篇非常有意思的Unsafe 随堂小测[1],我来尝试解答一下。本文为第一篇。

虽然我被知乎永久限制账号,但给出链接的文章,我还是可以“白嫖”的。

Unsafe 术语背景

在做 Unsafe 随堂小测之前,必须先了解关于 Unsafe Rust 一些术语背景。

官方对 Unsafe Rust 术语给出了定义和解释,见 Unsafe Code Guidelines Reference | Glossary[2],我在 《Rust 编码规范》的 Unsafe Rust 小节里也给出了中文翻译[3]

健全性(Soundness),意味着类型系统是正确的,健全性是类型良好的程序所需的属性。

官方给出的解释为:

健全性是一个类型系统的概念,意味着类型系统是正确的,即,类型良好的程序实际上应该具有该属性。对于 Rust 来说,意味着类型良好的程序不会导致未定义行为。但是这个承诺只适用于 Safe Rust。对于 Unsafe Rust要有开发者/程序员来维护这个契约。因此,如果Safe 代码的公开 API 不可能导致未定义行为,就可以说这个库是健全的。反之,如果安全代码导致未定义行为,那么这个库就是不健全的。

也就是说,开发者在编写 Unsafe Rust 代码的时候,有义务来保证提供的安全抽象接口是不会有未定义行为产生的。违反了健全性,就是不健全(Unsound)的。

未定义行为 (Undefined Behavior) 的准确定义,可以参加上面提到的术语指南。

在对这两个基本术语了解以后,我们就可以来解题了。

题目与题解

先来看题,大家可以尝试自己思考一下。

第一题:以下 bytes_of 函数为什么是不健全(unsound)的?(30分)

本题原型是 bytemuck 中的 bytes_of[4] 函数。

/// !!!unsound!!!
pub fn bytes_of<T>(val: &T) -> &[u8] {
    let len: usize = core::mem::size_of::<T>();
    let data: *const u8 = <*const T>::cast(val);
    unsafe { core::slice::from_raw_parts(data, len) }
}

首先,core::slice::from_raw_parts是来自于核心库的 unsafe 函数,对于 unsafe 函数,出于一种惯例,unsafe 函数必须要指定 Safety 的说明,以便调用者知悉该函数在什么样的边界条件下会发生 UB。所以我们先看`from_raw_parts`函数的文档[5],然后再来看看该bytes_of函数是否满足from_raw_parts的安全条件。

from_raw_parts的安全条件

函数完整签名:pub unsafe fn from_raw_parts<'a, T>(data: *const T, len: usize) -> &'a [T]

代码实现:

pub const unsafe fn from_raw_parts<'a, T>(data: *const T, len: usize) -> &'a [T] {
    // SAFETY: the caller must uphold the safety contract for `from_raw_parts`.
    unsafe {
        assert_unsafe_precondition!(
            is_aligned_and_not_null(data)
                && crate::mem::size_of::<T>().saturating_mul(len) <= isize::MAX as usize
        );
        &*ptr::slice_from_raw_parts(data, len)
    }
}

`assert_unsafe_precondition!`[6] 是编译器内置宏。它会检查是否遵循了 Unsafe 函数的先决条件,如果 debug_assertions 开启,则此宏将在运行时进行检查。

该函数一般被用于 FFi 中将一个来自于 C 的数据切片转为 Rust 的切片类型。所以安全性要非常注意。

如果违反以下任何条件,则行为未定义:

  1. data 必须对读取 len * mem::size_of::<T>() 的多个字节有效,而且必须正确对齐。这意味着以下两个条件:
  • 1.1 整个 slice 的内存范围必须包含在单一的分配对象里。slice 不能跨越多个分配对象。文档里有对应的错误用法示例展示。
  • 1.2 即便是零长度的 slice,数据也必须是非空的和对齐的。其中一个原因是枚举布局优化可能依赖于引用(包括任何长度的 slice)的对齐和非空来区分它们与其他数据。你可以使用NonNull::dangling()获得一个可作为零长度slice的数据的指针。
  • data必须指向len连续的正确初始化的T类型的值。
  • 返回的 slice 所引用的内存在生命期'a内不能被改变,除非是在UnsafeCell内。
  • slice 的总大小 len * mem::size_of::<T>() 必须不大于 isize::MAX,见 `pointer::offset` 的相关文档[7]
  • 判断bytes_of函数是否满足安全条件

    1. 对齐没啥问题。

    2. val 也是内存对齐的,因为它使用了引用。因此就存在一种可能性,传入的&T中会包含用于对齐的未初始化 padding 字节,在进行cast转换以后,data指针 也许正好会指向哪些padding字节,这个时候就是 UB。或者传入 &MaybeUninit<T> 也可能是未初始化的。即,违反上面第二条。

    3. 显然,因为指针类型的转换,本来应该合法处理的内存也发生了改变。第三条也违反了。除非返回 &[Unsafe<u8>]

    4. assert_unsafe_precondition!宏用于检查是否遵循了 Unsafe 函数的先决条件,如果 debug_assertions 开启,仅在运行时执行。从某种意义上说,如果这个宏有用的话,它就是 UB。这里传入的安全条件是判断是否对齐和非空,并且 T 的大小是否不超过 isize::MAX。第一题中的函数满足此条件。

    第二题:以下 Memory trait 的 as_bytes 方法为什么是不健全的?(10分)请提出至少两种修复方案,使该 trait 健全。(20分)

    pub trait Memory {
        fn addr(&self) -> *const u8;

        fn length(&self) -> usize;

        /// !!!unsound!!!
        fn as_bytes(&self) -> &[u8] {
            let data: *const u8 = self.addr();
            let len: usize = self.length();
            unsafe { core::slice::from_raw_parts(data, len) }
        }
    }

    该题依然和 core::slice::from_raw_parts 函数有关,先判断它的安全条件:data 不满足 对齐和非空,assert_unsafe_precondition!宏会 panic,意味着 UB。

    修复思路:

    1. 现在 trait 是默认安全 trait,并且 as_bytes 函数本身是有 UB 风险的。所以,一种修复办法是,将 as_bytes函数标记为 unsafe。并且,同时将 Memory trait 标记为 unsafe。因为 在实现 Memory trait 的时候,实现其addr方法存在风险,返回指针可能为空。(标准库中有类似案例:std::str::pattern::Searcher[8])。并且增加文档注释。
    /// #SAFETY
    /// The trait is marked unsafe because the pointer returned by the addr() methods are required to non-null and aligned
    pub unsafe trait Memory { 
        fn addr(&self) -> *const u8;

        fn length(&self) -> usize;

        /// #SAFETY
        /// Ensure that the addr return pointer to self is non-snull and aligned and others(conditions should be equal to core::slice::from_raw_parts)
        unsafe fn as_bytes(&self) -> &[u8] {
            let data: *const u8 = self.addr();
            let len: usize = self.length();
            unsafe { core::slice::from_raw_parts(data, len) }
        }
    }
    1. 另外一种修复思路就是对其进行安全抽象

    这种方式,有一个前提就是:开发者可以确保代码在当前执行环境中,实现 Memory trait 的 addr()方法都不可能非空或非对齐。所以可以默认约定Memory trait 是安全的。但是需要将 addr()方法标记为 unsafe,并添加Invariant文档来表达默认的信任。并且在 as_bytes 方法中添加 #SAFETY注释。

    pub trait Memory { 
        /// # Invariant
        /// Ensure that the implementation of this method returns a non-null and aligned pointer and others(conditions should be equal to core::slice::from_raw_parts)
        unsafe fn addr(&self) -> *const u8;

        fn length(&self) -> usize;

        fn as_bytes(&self) -> &[u8] {
            // # SAFETY
            // Invariance is guaranteed by the implementation of the addr method
            let data: *const u8 = self.addr();
            let len: usize = self.length();
            unsafe { core::slice::from_raw_parts(data, len) }
        }
    }

    第三题:以下 alloc_for 函数为什么是不健全的?(10分)请写出修复方案,不能改变函数签名。(10分)

    /// !!!unsound!!!
    pub fn alloc_for<T>() -> *mut u8 {
        let layout = std::alloc::Layout::new::<T>();
        unsafe { std::alloc::alloc(layout) }
    }

    当调用 alloc_for::<()>();时,会发生 UB。因为 ()是零大小类型(ZST)。顾名思义,零大小类型不能被分配内存。

    修复思路就是判断 T是否为零大小类型,然后根据具体情况返回合适的值即可。

    比如:

    pub fn alloc_for<T>() -> *mut u8 {
        if mem::size_of::<T>() == 0 {
            panic!("don't creat allocation with size 0 (ZST)");
            // or
            // NonNull::<T>::dangling().as_ptr()
        }else{
            let layout = std::alloc::Layout::new::<T>();
         unsafe { std::alloc::alloc(layout) }
        }
        
    }

    第四题:以下 read_to_vec 函数为什么是不健全的?(10分)请写出修复方案,不能改变函数签名。(10分)

    use std::io;

    /// !!!unsound!!!
    pub fn read_to_vec<R>(mut reader: R, expected: usize) -> io::Result<Vec<u8>>
    where
        R: io::Read,
    {
        let mut buf: Vec<u8> = Vec::new();
        buf.reserve_exact(expected);
        unsafe { buf.set_len(expected) };
        reader.read_exact(&mut buf)?;
        Ok(buf)
    }

    注意看该函数中 unsafe 方法是 set_len。需要去看看标准库文档中 set_len使用安全条件[9]

    1. 传入的参数new_len必须必须小于或等于capacity()
    2. old_len..new_len 范围内的元素必须被初始化。

    上面代码似乎未违反其安全条件。

    但是,代码中有读 Buffer 的操作 ,使用 read_exact。但是当前代码中 Buffer 被分配了内存但并没有被初始化,就传给了 read_exact。在《Rust 编码规范》的 Unsafe Rust 编码规范部分,也包含了一条规则:P.UNS.SAS.03 不要随便在公开的 API 中暴露未初始化内存[10] ,对应此案例,并且有修复示例。

    修复思路:

    use std::io;

    /// !!!unsound!!!
    pub fn read_to_vec<R>(mut reader: R, expected: usize) -> io::Result<Vec<u8>>
    where
        R: io::Read,
    {
        let mut buf: Vec<u8> = vec![0; expected];
        reader.read_exact(&mut buf)?;
        Ok(buf)
    }

    延伸阅读

    https://gankra.github.io/blah/initialize-me-maybe/

    https://github.com/rust-lang/rust-clippy/issues/4483

    https://rust-lang.github.io/rfcs/2930-read-buf.html

    参考资料

    [1]

    Unsafe 随堂小测: https://zhuanlan.zhihu.com/p/532496013

    [2]

    Unsafe Code Guidelines Reference | Glossary: https://rust-lang.github.io/unsafe-code-guidelines/glossary.html

    [3]

    《Rust 编码规范》的 Unsafe Rust 小节里也给出了中文翻译: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/safe-guides/coding_practice/unsafe_rust/glossary.html

    [4]

    bytes_of: https://link.zhihu.com/?target=https%3A//docs.rs/bytemuck/latest/bytemuck/fn.bytes_of.html

    [5]

    from_raw_parts函数的文档: https://doc.rust-lang.org/core/slice/fn.from_raw_parts.html

    [6]

    assert_unsafe_precondition!: https://cs.github.com/rust-lang/rust/blob/10f4ce324baf7cfb7ce2b2096662b82b79204944/library/core/src/intrinsics.rs?q=assert_unsafe_precondition#L2002

    [7]

    pointer::offset 的相关文档: https://doc.rust-lang.org/core/primitive.pointer.html#method.offset

    [8]

    std::str::pattern::Searcher: https://doc.rust-lang.org/std/str/pattern/trait.Searcher.html

    [9]

    使用安全条件: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.set_len

    [10]

    P.UNS.SAS.03 不要随便在公开的 API 中暴露未初始化内存: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/safe-guides/coding_practice/unsafe_rust/safe_abstract/P.UNS.SAS.03.html#punssas03--不要随便在公开的-api-中暴露未初始化内存


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存