Unsafe 随堂小测题解(一)
“本文节选自「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 的切片类型。所以安全性要非常注意。
如果违反以下任何条件,则行为未定义:
data
必须对读取len * mem::size_of::<T>()
的多个字节有效,而且必须正确对齐。这意味着以下两个条件:
1.1 整个 slice 的内存范围必须包含在单一的分配对象里。slice 不能跨越多个分配对象。文档里有对应的错误用法示例展示。 1.2 即便是零长度的 slice,数据也必须是非空的和对齐的。其中一个原因是枚举布局优化可能依赖于引用(包括任何长度的 slice)的对齐和非空来区分它们与其他数据。你可以使用 NonNull::dangling()
获得一个可作为零长度slice的数据的指针。
'a
内不能被改变,除非是在UnsafeCell
内。len * mem::size_of::<T>()
必须不大于 isize::MAX
,见 `pointer::offset` 的相关文档[7]判断bytes_of
函数是否满足安全条件
对齐没啥问题。
val 也是内存对齐的,因为它使用了引用。因此就存在一种可能性,传入的
&T
中会包含用于对齐的未初始化 padding 字节,在进行cast转换以后,data指针 也许正好会指向哪些padding字节,这个时候就是 UB。或者传入&MaybeUninit<T>
也可能是未初始化的。即,违反上面第二条。显然,因为指针类型的转换,本来应该合法处理的内存也发生了改变。第三条也违反了。除非返回
&[Unsafe<u8>]
。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。
修复思路:
现在 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) }
}
}
另外一种修复思路就是对其进行安全抽象
这种方式,有一个前提就是:开发者可以确保代码在当前执行环境中,实现 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]:
传入的参数 new_len
必须必须小于或等于capacity()
。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
参考资料
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
assert_unsafe_precondition!
: https://cs.github.com/rust-lang/rust/blob/10f4ce324baf7cfb7ce2b2096662b82b79204944/library/core/src/intrinsics.rs?q=assert_unsafe_precondition#L2002
pointer::offset
的相关文档: https://doc.rust-lang.org/core/primitive.pointer.html#method.offset
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-中暴露未初始化内存