O Contrato de Rust unsafe
A promessa central de Rust é segurança de memória sem um garbage collector, verificada inteiramente em tempo de compilação. unsafe é a palavra-chave que permite a um programador sair das garantias de tempo de compilação para o punhado de operações que o compilador não consegue verificar por si só.
Não desativa Rust. Ele desbloqueia cinco capacidades específicas e deixa todas as outras regras em vigor, que é a fonte da maior parte da confusão para pessoas novas na linguagem. Entender exatamente o que muda sob unsafe, e o que permanece o mesmo, é a base para todo o resto nesta seção: ponteiros brutos, FFI, o modelo de memória e a disciplina de construir abstrações seguras sobre código unsafe.
Resumo
unsafeconcede acesso a cinco operações específicas que o compilador não pode verificar automaticamente; não desabilita o restante da rede de segurança de Rust.- Por que Importa: Alguns trabalhos do mundo real, como falar com hardware, chamar bibliotecas C, construir estruturas de dados de alto desempenho, exigem operações que o borrow checker não consegue provar como seguras, e Rust precisa de uma maneira principiada de permiti-las sem abandonar a segurança em todos os outros lugares.
- Conceitos Chave: superpoderes
unsafe, soundness, comportamento indefinido (UB), abstração segura, invariante, limite de encapsulamento. - Quando Usar: Envolver uma biblioteca C atrás de uma API Rust, implementar uma estrutura de dados que o borrow checker não consegue expressar, escrever código crítico de desempenho que necessita de aritmética de ponteiro bruto, ou trabalhar diretamente com hardware em contextos embarcados.
- Limitações / Trade-offs: Cada bloco
unsafetransfere a responsabilidade pela correção do compilador para o programador, e um único invariante perdido pode produzir comportamento indefinido que se manifesta longe de sua causa real. - Tópicos Relacionados: ponteiros brutos, o modelo de memória de Rust, interfaces de função estrangeira, Miri e sanitizers.
Fundamentos
unsafe é uma palavra-chave, não um modo.
Envolver código em um bloco unsafe, marcar uma função unsafe fn, ou escrever unsafe trait não desabilita a verificação de empréstimo (borrow checking), semântica de movimento, ou o sistema de tipos dentro desse escopo. Todas as regras que se aplicam a Rust seguro ainda se aplicam dentro de código unsafe, com cinco adições específicas sobrepostas.
Essas cinco adições são o que a referência de Rust chama de "superpoderes unsafe": desreferenciar um ponteiro bruto, chamar uma unsafe fn (incluindo funções estrangeiras), implementar um unsafe trait, acessar ou mutar uma variável static mut, e ler um campo de uma union.
Nada mais muda.
Você não pode usar unsafe para contornar um tempo de vida (lifetime), pular silenciosamente uma verificação de limite, ou fazer um tipo implementar um trait que ele não implementa. O sistema de tipos continua funcionando exatamente como no código seguro.
Uma analogia útil é um canteiro de obras com uma gaiola de ferramentas trancada. A maior parte do trabalho usa ferramentas padrão com proteções de segurança embutidas, e o compilador é o inspetor verificando automaticamente cada uso. Algumas tarefas precisam das ferramentas elétricas na gaiola, e pegar a chave não desliga as outras regras do local; apenas significa que o inspetor confia no operador naquela ferramenta específica.
A palavra "unsafe" também é uma fonte comum de reações exageradas.
Um bloco unsafe não significa "este código é perigoso" em algum sentido vago. Significa "este código executa uma operação cuja segurança o compilador não pode verificar, e o programador está afirmando que os pré-requisitos dessa operação são válidos."
Código unsafe bem escrito, apoiado por um argumento documentado sobre por que seus pré-requisitos sempre se mantêm, pode ser exatamente tão confiável quanto código seguro. Toda a biblioteca padrão de Rust é construída dessa forma: Vec, String, e HashMap usam unsafe internamente para obter desempenho que o borrow checker sozinho não consegue expressar, enquanto apresentam uma API pública totalmente segura.
Mecânicas e Interações
O conceito que realmente importa para escrever código unsafe correto é soundness, e é fácil de julgar mal no início.
Soundness não é uma propriedade de um bloco unsafe isoladamente. É uma propriedade da API pública que o envolve.
Uma função é sound se nenhum chamador seguro possível, usando apenas Rust seguro, puder acionar comportamento indefinido chamando-a. Uma função é unsound se existir mesmo um padrão de chamada segura, por mais obscuro que seja, que leve a UB, mesmo que esse padrão nunca apareça na base de código atual.
Essa distinção reformula o trabalho de escrever código unsafe.
A pergunta nunca é "este local de chamada específico parece seguro?", mas sim "eu provei que todos os chamadores seguros possíveis, incluindo aqueles que não imaginei, não podem usar indevidamente esta função?" Essa prova geralmente assume a forma de um invariante, alguma condição que deve ser mantida sempre que um valor de um determinado tipo existe, mantido por todos os métodos seguros que o tipo expõe.
Considere um exemplo mínimo: um tipo que promete que seu buffer interno está sempre inicializado até um campo len antes que algo o leia.
pub struct Buffer {
data: Vec<std::mem::MaybeUninit<u8>>,
len: usize, // invariante: data[..len] está sempre inicializado
}
impl Buffer {
pub fn push(&mut self, byte: u8) {
self.data[self.len].write(byte); // escrita unsafe, invariante mantido
self.len += 1;
}
pub fn get(&self, i: usize) -> Option<u8> {
if i < self.len {
// SAFETY: o invariante garante que data[..len] está inicializado
Some(unsafe { self.data[i].assume_init() })
} else {
None
}
}
}Cada operação unsafe dentro de Buffer depende do mesmo invariante, e cada método seguro que toca len é responsável por manter esse invariante verdadeiro. O argumento de soundness para todo o tipo colapsa para uma frase, verificada em um lugar, em vez de ser redervada em cada local de chamada.
É por isso também que o código unsafe interage tão de perto com o encapsulamento.
O sistema de privacidade de Rust, pub versus campos privados, não é apenas uma conveniência de design de API quando código unsafe está envolvido. É o mecanismo que torna a soundness provável.
Se len fosse um campo público, qualquer chamador seguro poderia defini-lo além do comprimento real do buffer e transformar get em uma leitura de memória não inicializada, uma violação instantânea de soundness, apesar do bloco unsafe em si ter sido escrito corretamente. O encapsulamento é o que permite a um autor de biblioteca dizer "confie em mim" uma vez, em uma pequena superfície auditável, em vez de pedir a cada usuário downstream para raciocinar sobre memória bruta.
O próprio comportamento indefinido merece um modelo mental aqui, porque ele se comporta de maneira diferente de um bug normal.
Um erro lógico em Rust seguro produz uma resposta errada, um pânico, ou um crash, algo observável e depurável localmente. UB é diferente: o compilador tem permissão para assumir que nunca acontece, e uma vez que um pré-requisito unsafe é realmente violado, o otimizador é livre para transformar o código circundante de maneiras que fazem a falha aparecer em qualquer lugar, inclusive em código que parece não relacionado.
É por isso que bugs unsafe são notórios por aparecerem como um crash em uma função totalmente diferente, às vezes lançamentos depois, e por que "funcionou quando testei" carrega quase nenhum peso probatório para código unsafe.
Considerações Avançadas e Aplicações
Em bases de código de produção de Rust, unsafe geralmente é concentrado em um pequeno número de módulos cuidadosamente revisados com um argumento de segurança documentado, frequentemente um comentário de documentação # Safety acima de cada unsafe fn, em vez de espalhado pelo código.
Grandes equipes de sistemas frequentemente rastreiam um "inventário unsafe", uma auditoria periódica de cada bloco unsafe, porque o custo de uma abstração unsound escala com o quanto de código seguro depende dela. Várias disciplinas competem para gerenciar esse risco, e bases de código maduras tipicamente combinam mais de uma.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
Revisão manual + comentários # Safety | Barato, funciona em todos os lugares, força o autor a escrever o argumento | Depende da diligência humana; comentários podem ficar desatualizados | Cada bloco unsafe, como linha de base |
| Miri (interpretador) | Captura UB real (leituras não inicializadas, violações de aliasing, fora dos limites) que compila limpo | Lento, não pode executar todo o código (sem chamadas de sistema brutas ou a maior parte do FFI) | CI em crates com muito unsafe |
| Sanitizers (AddressSanitizer, etc.) | Captura erros de memória com velocidade nativa, funciona com FFI | Requer toolchain nightly, específico da plataforma | Testando FFI pesado ou unsafe crítico de desempenho |
| Fuzzing | Encontra casos extremos que humanos não pensam em testar | Precisa de um harness e orçamento de tempo; probabilístico, não prova | Parsers e estruturas de dados com invariantes complexos |
| Typestate / codificação formal | Torna o uso indevido um erro de compilação em vez de uma regra documentada | Adiciona complexidade à API; nem sempre expressível | Protocolos com uma ordem estrita de uso válido |
A distinção entre unsafe fn e um pub fn contendo um bloco unsafe vale a pena ser destacada, pois é um erro de design comum.
Marcar uma função como unsafe fn transfere a obrigação de segurança para o chamador, que deve ler a documentação e manter manualmente os pré-requisitos que o compilador não verificará. Um pub fn seguro que usa unsafe internamente mantém essa obrigação dentro da própria função, o que é quase sempre preferível, pois apenas o implementador precisa raciocinar sobre o invariante em vez de todos os chamadores para sempre. Reserve unsafe fn para casos em que o pré-requisito genuinamente não pode ser verificado pela própria função, como "este ponteiro bruto deve ser válido", onde apenas o chamador poderia saber disso.
Limites de FFI merecem uma nota especial, pois as garantias de Rust param no limite e o lado estrangeiro não oferece nenhuma das suas. Um padrão comum isola todas as chamadas FFI brutas em um módulo interno, e então envolve cada handle estrangeiro em um tipo seguro antes que ele chegue ao resto da base de código, de modo que o limite de confiança seja um único arquivo auditado.
A própria trajetória do projeto Rust mostra essa disciplina evoluindo: MaybeUninit substituiu o antigo e unsound mem::uninitialized(), a edição 2024 apertou as regras em torno de blocos unsafe extern, e Miri passou de um projeto de pesquisa para CI padrão para crates com muito unsafe. Cada mudança torna estruturalmente mais difícil escrever código unsound por acidente.
Concepções Erradas Comuns
- "
unsafedesabilita o borrow checker" - não desabilita; empréstimos, semântica de movimento e tempos de vida são aplicados identicamente dentro de um blocounsafe, que apenas adiciona permissão para as cinco operações listadas acima. - "Se compila e roda corretamente nos testes, o código
unsafeestá bom" - comportamento indefinido é uma propriedade de qualquer execução possível, não daquela que você testou, então passar nos testes é, na melhor das hipóteses, uma evidência fraca. - "Apenas o código dentro das chaves
unsafe {}precisa estar correto" - soundness é uma propriedade de toda a API pública em torno desse bloco, incluindo todos os métodos seguros que podem influenciar o estado que o códigounsafelê. - "
unsafe fné mais perigoso que umpub fnseguro com um blocounsafedentro" - é um sinal de design sobre quem possui a obrigação de segurança, não uma medida de perigo; uma função segura mal encapsulada pode ser igualmente unsound. - "Programas Rust com código
unsafenão são mais seguros que C" - a vasta maioria do programa, tudo fora dos blocosunsafe, ainda recebe segurança completa verificada pelo compilador, e a superfícieunsafeé explícita e pesquisável de uma forma que a falta de segurança implícita de C não é. - "Código
unsafeprecisa ser rápido, então um pouco de desleixo é aceitável" - desempenho e correção são eixos separados; códigounsafeunsound que por acaso é rápido ainda é um bug crítico esperando para ser acionado.
FAQs
Qual é a diferença entre "unsafe" e "unsound"?
unsafe é uma palavra-chave que marca código que executa operações que o compilador não pode verificar. Unsound descreve uma API segura que um chamador seguro pode usar indevidamente para acionar comportamento indefinido. Código unsafe bem escrito produz APIs sound; o objetivo é sempre a soundness, não meramente a presença ou ausência da palavra-chave.
O Rust `unsafe` desliga o verificador de tipos?
Não. Verificação de tipos, resolução de traits e limites genéricos são executados exatamente como em código seguro. unsafe apenas adiciona permissão para cinco operações específicas; não muda como o sistema de tipos avalia o resto da função.
Por que a biblioteca padrão usa tanto código unsafe?
Tipos como Vec e String gerenciam memória bruta diretamente para desempenho de maneiras que o borrow checker não consegue expressar com segurança. A biblioteca paga esse custo uma vez, atrás de um limite revisado e fuzzado, para que os usuários downstream nunca precisem.
Como sei se meu código `unsafe` é sound?
Nenhuma verificação automatizada única prova a soundness em geral. Equipes combinam um argumento # Safety escrito, Miri em CI, fuzzing e revisão de alguém experiente com Rust unsafe. A confiança é construída, não provada diretamente, fora de casos estreitos passíveis de métodos formais.
Qual é o menor bloco `unsafe` que devo escrever?
O menor possível, idealmente uma única expressão como um desreferenciamento de ponteiro, imediatamente protegido por uma verificação segura. Grandes blocos unsafe tornam mais difícil ver qual operação específica depende de qual invariante, e mais difícil de revisar.
Código seguro pode causar comportamento indefinido por si só?
Não por si só. UB em um programa Rust sempre remonta a um pré-requisito unsafe sendo violado em algum lugar, mesmo que a violação ocorra profundamente em uma dependência. Código seguro que chama uma API segura unsound expõe o bug, mas a causa raiz é a implementação unsafe unsound.
É `unsafe { *raw_ptr }` sempre errado?
Não, desreferenciar um ponteiro bruto é uma das cinco coisas que unsafe existe para permitir. Está correto desde que o ponteiro seja válido, devidamente alinhado e aponte para um valor inicializado do tipo correto no momento da desreferência, condições que o programador deve garantir de alguma outra forma.
Por que `unsafe fn` transfere a responsabilidade para o chamador?
Porque a função não tem como verificar seu próprio pré-requisito, muitas vezes porque o pré-requisito é sobre o contexto do chamador, como a proveniência de um ponteiro ou um invariante de comprimento, em vez de algo visível dentro do corpo da função. Documentar esse pré-requisito em um comentário # Safety é o que permite aos chamadores mantê-lo corretamente.
Qual é a relação entre unsafe e FFI?
Cada chamada para uma função estrangeira é unsafe porque o compilador Rust não tem visibilidade sobre o que o código estrangeiro realmente faz. O padrão comum é uma camada unsafe fina e pesadamente auditada diretamente em torno das chamadas FFI, com uma API Rust segura construída por cima para o resto da base de código.
Mais código `unsafe` significa código pior?
Não inerentemente. Domínios como sistemas embarcados, alocadores personalizados e parsers de zero-cópia genuinamente exigem operações unsafe. O que importa é se a superfície é pequena, documentada e encapsulada atrás de uma API segura e sound, não a contagem bruta de palavras-chave unsafe.
Como Miri ajuda sem mudar o código?
Miri interpreta o programa em vez de compilá-lo nativamente, verificando cada acesso à memória contra as regras de aliasing e inicialização de Rust enquanto ele roda. Ele captura UB que uma execução de teste normal toleraria silenciosamente.
Posso escrever um programa Rust com zero código `unsafe`?
Sim, e muitas bases de código de aplicativos o fazem, dependendo inteiramente de abstrações seguras da biblioteca padrão e crates verificados. #![forbid(unsafe_code)] na raiz do crate impõe isso em tempo de compilação para equipes que desejam a garantia.
Relacionados
- Fundamentos de
unsafe- as cinco operações queunsafedesbloqueia, com exemplos executáveis - Abstrações Sound sobre
unsafe- padrões para encapsular códigounsafeatrás de uma API segura - Comportamento Indefinido - o que é UB e como ele se manifesta
- O Modelo de Memória de Rust - as regras de layout e aliasing que o código
unsafedeve respeitar - Miri & Sanitizers - ferramentas para capturar unsoundness antes da produção
Versões da Pilha: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.