被视为未定义的行为
如果 Rust 代码表现出以下列表中的任何行为,则该代码是错误的。这包括 unsafe 块和 unsafe 函数中的代码。unsafe 仅意味着避免未定义行为的责任在于程序员;它不会改变 Rust 程序绝不应导致未定义行为的事实。
编写 unsafe 代码时,程序员有责任确保与 unsafe 代码交互的任何安全代码都无法触发这些行为。满足任何安全客户端此属性的 unsafe 代码称为健全的;如果 unsafe 代码可以被安全代码误用以表现出未定义行为,则它是不健全的。
Warning
以下列表并非详尽无遗;它可能会增长或缩小。Rust 的语义对于不安全代码中允许和不允许的内容没有正式模型,因此可能有更多行为被视为不安全。我们还保留将来使该列表中的某些行为成为已定义的权利。换句话说,此列表并不表示任何内容在所有未来的 Rust 版本中肯定始终是未定义的(但我们将来可能会对某些列表项做出此类承诺)。
编写不安全代码之前,请阅读 Rustonomicon。
- 数据竞争。
- 执行违反边界内指针算术要求的偏移位置投影。偏移位置投影是字段表达式、元组索引表达式或数组/切片索引表达式。
-
违反指针别名规则。确切的别名规则尚未确定,但以下是一般原则的概述:
&T必须指向在其存活期间不被修改的内存(UnsafeCell<U>内部的数据除外),而&mut T必须指向不被任何非从该引用派生的指针读取或写入的内存,并且在其存活期间没有其他引用指向。Box<T>在这些规则的目的上被视为类似于&'static mut T。确切的存活持续时间未指定,但存在一些界限:- 对于引用,存活持续时间的上限是借用检查器分配的语法生命周期;它不能比该生命周期存活更长。
- 每次引用或 box 被解引用或重新借用时,它都被视为存活。
- 每次引用或 box 被传递给函数或从函数返回时,它都被视为存活。
- 当引用(但不是
Box!)被传递给函数时,它至少在该函数调用期间存活,除非&T包含UnsafeCell<U>。
当这些类型的值在复合类型的(嵌套)字段中传递时,所有这些也适用,但在指针间接之后则不适用。
-
修改不可变字节。通过常量提升表达式可到达的所有字节都是不可变的,通过
static和const初始化器中的借用可到达的字节也是如此,这些借用已被生命周期扩展到'static。由不可变绑定或不可变static拥有的字节是不可变的,除非这些字节是UnsafeCell<U>的一部分。此外,由共享引用指向的字节,包括通过其他引用(共享和可变)和
Box传递地指向的字节,是不可变的;传递性包括存储在复合类型字段中的引用。修改是写入超过 0 字节的任何写入,与任何相关字节重叠(即使该写入不更改内存内容)。
- 通过编译器内部函数调用未定义行为。
- 执行使用当前平台不支持的平台功能编译的代码(参见
target_feature),除非平台明确记录这是安全的。
- 使用错误的调用 ABI 调用函数,或展开超过不允许展开的栈帧(例如,通过调用导入或转换为
"C"函数或函数指针的"C-unwind"函数)。
- 产生无效值。“产生“值发生在值被赋值到位置或从位置读取、传递给函数/原始操作或从函数/原始操作返回时。
- 错误使用内联汇编。有关更多详细信息,请参阅编写使用内联汇编代码时要遵循的规则。
- 违反 Rust 运行时的假设。Rust 运行时的大多数假设目前没有明确记录。
- 有关与展开相关的假设,请参阅 panic 文档。
- 运行时假定 Rust 栈帧不会在不执行栈帧拥有的局部变量的析构器的情况下被释放。此假设可以被
longjmp等 C 函数违反。
Note
未定义行为影响整个程序。例如,调用 C 中表现出 C 未定义行为的函数意味着您的整个程序包含也可能影响 Rust 代码的未定义行为。反之,Rust 中的未定义行为可能对通过任何 FFI 调用其他语言执行的代码产生不利影响。
指向的字节
指针或引用“指向“的字节范围由指针值和被指向类型的大小(使用 size_of_val)确定。
基于未对齐指针的位置
如果位置计算期间的最后一个 * 投影是在未对其类型对齐的指针上执行的,则该位置被称为“基于未对齐指针“。(如果位置表达式中没有 * 投影,则这是访问局部变量或 static 的字段,rustc 将保证正确对齐。如果有多个 * 投影,则每个投影都会导致从内存加载要解引用的指针本身,并且每个加载都受对齐约束。请注意,由于自动解引用,某些 * 投影可以在表面 Rust 语法中省略;我们在这里考虑完全展开的位置表达式。)
例如,如果 ptr 类型为 *const S,其中 S 的对齐为 8,则 ptr 必须是 8 对齐的,否则 (*ptr).f 是“基于未对齐指针“的。即使字段 f 的类型是 u8(即对齐为 1 的类型),也是如此。换句话说,对齐要求源自被解引用的指针类型,而不是被访问的字段类型。
请注意,基于未对齐指针的位置仅在加载或存储时导致未定义行为。
在此类位置上允许使用 &raw const/&raw mut。
位置上的 &/&mut 需要字段类型的对齐(否则程序将“产生无效值“),这通常比基于对齐指针的要求更宽松。
在字段类型可能比包含它的类型更对齐的情况下(即 repr(packed)),获取引用将导致编译器错误。这意味着基于对齐指针始终足以确保新引用是对齐的,但这并非总是必要的。
悬垂指针
如果引用/指针指向的所有字节不都是同一存活分配的一部分(因此它们都必须是某个分配的一部分),则该引用/指针是“悬垂的“。
如果大小为 0,则指针显然永远不会“悬垂“(即使它是空指针)。
请注意,动态大小类型(如切片和字符串)指向其整个范围,因此长度元数据永远不能太大。
特别是,Rust 值的动态大小(由 size_of_val 确定)绝不能超过 isize::MAX,因为单个分配不可能大于 isize::MAX。
无效值
Rust 编译器假定程序执行期间产生的所有值都是“有效的“,因此产生无效值是立即的 UB。
值是否有效取决于类型:
bool值必须是false(0)或true(1)。
fn指针值必须非空。
char值不得是代理项(即不得在范围0xD800..=0xDFFF内)且必须等于或小于char::MAX。
!值绝不能存在。
- 整数(
i*/u*)、浮点值(f*)或裸指针必须已初始化,即不得从未初始化内存获取。
str值被视为[u8],即必须已初始化。
enum必须具有有效的判别值,该判别值指示的变体的所有字段必须在其各自的类型处有效。
struct、元组和数组要求所有字段/元素在其各自的类型处有效。
- 宽引用、
Box<T>或裸指针的元数据必须与未大小尾部的类型匹配:dyn Trait元数据必须是指向Trait的编译器生成的 vtable 的指针。(对于裸指针,此要求仍是一些争论的主题。)- 切片(
[T])元数据必须是有效的usize。此外,对于宽引用和Box<T>,如果切片元数据使被指向值的总大小大于isize::MAX,则切片元数据无效。
-
如果类型具有自定义的有效值范围,则有效值必须在该范围内。在标准库中,这影响
NonNull<T>和NonZero<T>。Note
rustc使用不稳定的rustc_layout_scalar_valid_range_*属性实现此目的。
-
在常量上下文中:除了上面描述的内容之外,在常量求值期间还适用与来源相关的进一步要求。任何持有纯整数数据的值(
i*/u*/f*类型以及bool和char、枚举判别值和切片元数据)不得携带任何来源。任何持有指针数据的值(引用、裸指针、函数指针和dyn Trait元数据)必须不携带来源,或者所有字节必须是同一原始指针值的片段且顺序正确。这意味着如果指针具有来源,将指针(引用、裸指针或函数指针)转换或重新解释为非指针类型(如整数)是未定义行为。
Example
以下都是 UB:
#![allow(unused)] fn main() { use core::mem::MaybeUninit; use core::ptr; // We cannot reinterpret a pointer with provenance as an integer, // as then the bytes of the integer will have provenance. const _: usize = { let ptr = &0; unsafe { (&raw const ptr as *const usize).read() } }; // We cannot rearrange the bytes of a pointer with provenance and // then interpret them as a reference, as then a value holding // pointer data will have pointer fragments in the wrong order. const _: &i32 = { let mut ptr = &0; let ptr_bytes = &raw mut ptr as *mut MaybeUninit::<u8>; unsafe { ptr::swap(ptr_bytes.add(1), ptr_bytes.add(2)) }; ptr }; }
**注意:**对于具有受限有效值集的任何类型,未初始化的内存也隐式无效。换句话说,允许读取未初始化内存的唯一情况是在 union 内部和“填充“(类型字段之间的间隙)中。