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.
Isso está incorreto. Estamos usando essa variável - ou seja, estamos descartando o
Vec
, portanto, descartando todos osVec
s nele, e como eles não são inicializados, estamos tentando descartar memória arbitrária.Sim, de acordo com a documentação para
set_len
:Este requisito é explicitamente violado.
Se você disparar um comportamento indefinido, o processo de compilação pode fazer qualquer coisa com seu programa , por definição. Não é "este código pode estar mal otimizado desta forma" - é "você mentiu para o compilador, todas as apostas estão canceladas".
Isenção de responsabilidade: Cerberus respondeu à pergunta principal. Vou me concentrar nas melhores práticas/perguntas implícitas.
Uso correto de
set_len
Conforme mencionado na resposta do @Cerberus,
set_len
só deve ser usado depois que os elementos forem inicializados.Ou seja, a maneira correta de usar
set_len
é:Pré-lavagem de suas calças com Rust
Gankra 1 , um dos principais designers do Rust inseguro nos primeiros dias, escreveu um artigo chamado Pre-pooping your pants with Rust , que é muito apropriado aqui.
A essência do artigo é que uma das principais dificuldades para escrever um código sólido
unsafe
é prever todas as maneiras pelas quais algo pode dar errado: qualquer retorno antecipado, qualquer pânico, etc., que podem ocorrer antes de você "cumprir" uma promessa.Dica: é basicamente impossível, tendo em vista que o código está em constante mudança.
A solução que Grankra propôs é, portanto, "pré-cagar" suas calças. Por exemplo, quando você chama
vec.drain(start..end)
, todos os elementos emstart..end
devem ser removidos, e apenas os elementos em0..start
eend..
(catenados juntos) permanecem. Mas quem sabe o que pode ocorrer ao fazer tudo isso?Assim, o código para drenagem irá:
vec
parastart
. Ele não o deixará no comprimento atual, nem o definirá no comprimento final.start..end
tenham sido retirados.end..
(start..
preenchendo o buraco, parcialmente).vec
para o número final de elementos que realmente restam.Se por qualquer razão o
Drain
iterador vazar no meio de tudo isso, deixando um buraco com elementos não inicializados... está tudo certo , porque todos os elementos0..start
estão (ainda) inicializados, e essa é a única promessa quevec
está sendo feita até (4), e em (4) tudo está bem novamente.Ou, dito de outra forma: você nunca promete que fará algo que não seja seguro; em vez disso, você primeiro faz e depois anuncia que está feito.
1 Você pode conhecer Gankra de Learn Rust With Entirely Too Many Linked Lists .
Talvez o Uninit seja inconveniente
Sim. Sim, é.
Na verdade, código escrito corretamente
unsafe
, com todas as// Safety
anotações, é tedioso de escrever. E verboso. É o que é preciso para escreverunsafe
código verificável e sólido.Se você não se importa, então não use
unsafe
!Gostaria de ressaltar que não há benefício algum em usar
unsafe
nos cenários que você demonstrou aqui:defaulted_vec
criaráVec
4 valores padrão, que são baratos — porqueVec::new()
não alocam nada — e você poderá usá-los/substituí-los quando quiser.