FlawOpen

flawopen.com/Segurança de memória/Rust unsafe

O bloco unsafe do Rust continua seguro para a memória?

CWE-787: Out-of-bounds WritePágina de referência
Resposta curta

unsafe não desativa as verificações do Rust. Ele permite operações específicas cujas condições de segurança o compilador não consegue provar; o programador precisa cumprir essas obrigações. O ponto essencial é que uma abstração unsafe não sound pode permitir que chamadas safe comuns causem comportamento indefinido (UB), mesmo sem usar a palavra unsafe.

Termos-chave

Operação unsafe:
Operação com condições de segurança que o compilador não consegue provar, como desreferenciar um ponteiro raw.
Abstração sound:
API safe cujos chamadores safe não conseguem causar comportamento indefinido.
size_hint():
Estimativa do tamanho de um iterador para otimização; não é uma contagem exata nem prova de segurança de memória.
Comportamento indefinido (UB):
Comportamento que o Rust não define; código unsafe deve impedir que chamadores safe o provoquem.

Como uma abstração safe não sound falha

Etapa 1

Uma API safe recebe um iterador

O chamador fornece um iterador comum a uma função sem pré-condição unsafe.

Etapa 2

A implementação reserva com base em uma estimativa

A função reserva apenas o limite inferior informado, que pode ser menor que o número de itens produzidos.

Etapa 3

Escritas sem verificação excedem a capacidade

A implementação grava cada item por um ponteiro raw sem aumentar a alocação nem conferir a capacidade a cada item.

Etapa 4

O uso safe alcança UB

Se o iterador produzir mais itens do que cabem, uma chamada safe pode escrever fora dos limites. Use Vec::extend ou outra implementação que aumente a capacidade com segurança.

NÃO SOUND
// UNSOUND: reserve uses only the lower hint,
// then unchecked writes consume every item.
fn append_unsound(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) {
    let (lower, _) = iter.size_hint();
    dst.reserve(lower);
    let start = dst.len();
    let mut written = 0;
    unsafe {
        let out = dst.as_mut_ptr().add(start);
        for item in iter {
            // UB once writes exceed spare capacity.
            out.add(written).write(item);
            written += 1;
        }
        dst.set_len(start + written);
    }
}
SOUND
// SOUND: Vec::extend handles capacity
// growth and length updates safely.
fn append(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) {
    dst.extend(iter);
}

O que a garantia do Rust realmente diz

A garantia precisa é: Rust safe não pode causar comportamento indefinido, desde que o código unsafe do qual depende seja sound. A Rust Reference define soundness na fronteira: código unsafe deve garantir que clientes safe não provoquem UB. O compilador não verifica todas as obrigações de segurança dentro de uma operação unsafe; programadores e revisores precisam comprová-las.

Isso ainda é uma grande vantagem prática em relação a C e C++: Rust deixa operações sensíveis à memória e suas obrigações mais visíveis, permitindo que revisores se concentrem nas fronteiras unsafe e nas abstrações safe construídas sobre elas. Essas fronteiras incluem blocos unsafe, contratos de funções unsafe, implementações de traits unsafe, declarações FFI e atributos unsafe; não ficam sempre restritas a um único bloco pequeno. Um caso concreto é o CVE-2021-25900 no smallvec: SmallVec::insert_many reservava espaço usando o limite inferior de um iterador e gravava mais elementos do que a alocação comportava; a gravidade foi CVSS 9.8.

Soundness é o padrão, não cuidado

Uma API é não sound se código safe puder usá-la para causar comportamento indefinido. Por exemplo, Iterator::size_hint fornece limites úteis para otimização, não uma contagem exata: um iterador válido pode produzir mais itens que o limite inferior, e um iterador com defeito pode violar sua estimativa. A documentação padrão diz explicitamente que essa dica não deve ser usada para remover verificações de limites em código unsafe. Comportamentos incorretos de traits safe também podem causar erros lógicos ou panics; isso não é automaticamente UB, mas o código unsafe deve preservar invariantes de memória mesmo quando o código fornecido pelo chamador se comporta de modo inesperado.

O código unsafe não é local: uma invariante de memória estabelecida em um ponto pode ser usada por APIs safe acima dele. Se a invariante estiver errada, o defeito está na abstração que expôs a API safe, não nos chamadores safe que a usam conforme o contrato.

Regras práticas

Perguntas frequentes

Posso simplesmente proibir unsafe?

#![forbid(unsafe_code)] pode impedir a sintaxe unsafe no crate onde é habilitada, o que é útil para crates de aplicação. Não controla dependências, e algumas implementações, fronteiras FFI e bibliotecas precisam de operações unsafe.

Existe uma forma de permitir que código unsafe confie em uma trait?

Sim. Uma unsafe trait documenta obrigações que as implementações precisam cumprir; código unsafe pode confiar nelas. TrustedLen é uma trait instável da biblioteca padrão para iteradores que prometem uma estimativa de tamanho exata. Iterator::size_hint comum não oferece essa garantia.

Isso enfraquece o argumento a favor do Rust?

Não. O modelo de segurança do Rust reduz onde surgem obrigações de segurança de memória e as torna explícitas nas fronteiras unsafe. Ainda é necessário auditar código unsafe, dependências, contratos FFI e APIs safe construídas sobre implementações unsafe.

Referências

Veja também strncpy() é mais segura que strcpy()? CWE vs. CVE: qual é a diferença? Mass assignment: riscos e listas seguras de campos Vazamento de estado serverless: isole dados por requisição PHP == e comparações de magic hash