前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unsafe 随堂小测题解(一)

Unsafe 随堂小测题解(一)

作者头像
张汉东
发布2022-12-08 20:52:04
8780
发布2022-12-08 20:52:04
举报
文章被收录于专栏:Rust 编程Rust 编程

“本文节选自「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] 函数。

代码语言:javascript
复制
/// !!!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]

代码实现:

代码语言:javascript
复制
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的数据的指针。
  2. data必须指向len连续的正确初始化的T类型的值。
  3. 返回的 slice 所引用的内存在生命期'a内不能被改变,除非是在UnsafeCell内。
  4. 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分)

代码语言:javascript
复制
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])。并且增加文档注释。
代码语言:javascript
复制
/// #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注释。

代码语言:javascript
复制
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分)

代码语言:javascript
复制
/// !!!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是否为零大小类型,然后根据具体情况返回合适的值即可。

比如:

代码语言:javascript
复制
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分)

代码语言:javascript
复制
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] ,对应此案例,并且有修复示例。

修复思路:

代码语言:javascript
复制
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-中暴露未初始化内存

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-07-24,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 觉学社 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Unsafe 术语背景
  • 题目与题解
    • 第一题:以下 bytes_of 函数为什么是不健全(unsound)的?(30分)
      • 第二题:以下 Memory trait 的 as_bytes 方法为什么是不健全的?(10分)请提出至少两种修复方案,使该 trait 健全。(20分)
        • 第三题:以下 alloc_for 函数为什么是不健全的?(10分)请写出修复方案,不能改变函数签名。(10分)
          • 第四题:以下 read_to_vec 函数为什么是不健全的?(10分)请写出修复方案,不能改变函数签名。(10分)
          • 延伸阅读
            • 参考资料
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
            http://www.vxiaotou.com