Rust的安全卫生原则
在Rust编程世界里,unsafe
关键字常常让初学者感到困惑。他们经常会问:“在unsafe
块里能做什么?”“什么时候需要使用unsafe
?” 这些问题虽然很常见,但对于真正理解Rust的安全机制来说还远远不够。因为随着Rust操作语义的不断完善,unsafe
代码的能力边界也在变化;而且“什么时候需要unsafe
”这种问题的答案往往比较笼统,对编写可维护的unsafe
代码以及改进Rust的安全工具帮助不大。
1. 安全卫生原则概述
Rust的安全卫生原则,简单来说,就是清晰地标记和记录内存安全义务产生和解除的地方,它基于三条基本原则:
- 使用关键字和对义务的解释来引入安全义务。
- 使用关键字和对解除原因的解释来解除安全义务。
- 当不涉及安全义务时,避免使用相关关键字和安全注释。
在Rust中,这个关键字就是unsafe
,引入安全义务时通常用# Safety
注释说明,解除时则用// SAFETY
注释。Rust的相关工具也基本围绕这三条规则来设计。
在这里的“安全卫生”语境里,“卫生”可类比日常生活中保持环境整洁、预防疾病的概念,核心是维护系统(这里指代码)的“健康”,免受因内存不安全引发的“问题”侵扰。它有三方面具体含义:
- 明确标记与记录:就像在卫生管理中,对不同区域的清洁责任和操作规范都有清晰界定一样。在代码里,使用
unsafe
关键字和相关注释,准确标记内存安全义务的产生和解除之处。在zerocopy
库的Ptr<T, I>
结构体定义及new
方法中,详细列出字段的安全不变量,并在合适位置添加SAFETY
注释,让开发者清楚知道代码中潜在的安全风险点以及如何确保安全,这有助于维护代码的安全“健康”状态 。 - 规范操作与约束:如同卫生标准规范人们的日常行为,防止疾病传播。安全卫生原则约束开发者对
unsafe
代码的使用。函数和trait安全卫生要求,有安全条件的函数和trait用unsafe
标记,调用或实现时也要遵循相应规则,严格限制unsafe
的使用范围,避免随意操作内存带来的风险,保障代码的安全性和稳定性。 - 预防潜在问题:类似卫生措施能预防疾病发生,安全卫生通过一系列规则和机制,预防因内存操作不当导致的程序错误,如内存泄漏、非法访问等。通过规范的安全注释和严格的安全义务管理,在开发过程中提前发现和解决潜在的内存安全问题,确保程序“健康”运行 。
2. 函数安全卫生实践
函数安全卫生要求在函数有安全条件时进行明确标记和记录,调用函数时也要确保满足这些条件。比如,一个遵循良好函数安全卫生原则的库会这样做:
- 用
unsafe
关键字标记有安全条件的函数,并提供相应的安全文档。 - 用
unsafe
关键字调用这些函数,并给出安全注释。 - 严格控制
unsafe
的使用范围,没有安全前置条件的函数不应该标记为unsafe
,也不能用unsafe
调用安全函数。
例如:
// 定义一个有安全条件的函数,这里假设这个函数直接操作内存,需要调用者确保内存的安全性
unsafe fn dangerous_function(ptr: *const i32) -> i32 {// 直接读取指针指向的值,这是不安全的操作,因为调用者需要保证指针有效*ptr
}fn main() {let value = 42;let ptr = &value as *const i32;// 调用dangerous_function时,需要用unsafe关键字,并添加安全注释说明满足的条件unsafe {// SAFETY: 这里ptr指向的内存是有效的,因为ptr是由合法的变量value创建的let result = dangerous_function(ptr);println!("The result is: {}", result);}
}
在这个例子中,dangerous_function
函数因为直接操作指针,所以标记为unsafe
,调用时也必须在unsafe
块里进行,并且添加了安全注释解释调用的安全性。
3. 特征(Trait)安全卫生实践
特征安全卫生和函数安全卫生类似,当特征带有安全不变量,以及实现满足这些不变量时,都要进行标记和记录。具体做法如下:
- 用
unsafe
关键字标记带有安全不变量的特征,并提供安全文档。 - 用
unsafe
关键字实现这些特征,并添加安全注释。 - 同样,没有安全条件的特征不应标记为
unsafe
,也不能用unsafe
实现安全特征。
比如:
// 定义一个带有安全不变量的特征,假设这个特征用于操作特定的内存区域,需要实现者保证内存安全
unsafe trait MemorySafeTrait {// 方法定义,这里只是示例,实际可能涉及复杂的内存操作fn perform_safe_operation(&self);
}struct SafeStruct;// 实现MemorySafeTrait特征,因为特征是unsafe的,所以实现也需要用unsafe
unsafe impl MemorySafeTrait for SafeStruct {// SAFETY: 这里假设SafeStruct的实现保证了perform_safe_operation方法中的内存操作安全fn perform_safe_operation(&self) {// 这里可以进行一些假设安全的内存操作}
}
在这个代码示例中,MemorySafeTrait
特征因为涉及内存安全相关的操作,所以标记为unsafe
,SafeStruct
实现这个特征时也用unsafe
关键字,并添加了安全注释。
4. 字段安全卫生实践
在一些库(如zerocopy
和itertools
)中,还会进行字段安全卫生实践,即标记和注释字段的安全不变量,以及使用这些字段时如何解除这些不变量。以zerocopy
库中的Ptr<T, I>
类型为例:
use std::ptr::NonNull;
use std::marker::PhantomData;// 定义Ptr结构体,包含一些安全不变量
pub struct Ptr<'a, T, I>
whereT: 'a + ?Sized,I: Invariants,
{/// # Invariants////// 0. 如果ptr指向的对象不是零大小,那么ptr来自某个有效的Rust分配A。/// 1. 如果ptr指向的对象不是零大小,那么ptr对于A有有效的来源。/// 2. 如果ptr指向的对象不是零大小,那么ptr指向的字节范围完全包含在A中。/// 3. ptr指向的字节范围的长度可以用isize表示。/// 4. ptr指向的字节范围不会在地址空间中环绕。/// 5. 如果ptr指向的对象不是零大小,A至少会存活'a生命周期。/// 6. T: 'a。/// 7. ptr符合[`I::Aliasing`]的别名不变量。/// 8. ptr符合[`I::Alignment`]的对齐不变量。/// 9. ptr符合[`I::Validity`]的有效性不变量。// SAFETY: `NonNull<T>`在T上是协变的 [1]。//// [1]: https://doc.rust-lang.org/std/ptr/struct.NonNull.htmlptr: NonNull<T>,// SAFETY: `&'a ()`在'a上是协变的 [1]。//// [1]: https://doc.rust-lang.org/reference/subtyping.html#variance_invariants: PhantomData<&'a I>,
}// 为Ptr结构体实现new方法,创建Ptr实例
impl<'a, T, I> Ptr<'a, T, I>
whereT: 'a + ?Sized,I: Invariants,
{/// 从[`NonNull`]构造一个`Ptr`。////// # Safety////// 调用者承诺:////// 0. 如果ptr指向的对象不是零大小,那么ptr来自某个有效的Rust分配A。/// 1. 如果ptr指向的对象不是零大小,那么ptr对于A有有效的来源。/// 2. 如果ptr指向的对象不是零大小,那么ptr指向的字节范围完全包含在A中。/// 3. ptr指向的字节范围的长度可以用isize表示。/// 4. ptr指向的字节范围不会在地址空间中环绕。/// 5. 如果ptr指向的对象不是零大小,那么A至少会存活'a生命周期。/// 6. ptr符合[`I::Aliasing`]的别名不变量。/// 7. ptr符合[`I::Alignment`]的对齐不变量。/// 8. ptr符合[`I::Validity`]的有效性不变量。pub(super) unsafe fn new(ptr: NonNull<T>) -> Ptr<'a, T, I> {// SAFETY: 调用者已经承诺满足Ptr的所有安全不变量Self { ptr, _invariants: PhantomData }}
}
在这个例子中,Ptr
结构体的字段ptr
和_invariants
都有相应的安全注释,new
方法也标记为unsafe
,并详细说明了调用者需要满足的安全条件。
5. Rust安全工具的现状与改进方向
目前,Rust的安全工具在支持安全卫生原则方面还有一些不足。例如,对于字段安全卫生,Rust没有提供相应的工具支持,甚至有些语法限制会阻碍相关实践,像不能对字段使用unsafe
关键字,unused_unsafe
lint也不鼓励在一些“安全”操作中使用unsafe
块。
不过,有很多改进Rust安全工具的提案:
- 区分定义和解除:有人提议用不同的关键字区分安全条件的定义和解除,比如用
unsafe
声明义务,用checked
解除义务。 - 区分前置条件和后置条件:可以使用不同的标记区分函数或操作的前置安全条件和后置安全条件,像用
unsafe(pre)
表示前置条件,unsafe(post)
表示后置条件。 - 结构化安全文档:通过添加工具来检查安全条件的定义和解除是否正确记录,比如用属性替代无结构的安全注释,或者扩展
unsafe
关键字,在定义和使用时明确列出安全条件标签。
此外,还可以采取一些基础的改进措施,比如将一些安全卫生相关的lint从Clippy迁移到rustc并设置为默认警告,以及按照RFC3458的提议扩展Rust的安全卫生工具支持字段安全。
6. 安全联合(Safe Unions)问题
在Rust中,联合(Unions)目前的定义、构造和修改是安全的,但读取需要unsafe
。这就可能导致一个问题,比如下面这个例子:
#[derive(Copy, Clone)]
#[repr(u8)]
enum Zero {V = 0
}#[derive(Copy, Clone)]
#[repr(u8)]
enum One {V = 1
}union Tricky {a: (Zero, One),b: (One, Zero),
}fn main() {let mut tricky = Tricky { a: (Zero::V, One::V) };tricky.b.0 = One::V;// 现在,tricky.a和tricky.b都处于无效状态!
}
在这个例子中,通过安全代码就可以让联合Tricky
处于一种无法安全读取字段的状态。为了解决这个问题,有人提议将隐式不安全的联合改为显式不安全,比如:
union MaybeUninit<T> {uninit: (),unsafe value: ManuallyDrop<T>,
}
这样可以释放语法空间,让Rust在联合的安全性上更符合默认安全的惯例。
总之,Rust的安全卫生原则为理解和改进Rust的安全机制提供了一个重要框架,通过不断完善安全工具和遵循这些原则,Rust的安全性会越来越可靠。