Miri em Rust: Detecte Undefined Behavior | Rust Brasil

Aprenda a usar Miri em projetos Rust para encontrar use-after-free, ponteiros inválidos, violações de aliasing e outros bugs em código unsafe e FFI na CI.

Miri é a ferramenta mais prática do ecossistema Rust para executar testes e encontrar várias formas de undefined behavior antes que elas virem falhas raras em produção. Depois de instalar a toolchain nightly, rode cargo +nightly miri test. O interpretador pode apontar acesso a memória já liberada, ponteiro desalinhado, valor inválido, violações de aliasing e algumas data races — problemas que um teste comum pode executar aparentemente “sem erro”.

O Miri é especialmente valioso quando seu projeto contém blocos unsafe, ponteiros raw, MaybeUninit, estruturas de dados próprias, concorrência de baixo nível ou uma camada de FFI entre Rust e C. Ele não transforma código inseguro em seguro automaticamente e não substitui revisão. Seu papel é tornar observáveis violações que, em uma execução nativa, podem depender do compilador, da otimização, do alocador ou de uma combinação difícil de reproduzir.

Este guia mostra como instalar e usar Miri em Rust, interpretar diagnósticos, organizar testes para código unsafe e adicionar uma verificação útil à CI sem deixar o pipeline impraticavelmente lento.

O que é undefined behavior em Rust

Comportamento indefinido, ou undefined behavior (UB), significa que o programa violou regras fundamentais assumidas pelo compilador. A partir desse ponto, não existe garantia sobre o resultado. O programa pode encerrar, imprimir um valor aparentemente correto, corromper dados ou mudar de comportamento quando você ativa otimizações.

Rust seguro foi desenhado para impedir essas violações por meio do sistema de tipos, ownership e borrowing. Já um bloco unsafe permite realizar operações que o compilador não consegue validar sozinho, como:

  • dereferenciar ponteiros raw;
  • chamar funções unsafe;
  • acessar ou modificar estado global mutável;
  • implementar traits unsafe;
  • trabalhar com unions;
  • cruzar uma fronteira de FFI.

A palavra-chave unsafe não desativa as regras da linguagem. Ela apenas transfere para quem escreveu a abstração a responsabilidade de preservar essas regras. O guia de unsafe Rust explica essa diferença em profundidade.

Entre as classes que podem produzir UB estão:

  • use-after-free: ler ou escrever uma região depois que ela foi liberada;
  • double free: liberar a mesma alocação duas vezes;
  • acesso fora dos limites: formar ou usar um acesso que não pertence ao objeto válido;
  • desalinhamento: criar uma referência para um endereço incompatível com o alinhamento do tipo;
  • valor inválido: materializar, por exemplo, um bool com representação que não é válida para bool;
  • violação de aliasing: manter acessos incompatíveis à mesma memória;
  • data race: acessos concorrentes sem sincronização adequada, com pelo menos uma escrita.

Nem todo bug é UB. Um cálculo errado, um deadlock ou um vazamento de memória pode ser grave sem violar o modelo de memória. Miri tem um escopo específico: interpretar a execução e verificar operações que ele consegue modelar.

Como o Miri funciona

O compilador Rust transforma o código em uma representação intermediária chamada MIR (Mid-level Intermediate Representation). Miri interpreta essa representação em vez de gerar e executar diretamente um binário nativo.

Essa camada permite acompanhar propriedades que normalmente desaparecem durante a compilação: origem e validade de ponteiros, estado de alocações, alinhamento, inicialização de bytes e permissões associadas a referências. Quando uma operação viola o modelo monitorado, a execução é interrompida com um diagnóstico.

A interpretação traz duas consequências importantes:

  1. Miri é bem mais lento que uma execução nativa. Uma suíte que leva segundos com cargo test pode levar minutos.
  2. Miri vê somente os caminhos executados. Se um teste nunca entra no ramo defeituoso, a ferramenta não analisa magicamente esse ramo.

Por isso, a melhor combinação é uma suíte pequena, determinística e capaz de explorar os invariantes da abstração. Property-based testing e fuzzing podem gerar entradas variadas; Miri verifica se as execuções selecionadas preservam as regras de memória.

Como instalar o Miri

Miri acompanha o ecossistema nightly porque depende de detalhes internos do compilador. Com rustup instalado, use:

