Unsafe 随堂小测题解(一)
在知乎发现了几篇非常有意思的 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 的数据的指针。data 必须指向 len 连续的正确初始化的 T 类型的值。 返回的 slice 所引用的内存在生命期 'a内不能被改变,除非是在UnsafeCell内。slice 的总大小 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。并且,同时将Memorytrait 标记为 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- 中暴露未初始化内存
公众号历史文章
Gear Technologies 完成 1200 万美元融资
关于 GearFans
Gear 是波卡生态的计算组件,GearFans 是 Gear 爱好者社区。
官网:https://gear-tech.io/
Twitter:https://twitter.com/gear_techs
中文 Twitter:https://twitter.com/gear_fans
GitHub:https://github.com/gear-tech
Discord:https://discord.com/invite/7BQznC9uD9
Telegram 中文群:https://t.me/Gear_CN
QQ 群:677703337
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。
来源链接

