据我所知,没有官方资源明确说明具有比正确寿命更长的引用和/或取消引用引用是否会导致未定义的行为。但是,有多个资源谈论了Rust中的未定义行为以及具有无限寿命的引用的取消引用,这些资源提供了关于执行此操作的定义性的提示。
引起Rustonomicon中未定义行为的事物:
引用
Rustonomicon,第“什么是不安全”的章节中的引用(本文和所有后续引用中的
粗体突出显示和
[方括号内的斜体文本]由我完成):
与 C 不同,Rust 中未定义行为的范围非常有限。所有核心语言关心的仅仅是防止以下几种情况发生:
- 解引用(使用
*
操作符)悬空或未对齐的指针(见下文)
- 违反
指针别名规则
- 以错误的调用 ABI 调用函数或从错误的取消 ABI 函数中取消。
- 导致
数据竞争
- 执行使用当前执行线程不支持的
目标特性编译的代码
- 生成无效值(作为单独的值或复合类型(如
enum
/
struct
/array/tuple)的字段):
- [很多与引用生命周期无关的子项]
"生成"值是指任何时候都会分配值、传递给函数/原始操作或从函数/原始操作返回值。如果引用/指针为“悬空”,则它为空或者它所指向的字节不全部属于同一分配(因此它们都必须属于某个分配)。它所指向的字节跨度由指针值和指针类型的大小确定。因此,如果跨度为空,则“悬空”与“非空”相同。请注意,切片和字符串指向其整个范围,因此重要的是长度元数据永远不要太大(特别是,分配和因此切片和字符串不能超过
isize::MAX
字节)。如果由于某种原因这太麻烦了,请考虑使用裸指针。这就是所有嵌入到 Rust 中的未定义行为的原因。当然,不安全的函数和特性可以自由地声明程序必须维护以避免未定义行为的任意其他约束。
只有前两个要点与指针和/或引用相关。
The first point is about dereferencing dangling and unaligned pointers.
- Unaligned pointers: Transmuting the lifetime of a reference cannot change its alignedness, so this is not a problem here.
- Dangling pointers: During the correct lifetime, a reference cannot be dangling according to the definition above. Therefore, during the correct lifetime, the transmuted reference is also not dangling and can be dereferenced without causing undefined behavior.
The second point is about the pointer aliasing rules of Rust. Quote from the Rustonomicon, chapter "References" (which is also linked in the quote above when talking about pointer aliasing rules):
There are two kinds of reference:
- Shared reference:
&
- Mutable reference:
&mut
Which obey the following rules:
- A reference cannot outlive its referent
- A mutable reference cannot be aliased
That's it. That's the whole model references follow.
Of course, we should probably define what aliased means.
error[E0425]: cannot find value `aliased` in this scope
--> <rust.rs>:2:20
|
2 | println!("{}", aliased);
| ^^^^^^^ not found in this scope
error: aborting due to previous error
Unfortunately, Rust hasn't actually defined its aliasing model.
While we wait for the Rust devs to specify the semantics of their language,
let's use the next section to discuss what aliasing is in general, and why it
matters.
That first point does indeed not sound that good for our case – "a reference cannot outlive its referent". However, quote from the Rustonomicon, chapter "Lifetimes", section "The area covered by a lifetime":
The lifetime (sometimes called a borrow) is alive from the place it is created to its last use. The borrowed thing needs to outlive only borrows that are alive.
So, as we are not using the reference after the end of the correct lifetime, it is not alive anymore. Therefore, the referent also does not need to be alive after the end of the correct lifetime.
The second point is only about mutable references – in your example, a shared reference is used. And either way, as long as the original value is not used while the transmuted reference is alive, there is no pointer aliasing going on by any definition (though, as the Rustonomicon says, there is no aliasing model defined for Rust, so language lawyering about this is hard...)
结论 - Rustonomicon中导致未定义行为的因素
Rustonomicon中列举的触发未定义行为的因素不包括具有比正确生命周期更长的引用并对其进行解引用只要在正确生命周期结束后不再访问此引用。
Rust参考文档中导致未定义行为的因素
Rustonomicon并非唯一讨论未定义行为的文档。引用自Rust参考文档,章节“被视为未定义的行为”:
如果 Rust 代码表现出以下行为中的任何一种,那么它就是错误的。这包括在 `unsafe` 块和 `unsafe` 函数内部的代码。`unsafe` 只意味着避免未定义的行为取决于程序员;它并不改变 Rust 程序绝不能引起未定义行为的事实。
编写 `unsafe` 代码时,程序员有责任确保与 `unsafe` 代码交互的任何安全代码都无法触发这些行为。对于任何安全客户端都满足此属性的 `unsafe` 代码称为“sound”;如果安全代码可以误用 `unsafe` 代码以表现未定义行为,则其为“unsound”。
⚠️ 警告:以下列表不是详尽无遗的。Rust 的语义模型没有正式的模型来确定 `unsafe` 代码中允许什么,不允许什么,因此可能会有更多被认为是不安全的行为。以下列表只是我们知道的肯定是未定义行为的内容。请在编写 `unsafe` 代码之前阅读
Rustonomicon。
数据竞争。
在悬空或未对齐的原始指针上评估解引用表达式(*expr),即使在放置表达式上下文中(例如 addr_of!(&*expr))也是如此。
违反指针别名规则。&mut T 和 &T 遵循 LLVM 的作用域 noalias 模型,但如果 &T 包含 UnsafeCell
则除外。
更改不可变数据。所有 const 项内的数据都是不可变的。此外,通过共享引用访问或由不可变绑定拥有的数据都是不可变的,除非该数据包含在 UnsafeCell 中。
通过编译器内部函数调用来调用未定义行为。
执行使用当前平台不支持的平台特性编译的代码(请参见 target_feature)。
使用错误的调用 ABI 调用函数或从具有错误展开 ABI 的函数展开。
生成无效值,即使在私有字段和局部变量中也是如此。每当将值分配给或从位置读取、传递给函数/原始操作或从函数/原始操作返回时,都会“生成”一个值。以下值是无效的(在其各自的类型上):
[大量与引用生命周期无关的子项]
注意:对于具有受限制的有效值集的任何类型,未初始化的内存也隐式为无效。换句话说,只有在 union 中和“填充”(类型的字段/元素之间的间隙)中才允许读取未初始化的内存。
注意:未定义行为影响整个程序。例如,在 C 中调用表现出 C 的未定义行为的函数意味着您的整个程序包含未定义行为,这也可能影响 Rust 代码。反之,在 Rust 中的未定义行为可能对通过任何 FFI 调用其他语言执行的代码产生不良影响。
悬空指针
如果引用/指针为空或它们指向的所有字节都不属于同一分配(因此它们都必须属于某个分配),则它们是“dangling”。它所指向的字节跨度由指针值和指针类型的大小(使用 size_of_val)确定。因此,如果跨度为空,则“悬挂”与“非空”相同。请注意,切片和字符串指向其整个范围,因此长度元数据永远不能太大。特别地,分配和因此切片和字符串的大小不能超过 isize::MAX 字节。
这个列表,包括两个粗体点,与Rustonomicon中的列表基本相同(虽然不太严格,因为第一个粗体符号只禁止解除悬空的原始指针,而不是解除悬空的引用 - 我想这是疏忽)。有一些有趣的链接到LLVM文档,但最终结果是相同的:根据此列表,具有比正确寿命更长的引用不会导致未定义行为,只要在正确寿命结束后不解引用该引用。但是,在这里还有一个额外的注释:
⚠️ 警告:以下列表不是详尽无遗的。 Rust的语义没有正式模型来确定不安全代码中允许或不允许的内容,因此可能会有更多被认为是不安全的行为。以下列表仅表示我们确定的未定义行为。编写不安全代码之前,请阅读[Rustonomicon]。
结论-根据Rust参考手册导致未定义行为的事情
Rust参考手册中没有详尽列举所有可能引发未定义行为的事项。虽然Rust参考手册没有明确说明具有超过正确生命周期的引用会触发未定义行为,但它也没有明确说明它们不会触发。
关于无界生命周期的Rustonomicon
来自Rustonomicon, 章节“无界生命周期”的引用:
不安全的代码通常会产生无中生有的引用或生命周期。这样的生命周期是 无限制的。最常见的来源是对裸指针进行解引用,这会产生一个具有无限制生命周期的引用。这样的生命周期会随着上下文需求而变得越来越大。实际上,这比简单地成为 'static
更加强大,因为例如 &'static &'a T
将无法进行类型检查,但无限制生命周期将完美地适应 &'a &'a T
。然而,对于大多数意图和目的,这样的无限制生命周期可以被视为 'static
。
几乎没有引用是 'static
的,所以这可能是错误的。 transmute
和 transmute_copy
是另外两个主要的罪犯。人们应该尽快将无限制生命周期限定在一个范围内,特别是跨函数边界时。
Rustonomicon提到应该“尽快限定无限生命周期,特别是在函数边界上”。它没有表明解引用无限制的引用(假设引用对象仍然存在)会导致未定义的行为。由于解引用是一种常见操作,我无法想象Rustonomicon不指出这样一个显而易见的陷阱。因此,只要引用对象仍然存在,我得出结论:解引用无限制的引用不会导致未定义的行为。
然而,问题不在于生命周期无限制的引用,而在于生命周期大于正确生命周期的引用,例如
&'static T
。Rustonomicon指出,“对于大多数意图,无限生命周期可以被视为
'static
”。这并不确定意味着与具有无限生命周期的引用相比,解引用具有更长生命周期的引用也是已定义的行为。但是,我不明白为什么
rustc
在这方面会以不同方式处理无限生命周期。如果是这样,我希望Rustonomicon包含一条注释说明它的确如此,并且无限生命周期仍然比错误绑定的生命周期更安全。
结论- Rustonomicon关于无限生命周期
"Dereferencing"指对一个生命周期不确定的引用进行解引用,根据Rustonomicon,这可能不是未定义行为。对于生命周期“有界但大于正确值”的引用,这是否适用尚不确定,但我认为是适用的。
transmute()
文档中的示例:标准库
std::mem::transmute()
文档的第二个示例:
Extending a lifetime [...]. This is advanced, very unsafe Rust!
struct R<'a>(&'a i32);
unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> {
std::mem::transmute::<R<'b>, R<'static>>(r)
}
*[...]*
这是最明确的证据,证明至少拥有比正确寿命更长的引用并不会导致未定义行为 - 否则,任何使用非“静态”引用调用此函数的人都会立即触发未定义行为,而此函数将变得非常无用。此外,对我来说,这意味着您也可以解引用由
extend_lifetime
返回的引用 - 否则,该函数的好处是什么?
结论 -
transmute()
文档示例
transmute()
文档中的示例似乎暗示着解引用具有比正确寿命更长的引用是被允许的。
最终结论
遗憾的是,Unsafe Rust的详细文档仍然不完整,并且存在许多关于像这样的边缘案例的问题,文档无法给出明确的答案。但是,所有与问题相关的文档似乎都暗示着在实际情况中解引用所讨论的引用是良好定义的行为。是否足够让您像这样做某些事情取决于您 - 对我来说可能足够了。
然而,您真的不应该这样做。
为了澄清:虽然这段代码可能定义良好,但它肯定仍然是一个“踩脚枪”。跨越函数边界传递这样的引用是一个坏主意,特别是如果该函数是公共的,并且可以被其他模块/板条箱调用。即使不跨越函数边界传递,仍然很容易误用此引用,导致您的代码引起未定义的行为。如果您认为可能需要这样做,请重新考虑是否可以重构代码以避免转换寿命。例如,直接使用具有不正确寿命的引用而不是裸指针可能更安全(或者至少更清楚地表明这是完全不安全的)。
x
。编译器没有捕获到这一点,因为我们欺骗了生命周期。但我的问题是:仅仅是欺骗生命周期,而没有做任何其他“坏事”,就已经是未定义行为了吗? - Lukas Kalbertodt