Este é um problema de comportamento potencialmente indefinido de memória não inicializada em Rust inseguro.
- primeiro, alocando o vetor da maneira segura usual (tipo
Vec<Vec<usize>>
, ou deve ser vetor de algo no heap em vez da pilha ; em outras palavras,Vec<usize>
o tipo não entrará em pânico aqui); - segundo, clone o vetor em um novo escopo ;
- terceiro, alocar um novo vetor por meio de uma maneira não inicializada e insegura (sem usar
MaybeUninit
); então ocorre uma liberação dupla.
O código ( playground ) listado da seguinte forma:
#[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();
}
- modo de depuração: sinal 6 (SIGABRT): abortar programa, liberação dupla detectada no tcache 2;
case_2
ficará preso aqui; - modo de liberação: sinal 4 (SIGILL): instrução ilegal; até mesmo
case_1
ficará preso aqui.
Pelo meu senso comum, o código em si não pretende liberar nenhuma variável duas vezes. Nós apenas declaramos uma variável não inicializada, mas não usamos essa variável. Se isso não for um bug, a otimização do compilador é provavelmente a única razão que pode explicar esse problema.
O que me deixa curioso é que: se esse código realmente aciona comportamento indefinido em rust inseguro, essa otimização do compilador pode causar problemas de double free ou outros. E
- se for realmente um UB, sinto que esse código pode realmente confundir os novatos em Rust (ou seja, eu mesmo).
- se não for um UB, é um bug de ferrugem insegura?
Além disso, sabe-se que o uso MaybeUninit
correto pode evitar erros de tempo de execução double-free como este:
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) };
}
Ainda assim, MaybeUninit
pode ser inconveniente em muitas circunstâncias. Subjetivamente, prefiro usar Vec<T>
em vez de Vec<MaybeUninit<T>>
, especialmente quando posso ter certeza de que os valores desse vetor não inicializado serão preenchidos corretamente mais tarde.