rustup toolchain install nightly --component miri
cargo +nightly miri --version

Na raiz do projeto, prepare o ambiente da ferramenta:

cargo +nightly miri setup

Depois execute os testes:

cargo +nightly miri test

O projeto não precisa adotar recursos instáveis nem trocar sua toolchain principal para nightly. É comum compilar e publicar com stable, mas usar nightly apenas no comando do Miri.

Se o repositório fixa uma versão em rust-toolchain.toml, o +nightly continua escolhendo explicitamente a toolchain da ferramenta para aquele comando. Em equipes e na CI, considere fixar uma nightly datada para reduzir mudanças inesperadas:

rustup toolchain install nightly-2026-07-20 --component miri
cargo +nightly-2026-07-20 miri test

Uma nightly datada precisa realmente conter o componente para a plataforma usada. Atualize-a de forma periódica, com revisão, para receber correções do compilador e do próprio Miri.

Primeiro exemplo: detectando use-after-free

Considere um exemplo propositalmente incorreto:

fn main() {
    let ponteiro = Box::into_raw(Box::new(42_u32));

    unsafe {
        drop(Box::from_raw(ponteiro));
        println!("{}", *ponteiro);
    }
}

A conversão com Box::into_raw transfere a responsabilidade de liberar a alocação para o código manual. Box::from_raw reconstrói o proprietário, e o drop libera a memória. A dereferência seguinte tenta ler uma alocação que já não está viva.

Uma execução nativa pode imprimir 42, mostrar lixo ou falhar. O fato de “funcionar na minha máquina” não torna a operação válida. Execute:

cargo +nightly miri run

Miri interrompe o programa e aponta um acesso inválido relacionado à alocação liberada. A redação exata do diagnóstico pode mudar entre versões, mas o relatório normalmente inclui a operação, a pilha e informações sobre a alocação.

A correção é preservar um único proprietário e não usar o ponteiro depois de reconstruir e descartar o Box:

fn main() {
    let valor = Box::new(42_u32);
    println!("{}", valor);
}

Código raw só se justifica quando a abstração precisa dele. Se tipos seguros resolvem o problema, eles reduzem o número de invariantes que a equipe precisa provar manualmente.

Testando uma abstração unsafe de forma útil

Em código real, não deixe todo o teste dentro de um grande bloco unsafe. Crie uma API segura, concentre o trecho inseguro em uma implementação pequena e teste o contrato público.

Exemplo simplificado de uma função que lê uma posição por aritmética de ponteiros:

pub fn obter(slice: &[u32], indice: usize) -> Option<u32> {
    if indice >= slice.len() {
        return None;
    }

    unsafe {
        Some(*slice.as_ptr().add(indice))
    }
}

#[cfg(test)]
mod tests {
    use super::obter;

    #[test]
    fn retorna_elementos_validos() {
        let dados = [10, 20, 30];
        assert_eq!(obter(&dados, 0), Some(10));
        assert_eq!(obter(&dados, 2), Some(30));
    }

    #[test]
    fn rejeita_indice_fora_do_slice() {
        let dados = [10, 20, 30];
        assert_eq!(obter(&dados, 3), None);
        assert_eq!(obter(&dados, usize::MAX), None);
    }
}

A função poderia usar slice.get(indice).copied() e eliminar o unsafe; o exemplo serve para mostrar a organização. O teste exercita tanto posições válidas quanto limites. Miri, ao executar esses caminhos, verifica a dereferência realizada pela implementação.

Para cada bloco inseguro, escreva os invariantes em comentários próximos ao código:

  • por que o ponteiro está alinhado;
  • por que aponta para uma alocação viva;
  • quantos elementos podem ser acessados;
  • quem possui a memória;
  • quais aliases podem existir;
  • qual sincronização protege acessos concorrentes;
  • quem executa a destruição e exatamente uma vez.

Miri ajuda a testar esses argumentos, mas o comentário permite que uma pessoa revisora descubra se um caso importante nem chegou à suíte.

Miri e violações de aliasing

Referências Rust carregam garantias mais fortes do que “um endereço que aponta para memória”. Uma referência mutável &mut T pressupõe acesso exclusivo durante seu período de validade; uma referência compartilhada &T não pode observar uma escrita incompatível.

