这是不安全锈蚀中未初始化内存的潜在未定义行为的问题。
- 首先,通过通常的安全方式分配向量(类型
Vec<Vec<usize>>
,或者必须是堆而不是堆栈上的某个向量;换句话说,Vec<usize>
类型不会在这里引起恐慌); - 第二,在新的范围内克隆向量;
- 第三,通过不安全的未初始化方式(不使用
MaybeUninit
)分配一个新的向量;然后发生双重释放。
代码(playground)如下:
#[deny(clippy::uninit_vec)]
unsafe fn uninitialized_vec<T>(size: usize) -> Vec<T> {
let mut v: Vec<T> = Vec::with_capacity(size);
unsafe { v.set_len(size) };
v
}
fn case_1() {
println!("=== Case 1 ===");
let vec_a: Vec<Vec<usize>> = vec![vec![0]; 4];
let _ = vec_a.clone();
let _: Vec<Vec<usize>> = unsafe { uninitialized_vec(4) };
}
fn case_2() {
println!("=== Case 2 ===");
let vec_a: Vec<Vec<usize>> = vec![vec![0]; 4];
{
let _ = vec_a.clone();
}
let _: Vec<Vec<usize>> = unsafe { uninitialized_vec(4) };
}
fn main() {
case_1();
case_2();
}
- 调试模式:信号 6(SIGABRT):中止程序,在 tcache 2 中检测到双重释放;
case_2
将卡在这里; - 释放模式:信号4(SIGILL):非法指令;甚至
case_1
会卡在这里。
从我的常识来看,代码本身并没有打算对任何变量进行双重释放。我们只是声明了一个未初始化的变量,但没有使用该变量。如果这不是一个错误,那么编译器优化可能是唯一可以解释这个问题的原因。
我很好奇的是:这段代码是否真的会在不安全的 Rust 中触发未定义的行为,编译器优化是否会导致双重释放或其他问题。
- 如果它实际上是一个 UB,我觉得这段代码确实会让 rust 新手感到困惑(这是我自己说的)。
- 如果不是UB的话,是不是存在不安全锈蚀的bug?
此外,众所周知,通过MaybeUninit
正确使用可以避免这样的双重释放运行时错误:
use std::mem::MaybeUninit;
unsafe fn uninitialized_vec<T>(size: usize) -> Vec<MaybeUninit<T>> {
let mut v: Vec<MaybeUninit<T>> = Vec::with_capacity(size);
unsafe { v.set_len(size) };
v
}
fn main() {
println!("=== This will not cause error ===");
let vec_a: Vec<Vec<usize>> = vec![vec![0]; 12];
{
let _ = vec_a.clone();
}
let _: Vec<MaybeUninit<Vec<usize>>> = unsafe { uninitialized_vec(12) };
}
尽管如此,MaybeUninit
在很多情况下还是不方便。主观上,我更喜欢使用Vec<T>
而不是Vec<MaybeUninit<T>>
,尤其是当我可以确保这个未初始化向量的值稍后会被正确填充时。