Unsafe Rust
Esta aula explora o conceito de Rust inseguro (unsafe), detalhando o que ele permite, como trabalhar com ponteiros crus, quando seu uso é necessário e as práticas para garantir segurança mesmo em código unsafe. Inclui exemplos práticos, exercícios com respostas e referências para aprofundamento.
Bem-vindo à aula sobre Rust inseguro (unsafe). Em Rust, a segurança de memória é uma das principais garantias da linguagem, mas há situações em que precisamos operar em um nível mais baixo, interagir com código externo (FFI) ou implementar abstrações que o compilador não consegue verificar. Para isso, Rust oferece a palavra-chave unsafe, que permite ao programador assumir responsabilidades adicionais em troca de capacidades que seriam proibidas em código seguro.
Nesta aula, vamos desmistificar o unsafe, entender seus usos legítimos e aprender como minimizar os riscos ao utilizá-lo. Você verá que, mesmo com unsafe, é possível manter um alto nível de segurança se seguirmos boas práticas e encapsularmos o código perigoso em interfaces seguras.
O que unsafe permite
Em Rust, o código seguro não pode executar certas operações que podem levar a comportamento indefinido, como acessar memória inválida ou criar referências que violam as regras de aliasing. A palavra-chave unsafe desbloqueia cinco capacidades principais:
- Desreferenciar ponteiros crus (
*const Te*mut T); - Chamar funções ou métodos que sejam marcados como
unsafe; - Acessar ou modificar variáveis estáticas mutáveis;
- Implementar traits inseguros (marcados com
unsafe trait); - Acessar campos de uniões (
union).
Essas capacidades são necessárias para tarefas de baixo nível, como escrever bindings para bibliotecas em C, criar estruturas de dados avançadas (como listas ligadas) ou otimizar código crítico. No entanto, elas devem ser usadas com extrema cautela, pois o compilador não verifica a segurança nessas operações.
Exemplo de bloco unsafe:
fn main() {
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 é: {}", *r1);
*r2 = 10;
println!("r2 é: {}", *r2);
}
}
Observe que, mesmo fora do bloco unsafe, criar ponteiros crus é permitido, mas desreferenciá-los exige o bloco unsafe.
Ponteiros crus
Ponteiros crus são semelhantes aos ponteiros em C: eles não têm as garantias de segurança que referências normais têm. Existem dois tipos: *const T (ponteiro constante) e *mut T (ponteiro mutável). Eles podem ser nulos, não garantem que a memória seja válida e não possuem contagem de referências ou regras de empréstimo.
Para criar um ponteiro cru, você pode converter uma referência usando as ou usar std::ptr::null() para um ponteiro nulo. Desreferenciar um ponteiro cru é uma operação unsafe porque o compilador não pode garantir que o ponteiro aponte para memória válida.
fn main() {
let valor = 42;
let ptr_const = &valor as *const i32;
let mut valor_mut = 10;
let ptr_mut = &mut valor_mut as *mut i32;
unsafe {
println!("Valor via ponteiro const: {}", *ptr_const);
*ptr_mut = 20;
println!("Novo valor via ponteiro mut: {}", *ptr_mut);
}
}
É importante notar que, ao contrário das referências, ponteiros crus não têm lifetime. Eles podem ser copiados e movidos livremente, mas o programador deve garantir que a memória apontada seja válida durante todo o uso.
Quando é necessário
O uso de unsafe é necessário em várias situações legítimas:
- FFI (Foreign Function Interface): Para chamar funções de bibliotecas em C ou outras linguagens, pois essas funções são consideradas
unsafe. - Implementação de estruturas de dados com comportamento não verificável pelo compilador: Por exemplo, listas ligadas, onde o compilador não pode provar a ausência de ciclos ou o correto gerenciamento de memória.
- Otimizações de baixo nível: Em alguns casos, usar ponteiros crus pode ser mais eficiente que referências, mas isso deve ser feito com cuidado.
- Interação com hardware: Em sistemas embarcados, é comum acessar registradores de memória mapeada, o que exige ponteiros crus.
Um exemplo comum de FFI:
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Valor absoluto de -3: {}", abs(-3));
}
}
Nesse caso, a função abs é declarada como extern e, portanto, é considerada unsafe para chamada.
Garantindo segurança
Embora unsafe permita operações perigosas, é possível minimizar riscos seguindo boas práticas:
- Encapsulamento: Isole todo o código
unsafeem funções ou módulos que exponham uma interface segura. Assim, os usuários da API não precisam lidar comunsafediretamente. - Documentação: Documente as invariantes que devem ser mantidas para que o código unsafe seja correto. Por exemplo, se um ponteiro deve ser não nulo ou apontar para memória alocada.
- Testes: Escreva testes extensivos, incluindo testes de propriedades, para verificar o comportamento do código unsafe.
- Revisão cuidadosa: Revise o código unsafe com atenção redobrada, procurando possíveis violações de memória.
- Uso de ferramentas: Utilize ferramentas como Miri (intérprete de Rust que detecta comportamento indefinido) e Sanitizers para validar o código.
Exemplo de encapsulamento: uma função segura que usa ponteiros crus internamente.
fn calcular_soma(ptr: *const i32, tamanho: usize) -> i32 {
let mut soma = 0;
for i in 0..tamanho {
unsafe {
soma += *ptr.add(i);
}
}
soma
}
fn main() {
let vetor = [1, 2, 3, 4];
let ptr = vetor.as_ptr();
println!("Soma: {}", calcular_soma(ptr, vetor.len()));
}
Aqui, a função calcular_soma é segura, mas internamente usa unsafe para desreferenciar ponteiros. O chamador não precisa se preocupar com unsafe.
Boas práticas e observações finais
O uso de unsafe deve ser a exceção, não a regra. Sempre que possível, prefira código seguro. Ao usar unsafe, mantenha o bloco o menor possível e evite espalhar operações perigosas pelo código. Lembre-se de que a segurança é uma responsabilidade do programador quando se usa unsafe; um erro pode causar comportamento indefinido, como corrupção de memória ou crashes.
Além disso, é fundamental entender as regras de aliasing e a validade das referências ao trabalhar com ponteiros crus. A documentação oficial do Rust tem uma seção detalhada sobre isso.
Referências
- The Rust Programming Language - Unsafe Rust
- Documentação do std::primitive.pointer
- The Rustonomicon
- Unsafe Code Guidelines Working Group
- Cargo Reference - Unsafe Code
- Rustc Lints - Unsafe Code
Exercícios
Explique o que é um ponteiro cru e quais são os dois tipos existentes em Rust. Dê um exemplo de criação de cada tipo.
✓ Resposta: Ponteiros crus são ponteiros sem as garantias de segurança das referências. Existem dois tipos:*const T(imutável) e*mut T(mutável). Exemplo:let x = 10; let ptr_const = &x as *const i32; let mut y = 20; let ptr_mut = &mut y as *mut i32;Escreva um bloco
unsafeque desreferencie um ponteiro cru para um inteiro e o imprima. Use um ponteiro criado a partir de uma referência.✓ Resposta:fn main() { let valor = 42; let ptr = &valor as *const i32; unsafe { println!("Valor: {}", *ptr); } }Cite duas situações em que o uso de
unsafeé necessário e explique brevemente por quê.✓ Resposta: 1. FFI: chamar funções de C, pois o compilador não pode verificar a segurança dessas chamadas. 2. Implementar estruturas de dados como listas ligadas, onde o compilador não pode provar a ausência de ciclos ou a validade dos ponteiros.Escreva uma função segura que use
unsafeinternamente para somar os elementos de um vetor de inteiros usando ponteiros crus.✓ Resposta:fn soma_vetor(v: &[i32]) -> i32 { let ptr = v.as_ptr(); let mut soma = 0; for i in 0..v.len() { unsafe { soma += *ptr.add(i); } } soma }Quais são as principais boas práticas para garantir segurança ao usar
unsafe? Liste pelo menos três.✓ Resposta: 1. Encapsular o código unsafe em funções seguras. 2. Documentar as invariantes que devem ser mantidas. 3. Escrever testes extensivos. 4. Usar ferramentas como Miri e sanitizers. 5. Manter os blocos unsafe o menor possível.