Código com ponteiros raw pode quebrar essas garantias mesmo quando todos os endereços continuam dentro da mesma alocação. Esse é um dos motivos pelos quais bugs de aliasing são difíceis de encontrar em testes normais: os bytes existem e o processador aceita o acesso, mas o compilador pode otimizar com base em garantias que o programa violou.

Miri mantém um modelo de permissões e proveniência para identificar várias dessas situações. Como esse modelo e suas opções evoluem, trate o diagnóstico como um sinal para revisar a validade da abstração, não como um convite para adicionar uma flag que apenas silencia o erro.

Ao manipular ponteiros:

  • derive-os de alocações válidas;
  • evite convertê-los em inteiros e de volta sem necessidade;
  • reduza o tempo de vida de referências criadas a partir deles;
  • não crie &mut se acesso exclusivo não pode ser garantido;
  • prefira NonNull<T> quando ele expressa melhor o contrato interno;
  • exponha uma API segura somente depois de documentar todos os invariantes.

Valores não inicializados e MaybeUninit

MaybeUninit<T> é necessário em estruturas de baixo nível, buffers, FFI e inicialização parcial. O risco aparece quando o código afirma que um valor está inicializado antes de todos os bytes e invariantes de T estarem prontos.

Um padrão correto para inicializar um array exige acompanhar quantos elementos foram escritos e destruir somente essa parte se uma operação falhar. Em vez de criar uma implementação manual por reflexo, procure primeiro APIs estáveis da biblioteca padrão ou crates bem revisadas.

Miri consegue detectar várias leituras de memória não inicializada e materializações de valores inválidos. Ainda assim, ele precisa executar o caminho. Teste:

  • tamanho zero;
  • capacidade mínima;
  • falha no primeiro elemento;
  • falha depois de uma inicialização parcial;
  • destruição normal;
  • panic durante a construção, quando aplicável;
  • tipos com Drop que registram quantas destruições ocorreram.

Esse tipo de matriz transforma “rodei Miri” em uma verificação relevante do contrato.

Concorrência: Miri, Loom e ThreadSanitizer

Miri pode detectar algumas data races durante a execução interpretada. Porém, concorrência possui muitas ordens possíveis, e uma única execução não explora todas elas.

Use ferramentas complementares conforme o problema:

FerramentaPergunta principal
cargo testO comportamento esperado passou neste ambiente?
MiriEsta execução violou regras de memória ou validade?
LoomA lógica de sincronização funciona sob diferentes interleavings modelados?
ThreadSanitizerUma execução nativa instrumentada observou data race?
Fuzzing/ProptestEntradas variadas quebram propriedades ou invariantes?

O guia de concorrência em Rust ajuda a escolher primitives seguras antes de construir sincronização manual. Para aplicações async, Tokio, channels e estruturas consolidadas costumam ser escolhas melhores do que uma abstração própria com atomics.

Se você realmente implementa uma fila lock-free ou uma primitive concorrente, combine revisão especializada, testes com Loom, Miri onde suportado e ferramentas nativas. Nenhuma execução isolada prova todas as ordens possíveis.

FFI: o que fazer quando Miri não executa a biblioteca externa

Uma chamada FFI atravessa o ambiente que Miri interpreta. Bibliotecas C arbitrárias, drivers e várias syscalls não podem ser tratados como se fossem código Rust comum dentro da ferramenta.

A arquitetura mais testável separa três camadas:

  1. declarações externas, geradas ou escritas com assinaturas corretas;
  2. adaptador unsafe pequeno, que converte ponteiros, tamanhos, erros e ownership;
  3. API Rust segura, onde vive a maior parte da lógica.

Teste com Miri a lógica pura e o adaptador que possa ser exercitado sem chamar a biblioteca nativa. Para a integração real, use testes nativos, sanitizers do compilador C/C++, fixtures e ambientes compatíveis com a dependência externa.

Quando necessário, cfg(miri) permite escolher uma implementação alternativa durante a interpretação:

#[cfg(miri)]
fn ler_dispositivo() -> Vec<u8> {
    vec![1, 2, 3] // implementação controlada para o teste
}

#[cfg(not(miri))]
fn ler_dispositivo() -> Vec<u8> {
    // chama o adaptador FFI real
    todo!()
}

