Rust é famoso por suas garantias de segurança de memória em tempo de compilação. O borrow checker elimina use-after-free, double-free, data races e dangling pointers sem precisar de garbage collector. Mas existe uma saída deliberada desse sistema: o bloco unsafe.
unsafe não desativa o compilador. Ele transfere para você a responsabilidade de provar que certas invariantes são respeitadas — invariantes que o compilador não consegue verificar sozinho. Usado corretamente, é a ferramenta que permite construir abstrações seguras sobre código de baixo nível. Usado de forma irresponsável, é a porta de entrada para os mesmos bugs que Rust promete eliminar.
Neste artigo exploramos o que unsafe habilita, quando seu uso é justificado, como contê-lo com o padrão de abstração segura, e as armadilhas mais comuns.
O que unsafe realmente habilita
Ao entrar em um bloco unsafe, você ganha acesso a cinco capacidades adicionais que o Rust normalmente proíbe:
- Desreferenciar ponteiros raw (
*const Te*mut T) - Chamar funções unsafe — incluindo funções
externde FFI - Acessar ou modificar variáveis estáticas mutáveis (
static mut) - Implementar traits unsafe (
Send,Sync,GlobalAlloc) - Acessar campos de unions
É só isso. O borrow checker continua ativo. O sistema de tipos continua intacto. Você não pode criar referências inválidas "magicamente" apenas por estar em um bloco unsafe — você continua sujeito às regras de lifetime e mutabilidade para referências seguras.
fn main() {
let x = 42i32;
let ponteiro: *const i32 = &x;
// Fora do unsafe: não é possível desreferenciar ponteiros raw
// let valor = *ponteiro; // erro de compilação
// Dentro do unsafe: você afirma que o ponteiro é válido
let valor = unsafe { *ponteiro };
println!("Valor: {valor}"); // 42
}
Quando usar unsafe
A resposta honesta é: com a menor frequência possível, e apenas quando não há alternativa segura viável. Os casos legítimos recorrentes são quatro.
1. FFI — Foreign Function Interface
Chamar código C, C++ ou qualquer biblioteca externa exige unsafe porque o compilador Rust não pode verificar o comportamento desse código:
use std::ffi::CString;
use std::os::raw::c_char;
// Declaração de função externa (C)
extern "C" {
fn strlen(s: *const c_char) -> usize;
}
fn comprimento_string_c(s: &str) -> usize {
let c_string = CString::new(s).expect("string contém nul byte");
// unsafe porque:
// 1. chamamos código externo cujo comportamento não verificamos
// 2. passamos um ponteiro raw
unsafe { strlen(c_string.as_ptr()) }
}
fn main() {
println!("{}", comprimento_string_c("hello")); // 5
}
2. Operações com ponteiros raw para performance
Algoritmos que manipulam memória diretamente — como parsers de alto desempenho, codecs, ou implementações de estruturas de dados — às vezes precisam de aritmética de ponteiros:
/// Copia `n` bytes de `src` para `dst`.
/// # Safety
/// - `src` e `dst` devem ser válidos para leitura e escrita de `n` bytes
/// - As regiões não devem se sobrepor (use `copy` se puderem se sobrepor)
unsafe fn copiar_bytes(dst: *mut u8, src: *const u8, n: usize) {
for i in 0..n {
*dst.add(i) = *src.add(i);
}
}
3. Construir abstrações que o borrow checker não consegue verificar
O exemplo clássico é dividir um slice em duas partes mutáveis. O borrow checker proibiria duas referências mutáveis ao mesmo slice, mas em regiões não sobrepostas isso é seguro:
/// Divide um slice em duas metades mutáveis sem sobreposição.
fn dividir_em_dois(slice: &mut [i32]) -> (&mut [i32], &mut [i32]) {
let meio = slice.len() / 2;
let ptr = slice.as_mut_ptr();
unsafe {
(
std::slice::from_raw_parts_mut(ptr, meio),
std::slice::from_raw_parts_mut(ptr.add(meio), slice.len() - meio),
)
}
}
fn main() {
let mut numeros = vec![1, 2, 3, 4, 5, 6];
let (esquerda, direita) = dividir_em_dois(&mut numeros);
esquerda[0] = 10;
direita[0] = 40;
println!("{:?}", numeros); // [10, 2, 3, 40, 5, 6]
}
4. Implementar alocadores e coleções personalizadas
Estruturas como Vec, HashMap e Box da stdlib são internamente construídas sobre unsafe. Se você precisar de uma estrutura de dados com semântica de memória diferente — como um arena allocator ou um ring buffer lock-free — precisará do mesmo.
O padrão fundamental: abstrações seguras sobre código unsafe
A regra de ouro do unsafe em Rust é: isole o código unsafe na menor região possível e exponha uma interface completamente segura para o exterior. Isso é o que a stdlib inteira faz.
/// Um buffer circular de tamanho fixo.
/// Internamente usa aritmética de ponteiros, mas a API é completamente segura.
pub struct RingBuffer<T, const N: usize> {
dados: [std::mem::MaybeUninit<T>; N],
leitura: usize,
escrita: usize,
tamanho: usize,
}
impl<T, const N: usize> RingBuffer<T, N> {
pub fn novo() -> Self {
RingBuffer {
dados: unsafe { std::mem::MaybeUninit::uninit().assume_init() },
leitura: 0,
escrita: 0,
tamanho: 0,
}
}
/// Insere um elemento. Retorna false se o buffer estiver cheio.
pub fn push(&mut self, valor: T) -> bool {
if self.tamanho == N {
return false;
}
// Safety: escrita está sempre dentro dos limites [0, N)
// e a posição não está inicializada ou já foi lida.
unsafe {
self.dados[self.escrita].write(valor);
}
self.escrita = (self.escrita + 1) % N;
self.tamanho += 1;
true
}
/// Remove e retorna o próximo elemento, ou None se vazio.
pub fn pop(&mut self) -> Option<T> {
if self.tamanho == 0 {
return None;
}
// Safety: leitura aponta para um elemento inicializado,
// garantido pelo invariante: tamanho > 0.
let valor = unsafe {
self.dados[self.leitura].assume_init_read()
};
self.leitura = (self.leitura + 1) % N;
self.tamanho -= 1;
Some(valor)
}
pub fn tamanho(&self) -> usize {
self.tamanho
}
}
// Drop correto para elementos inicializados
impl<T, const N: usize> Drop for RingBuffer<T, N> {
fn drop(&mut self) {
while self.pop().is_some() {}
}
}
Quem usa RingBuffer não vê nenhum unsafe. O contrato com o compilador é interno ao módulo.
Documentando safety com # Safety
Toda função unsafe que você escreve deve ter uma seção # Safety no doc comment explicando:
- Quais são os pré-requisitos para chamá-la corretamente
- O que acontece se eles forem violados (undefined behavior)
/// Retorna uma referência ao elemento no índice sem checar limites.
///
/// # Safety
///
/// O chamador deve garantir que:
/// - `indice` é menor que o comprimento do slice
/// - O slice não é modificado enquanto a referência estiver em uso
///
/// Violar essas condições resulta em undefined behavior.
pub unsafe fn get_unchecked_manual<T>(slice: &[T], indice: usize) -> &T {
&*slice.as_ptr().add(indice)
}
Esta documentação serve dois propósitos: ajuda quem chama a usar a função corretamente, e força você a articular explicitamente o que está assumindo — o que frequentemente revela bugs de lógica antes mesmo de executar o código.
Unsafe traits: Send e Sync
Alguns traits são marcados como unsafe porque implementá-los incorretamente pode causar undefined behavior mesmo em código seguro que os usa. Os mais importantes são Send e Sync:
use std::sync::atomic::{AtomicUsize, Ordering};
/// Contador atômico que pode ser compartilhado entre threads.
pub struct ContadorAtomico {
valor: AtomicUsize,
}
impl ContadorAtomico {
pub fn novo(inicial: usize) -> Self {
ContadorAtomico {
valor: AtomicUsize::new(inicial),
}
}
pub fn incrementar(&self) -> usize {
self.valor.fetch_add(1, Ordering::SeqCst)
}
pub fn ler(&self) -> usize {
self.valor.load(Ordering::SeqCst)
}
}
// Safety: AtomicUsize é thread-safe por design.
// Não há estado não-atômico nesta struct.
unsafe impl Send for ContadorAtomico {}
unsafe impl Sync for ContadorAtomico {}
Normalmente você não precisa implementar Send e Sync manualmente — o compilador os deriva automaticamente quando todos os campos os implementam. A implementação manual é necessária quando você usa ponteiros raw ou tipos explicitamente não-Send/Sync internamente mas pode provar a segurança de thread por outros meios.
FFI na prática: integrando com uma biblioteca C
Um cenário real de FFI envolve criar bindings para uma biblioteca C existente. Veja o padrão completo:
// build.rs — diz ao Rust para linkar com a biblioteca
fn main() {
println!("cargo:rustc-link-lib=crypto"); // -lcrypto (OpenSSL)
println!("cargo:rustc-link-search=native=/usr/lib");
}
use std::ffi::CStr;
use std::os::raw::{c_char, c_int, c_uchar, c_uint};
// Tipos opacos da biblioteca C
#[repr(C)]
pub struct EvpMdCtx {
_privado: [u8; 0],
}
// Declarações FFI
extern "C" {
fn EVP_MD_CTX_new() -> *mut EvpMdCtx;
fn EVP_MD_CTX_free(ctx: *mut EvpMdCtx);
fn EVP_DigestInit_ex(ctx: *mut EvpMdCtx, tipo: *const u8, engine: *const u8) -> c_int;
fn EVP_DigestUpdate(ctx: *mut EvpMdCtx, dados: *const u8, len: usize) -> c_int;
fn EVP_DigestFinal_ex(ctx: *mut EvpMdCtx, md: *mut c_uchar, s: *mut c_uint) -> c_int;
fn EVP_sha256() -> *const u8;
}
/// Wrapper seguro em torno do contexto de digest OpenSSL.
/// O Drop garante que EVP_MD_CTX_free seja sempre chamado.
pub struct Sha256 {
ctx: *mut EvpMdCtx,
}
impl Sha256 {
pub fn novo() -> Result<Self, String> {
let ctx = unsafe { EVP_MD_CTX_new() };
if ctx.is_null() {
return Err("falha ao alocar contexto EVP".into());
}
let resultado = unsafe {
EVP_DigestInit_ex(ctx, EVP_sha256(), std::ptr::null())
};
if resultado != 1 {
unsafe { EVP_MD_CTX_free(ctx) };
return Err("falha ao inicializar digest SHA-256".into());
}
Ok(Sha256 { ctx })
}
pub fn atualizar(&mut self, dados: &[u8]) -> Result<(), String> {
let resultado = unsafe {
EVP_DigestUpdate(self.ctx, dados.as_ptr(), dados.len())
};
if resultado != 1 {
Err("falha ao atualizar digest".into())
} else {
Ok(())
}
}
pub fn finalizar(self) -> Result<[u8; 32], String> {
let mut hash = [0u8; 32];
let mut tamanho: c_uint = 32;
let resultado = unsafe {
EVP_DigestFinal_ex(self.ctx, hash.as_mut_ptr(), &mut tamanho)
};
if resultado != 1 {
Err("falha ao finalizar digest".into())
} else {
Ok(hash)
}
}
}
impl Drop for Sha256 {
fn drop(&mut self) {
if !self.ctx.is_null() {
unsafe { EVP_MD_CTX_free(self.ctx) };
}
}
}
O usuário desta API nunca vê um ponteiro raw. O Drop garante que vazamentos de memória sejam impossíveis mesmo em casos de erro.
Armadilhas comuns em código unsafe
Undefined Behavior — o inimigo invisível
Ao contrário de C, onde UB frequentemente "funciona" em compilações debug, o Rust com otimizações pode produzir comportamentos completamente inesperados diante de UB. As causas mais comuns:
fn exemplos_de_ub() {
// 1. Desreferenciar ponteiro nulo
let ptr: *const i32 = std::ptr::null();
// unsafe { let _ = *ptr; } // UB: segfault ou pior
// 2. Use-after-free
let referencia: &i32;
{
let x = 42i32;
referencia = unsafe {
// UB: referência sobrevive ao valor
&*((&x) as *const i32)
};
}
// println!("{}", referencia); // UB: x foi dropado
// 3. Criar referência não-alinhada
let dados = [0u8; 8];
let ptr_u32 = dados.as_ptr().wrapping_add(1) as *const u32;
// unsafe { let _ = *ptr_u32; } // UB em arquiteturas que exigem alinhamento
// 4. Transmute com tamanhos diferentes
// unsafe { let _: u64 = std::mem::transmute(42u32); } // UB: tamanhos diferentes
}
transmute — a ferramenta mais perigosa
std::mem::transmute reinterpreta os bytes de um tipo como outro. É útil em casos específicos mas extremamente perigoso:
// USO LEGÍTIMO: converter entre representações numéricas compatíveis
fn f32_para_bits(f: f32) -> u32 {
// Seguro: f32 e u32 têm o mesmo tamanho e qualquer padrão de bits é válido para u32
unsafe { std::mem::transmute(f) }
}
// Prefira quando disponível:
fn f32_para_bits_seguro(f: f32) -> u32 {
f.to_bits() // equivalente, sem unsafe
}
// USO PERIGOSO: converter lifetime de referências
fn estender_lifetime_errado<'a>(r: &'a str) -> &'static str {
// NUNCA FAÇA ISSO — UB se o valor original for dropado
unsafe { std::mem::transmute(r) }
}
Data races com static mut
// PERIGOSO: acesso concorrente a static mut é UB
static mut CONTADOR: u32 = 0;
fn incrementar_inseguro() {
unsafe {
CONTADOR += 1; // data race se chamado de múltiplas threads
}
}
// CORRETO: use AtomicU32
use std::sync::atomic::{AtomicU32, Ordering};
static CONTADOR_SEGURO: AtomicU32 = AtomicU32::new(0);
fn incrementar_seguro() {
CONTADOR_SEGURO.fetch_add(1, Ordering::Relaxed);
}
Ferramentas para detectar bugs em código unsafe
Miri — interpretador de MIR
Miri é a ferramenta mais poderosa para detectar UB em código unsafe. Ele executa seu programa em um ambiente interpretado que detecta acessos inválidos, use-after-free, data races e outros problemas:
# Instalar Miri
rustup component add miri
# Executar testes com Miri
cargo miri test
# Executar o programa com Miri
cargo miri run
#[cfg(test)]
mod testes {
#[test]
fn teste_ring_buffer() {
let mut buf: super::RingBuffer<i32, 4> = super::RingBuffer::novo();
assert!(buf.push(1));
assert!(buf.push(2));
assert!(buf.push(3));
assert!(buf.push(4));
assert!(!buf.push(5)); // cheio
assert_eq!(buf.pop(), Some(1));
assert_eq!(buf.pop(), Some(2));
assert!(buf.push(5)); // agora há espaço
assert_eq!(buf.pop(), Some(3));
}
}
// Execute: cargo miri test
// Miri detectaria qualquer acesso inválido ao MaybeUninit
AddressSanitizer e Valgrind
# AddressSanitizer — detecta buffer overflows e use-after-free
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnu
# ThreadSanitizer — detecta data races
RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test --target x86_64-unknown-linux-gnu
Checklist antes de escrever código unsafe
Antes de adicionar qualquer bloco unsafe, percorra esta lista:
- Existe uma alternativa segura? Verifique se a stdlib ou crates como
bytemuck,zerocopy, oumemmap2já resolvem o problema com segurança. - O código unsafe está no menor escopo possível? Prefira blocos unsafe pequenos e precisos a funções inteiras marcadas como unsafe.
- Você documentou as invariantes de safety? Escreva a seção
# Safetyantes de implementar, não depois. - Você tem testes que cobrem o código unsafe? Execute com Miri.
- A API exposta é completamente segura? Quem usa sua abstração não deveria precisar de unsafe.
- O Drop está correto? Recursos alocados manualmente precisam ser liberados mesmo em pânico.
unsafe na prática: o que a stdlib revela
Vale examinar como a stdlib usa unsafe para entender o padrão em escala real. Vec::push, por exemplo, usa internamente unsafe para escrever no buffer alocado, mas toda a lógica de bounds checking e realocação está no código seguro ao redor. O total de linhas unsafe na stdlib Rust é uma fração pequena do total — o resto é código seguro construído sobre essa fundação.
Esta é a filosofia: unsafe é o custo de construir primitivas. O objetivo é manter esse custo localizado, auditável e documentado — para que toda a camada de código de aplicação acima possa operar em segurança total.
Fontes e leituras recomendadas
- "The Rustonomicon" — o guia oficial de unsafe Rust, essencial para qualquer uso sério — https://doc.rust-lang.org/nomicon/
- Referência do Rust: Behavior considered undefined — lista completa de UB — https://doc.rust-lang.org/reference/behavior-considered-undefined.html
- Miri — interpretador para detectar UB — https://github.com/rust-lang/miri
- "Learn Rust With Entirely Too Many Linked Lists" — tutorial prático de unsafe com estruturas de dados — https://rust-unofficial.github.io/too-many-lists/
- crate bytemuck — transmutes seguras para tipos Pod — https://docs.rs/bytemuck
- crate zerocopy — zero-copy parsing sem unsafe manual — https://docs.rs/zerocopy
- "Unsafe Rust: How and when (not) to use it" — Jon Gjengset (YouTube) — referência em vídeo sobre o tema