Não use o mock para “provar” que a FFI está correta. Ele serve para manter a lógica superior testável. O contrato da fronteira — tamanho de buffer, nulidade, lifetime, thread safety e função responsável por liberar memória — continua exigindo validação nativa e revisão.

Isolamento, variáveis e MIRIFLAGS

Miri restringe várias interações com o sistema para manter a execução controlada. Projetos que acessam rede, relógio, variáveis, sistema de arquivos ou aleatoriedade podem precisar de adaptação.

Existe a opção frequentemente vista em exemplos:

MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test

Ela reduz o isolamento e permite mais interações com o host, mas deve ser usada com cuidado. Não transforme essa flag na primeira reação a qualquer falha. Antes, pergunte se o teste pode receber seus dados por parâmetro, usar um diretório temporário, substituir o relógio por uma interface ou separar a lógica pura do efeito externo.

Flags -Z são instáveis e podem mudar. Confira as opções disponíveis na versão instalada:

cargo +nightly miri test --help
MIRIFLAGS="-Zmiri-help" cargo +nightly miri test

Uma suíte determinística e pouco dependente do sistema tende a ser mais rápida, confiável e útil também fora do Miri.

Como ler um diagnóstico do Miri

Quando a ferramenta falhar, concentre-se na primeira operação inválida, não em todas as mensagens posteriores. Um processo prático:

  1. identifique se o erro é acesso inválido, alinhamento, aliasing, valor não inicializado ou outra classe;
  2. encontre a primeira linha do seu projeto na pilha;
  3. descubra qual invariante deveria tornar aquela operação válida;
  4. reduza o teste até obter uma reprodução pequena;
  5. corrija a abstração, não somente o caso de entrada;
  6. mantenha o teste como regressão;
  7. rode testes normais, Miri e, quando relevante, sanitizers novamente.

Se o diagnóstico aparece dentro de uma dependência, atualize a crate e procure issues no projeto responsável. Também pode existir incompatibilidade entre aquela dependência e a nightly usada. Reduza a reprodução antes de concluir que é um falso positivo.

Miri na CI

Não coloque toda uma suíte de integração sob Miri. Crie um job dedicado para as crates e testes que realmente se beneficiam dele.

Exemplo para GitHub Actions ou executor compatível:

name: Miri

on:
  push:
  pull_request:

jobs:
  miri:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Instalar nightly com Miri
        run: |
          rustup toolchain install nightly --component miri
          cargo +nightly miri setup

      - name: Verificar biblioteca com Miri
        run: cargo +nightly miri test --lib

Para um workspace, selecione o package:

cargo +nightly miri test -p minha-crate-unsafe --lib

Boas práticas para o job:

  • execute cargo test normalmente em outro job;
  • selecione --lib, package ou nome de teste em vez de interpretar tudo;
  • evite rede e serviços externos;
  • fixe uma nightly quando reprodutibilidade for prioridade;
  • atualize a versão fixa em uma cadência definida;
  • mantenha o job obrigatório para mudanças na abstração unsafe;
  • registre o comando no README ou guia de contribuição.

Se o pipeline já usa cargo-nextest, mantenha os papéis separados: nextest acelera e organiza a execução nativa; Miri interpreta testes selecionados para verificar regras de memória.

Miri substitui Clippy e sanitizers?

Não. As ferramentas observam problemas diferentes.

O Clippy analisa padrões no código e sugere melhorias sem precisar executar todas as entradas. Ele encontra APIs mal utilizadas, construções confusas e potenciais problemas de performance ou correção, mas não acompanha cada acesso raw como Miri.

Sanitizers instrumentam uma compilação nativa. Eles são úteis para testar integração com C/C++, threads reais e cargas mais próximas da produção, embora também dependam dos caminhos executados e da plataforma.

Miri oferece visibilidade rica sobre a semântica de memória do Rust, mas interpreta o programa lentamente e possui limitações em operações do sistema e FFI.

Uma pipeline madura pode conter:

cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo +nightly miri test -p crate_de_baixo_nivel --lib

Projetos que publicam bibliotecas também devem manter documentação, exemplos e uma política de versões. O artigo de release engineering em Rust mostra como organizar essas verificações sem transformar a CI em uma lista de comandos sem responsabilidade definida.

Quando vale a pena usar Miri

Priorize Miri quando houver:

  • blocos unsafe escritos pelo projeto;
  • manipulação de ponteiros raw;
  • alocador, coleção ou buffer próprio;
  • MaybeUninit, unions ou casts de representação;
  • implementação manual de Send ou Sync;
  • abstração segura construída sobre FFI;
  • código concorrente de baixo nível;
  • correção de uma vulnerabilidade ou falha de memória;
  • uma crate pública cuja API segura depende de invariantes internos.

Em uma aplicação Axum comum sem unsafe próprio, Miri pode encontrar problemas transitivos, mas talvez não seja o investimento inicial mais eficiente. Comece com tipos seguros, testes, Clippy, auditoria de dependências e observabilidade. A segurança da supply chain Rust cobre riscos que Miri não foi criado para resolver.

Miri como habilidade de carreira

Conhecer o comando é menos importante do que saber explicar o raciocínio. Em entrevistas e portfólio, uma demonstração forte inclui:

  • uma abstração pequena que exige unsafe por motivo claro;
  • invariantes documentados no código;
  • teste que reproduz o problema;
  • diagnóstico do Miri antes da correção;
  • mudança que elimina ou restringe o unsafe;
  • teste de regressão na CI;
  • explicação das limitações da ferramenta.

Esse material é relevante para carreiras em programação de sistemas, segurança, runtimes, bancos de dados, embarcados e infraestrutura. Consulte as vagas Rust para observar como empresas descrevem experiência com performance, concorrência, sistemas e confiabilidade.

Quem compara linguagens de sistemas pode consultar também o guia de testes em Zig. Zig e Rust tomam decisões diferentes sobre segurança e ferramentas, mas em ambos os casos testes de baixo nível precisam tornar contratos de memória explícitos.

Checklist para adotar Miri

Antes de marcar o job como concluído, confirme:

  • a toolchain nightly e o componente Miri estão instalados;
  • cargo +nightly miri setup funciona no ambiente;
  • a suíte selecionada termina em tempo aceitável;
  • os testes exercitam limites, falhas e destruição parcial;
  • cada bloco unsafe possui invariantes documentados;
  • código com FFI foi separado da lógica Rust pura;
  • mocks sob cfg(miri) não estão sendo confundidos com teste real da biblioteca nativa;
  • flags de isolamento têm justificativa;
  • testes nativos e Clippy continuam no pipeline;
  • a nightly fixa, se usada, possui rotina de atualização;
  • um erro encontrado gera teste de regressão.

Conclusão

Miri torna verificáveis muitos contratos que existem por trás do unsafe em Rust. Ele não garante que todo caminho foi explorado, não executa qualquer dependência externa e não substitui revisão, testes ou sanitizers. Ainda assim, é uma das ferramentas de maior retorno para quem mantém código de baixo nível.

Comece pequeno: instale o componente, rode cargo +nightly miri test --lib e selecione a crate que concentra ponteiros, FFI ou inicialização manual. Quando surgir um erro, reduza a reprodução e descubra qual invariante foi quebrado. Depois mantenha o caso na CI.

Para aprofundar a prática, revise unsafe Rust, aprenda a criar entradas com Proptest e fuzzing e organize a execução diária com o guia de testes em Rust.

Perguntas frequentes

O que é Miri em Rust?

Miri é um interpretador da MIR, a representação intermediária do compilador Rust. Ele executa testes e programas enquanto monitora propriedades de memória para detectar várias classes de comportamento indefinido.

Como instalar o Miri?

Execute rustup toolchain install nightly --component miri, depois cargo +nightly miri setup. Na raiz do projeto, rode cargo +nightly miri test ou selecione uma crate e uma suíte menores.

Miri substitui testes, Clippy ou sanitizers?

Não. Miri verifica violações durante uma execução interpretada. Testes validam comportamento, Clippy analisa padrões e sanitizers observam execuções nativas instrumentadas. Use as ferramentas de forma complementar.

Miri consegue testar código com FFI?

Ele não executa bibliotecas nativas arbitrárias como um programa comum. Separe a lógica Rust da fronteira FFI, use uma implementação controlada sob cfg(miri) quando apropriado e teste a integração real de forma nativa.

Devo rodar Miri na CI?

Sim, quando o projeto mantém unsafe, ponteiros raw, FFI ou estruturas concorrentes de baixo nível. Selecione testes relevantes e mantenha o job separado, pois a interpretação é bem mais lenta do que cargo test.