Bugs de 11 anos no borrow checker do Rust: o que muda
Uma issue nova no repositório do Rust descreve bugs no borrow checker que estão no compilador há uns 11 anos, praticamente desde o Rust 1.0. Na prática, isso quer dizer que dá para escrever código safe, sem nenhum unsafe, que o compilador aceita e que mesmo assim lê memória já liberada. O borrow checker do Rust é justamente a peça que deveria barrar isso. A fonte é a issue #164250 no GitHub do rust-lang.
Antes que alguém poste "Rust acabou" no X, calma. A história é mais interessante que isso e diz muito sobre como a gente deveria pensar em garantias de linguagem. Inclusive quando o assunto é Bun.
O que é um bug de unsoundness no borrow checker do Rust?
O Rust te faz uma promessa simples: se o código compila e não tem unsafe, ele não tem use-after-free, data race nem ponteiro pendurado. Quem cumpre essa promessa é o borrow checker. Ele analisa lifetimes e empréstimos e rejeita o que for perigoso.
"Unsound" é o nome técnico para quando essa promessa quebra. O compilador aceita um programa que viola as próprias regras. Não é um bug de performance nem uma mensagem de erro confusa. É a garantia central da linguagem com um furo.
O time do Rust marca esses casos com a label I-unsound, e a lista não é curta. Alguns ficam abertos por anos. O exemplo mais famoso é a issue #25860, aberta em 2015 e ainda viva. Olha que bonito:
static UNIT: &'static &'static () = &&();
fn foo<'a, 'b, T>(_: &'a &'b (), v: &'b T) -> &'a T { v }
fn bad<'a, T>(x: &'a T) -> &'static T {
let f: fn(_, &'a T) -> &'static T = foo;
f(UNIT, x)
}
Nenhum unsafe aí. Mesmo assim, bad pega uma referência de vida curta e devolve como se ela vivesse para sempre. Use isso com um valor da stack e você tem um ponteiro para lixo, aprovado pelo compilador.
Por que esses bugs sobreviveram 11 anos?
Porque ninguém escreve esse código sem querer. Você precisa de uma combinação bem específica de lifetimes implícitos, coerção de função e variância para cair no buraco. Em código de produção normal, com struct, impl e um Vec aqui e ali, a chance de acertar isso por acidente é muito baixa.
Também tem o lado de engenharia. Corrigir alguns desses casos exige mudar como o compilador raciocina sobre tipos, e isso pode quebrar crates que compilam hoje. O Rust leva compatibilidade a sério. O trabalho no novo trait solver e no Polonius (a próxima geração do borrow checker) existe em parte para fechar essas brechas sem detonar o ecossistema.
Então os bugs não ficaram abertos por preguiça. Ficaram porque o conserto certo é caro e o risco prático é baixo. Dá para discordar dessa priorização, mas ela tem lógica.
Uma garantia de linguagem é um contrato, não um milagre. Contratos têm letras miúdas.
Isso quer dizer que Rust não é seguro?
Não. E aqui vai minha opinião sem rodeio: quem usar essa issue para dizer que Rust é tão inseguro quanto C está forçando a barra.
Compare as escalas. Em C ou C++, use-after-free aparece por descuido numa sexta-feira à tarde. Em Rust safe, para chegar nisso você precisa praticamente montar o bug de propósito, com uma construção que parece desafio de quebra-cabeça. Estudos do Google e da Microsoft já mostraram por anos que cerca de 70% das vulnerabilidades graves em código nativo vêm de erros de memória. Mesmo com furos raros, o Rust corta essa categoria quase toda.
O que a issue muda é o discurso. "Se compila, é seguro" sempre foi um "quase sempre". Agora a gente tem mais um exemplo concreto de onde fica o "quase".
Onde isso vira problema de verdade:
- Código malicioso de propósito. Uma crate de supply chain pode usar um furo desses para esconder comportamento perigoso sem nenhum
unsafeaparecendo no grep. - Sandboxes que confiam no compilador. Plataformas que rodam Rust de terceiros achando que "sem
unsafe= isolado" estão apostando errado. - Auditoria preguiçosa. Se seu processo de review é "procura
unsafee aprova o resto", isso nunca foi suficiente.
E onde o Bun entra nessa história?
O Bun é escrito em Zig, não em Rust. E toda vez que o Bun solta uma versão com um crash ou um segfault, aparece alguém nos comentários dizendo "deveria ter sido feito em Rust".
Eu entendo o argumento. Zig não tem borrow checker. Ele te dá ferramentas melhores que C (allocators explícitos, modo debug com checagens, defer), mas a responsabilidade pela memória é sua. O histórico de issues do Bun tem a sua cota de crashes de memória, como qualquer runtime grande escrito em linguagem sem gerenciamento automático.
Essa issue do Rust ajuda a colocar a discussão no lugar certo. A pergunta não é "qual linguagem é perfeita", porque nenhuma é. A pergunta é onde ficam os buracos e quão fácil é cair neles.
- Em Zig, o buraco está em todo lugar onde você gerencia memória. Você cai nele com código normal.
- Em Rust safe, o buraco existe, mas fica em casos patológicos de tipos. Você quase precisa procurar por ele.
Como usuário do Bun, você roda TypeScript em cima de JavaScriptCore. Nada disso muda seu dia. Seu Bun.serve não vai quebrar por causa de lifetime de Rust nem de allocator de Zig. Mas se você está escolhendo linguagem para escrever um runtime, um parser ou uma extensão nativa, essa diferença pesa.
O que muda no seu fluxo de trabalho
Se você escreve Rust, na prática quase nada muda amanhã. Mas vale ajustar algumas coisas:
- Acompanhe a label
I-unsound. Não precisa ler todo dia. Dá uma olhada quando atualizar a toolchain. - Mantenha o compilador atualizado. Correções de soundness chegam em releases normais, às vezes com um aviso de compatibilidade antes.
- Não use "sem
unsafe" como prova de segurança em dependência de terceiros. Usecargo-audit,cargo-vetou pelo menos olhe quem mantém a crate. - Rode Miri nos testes de código crítico. Ele interpreta o programa e pega comportamento indefinido em tempo de execução, inclusive alguns casos que o compilador deixou passar.
rustup +nightly component add miri
cargo +nightly miri test
Se você escreve Zig ou mexe em addon nativo para o Bun, a lição é parecida, só que com o sinal trocado. Rode os testes em modo Debug ou ReleaseSafe, use o GeneralPurposeAllocator para detectar vazamento e double free, e trate cada [*]u8 com a desconfiança que ele merece.
E se você só escreve TypeScript, o recado é sobre supply chain. Pacote npm com binário nativo (e hoje tem muito, de bundler a driver de banco) roda código que você não auditou, em linguagem que você talvez nem leia. A segurança da linguagem por baixo ajuda, mas não substitui saber o que você instala.
Vale a pena trocar de linguagem por causa disso?
Não. Se você já usa Rust, continue. Se você já usa Bun e gosta, continue também. Essa issue não é motivo de pânico nem de migração.
Para mim o que importa aqui é a transparência. O time do Rust documenta em público os furos da própria garantia, com label, exemplo mínimo e discussão aberta. É assim que se trata uma promessa de segurança: você mostra onde ela falha em vez de fingir que não falha. Confesso que um bug de lifetime fazer aniversário de 11 anos tem sua graça. Já está quase na idade de fazer review de PR sozinho.
A minha conclusão é que garantia de compilador é uma camada, não um escudo. Ela reduz muito a superfície de erro, mas review, testes e cuidado com dependências continuam sendo seu trabalho. Se quiser ver como eu aplico isso em projeto real, com TypeScript, Bun e um pouco de paranoia saudável, dá uma olhada nos meus projetos.
Resumo para o LinkedIn
Existe código Rust safe, sem nenhum unsafe, que lê memória já liberada. E o compilador aprova há 11 anos. Uma issue nova no rust-lang expõe bugs no borrow checker que estão lá praticamente desde o Rust 1.0. Antes do "Rust acabou": pra cair nesse buraco você quase precisa montar o bug de propósito. Em C, ele aparece numa sexta à tarde. O risco real está em supply chain e em review que só procura unsafe e aprova o resto. O que mais me chamou atenção foi a transparência. O time documenta em público onde a própria garantia falha. Garantia de compilador é uma camada, não um escudo. Review, testes e cuidado com dependências continuam sendo trabalho seu. Escrevi sobre isso, e sobre onde o Bun e o Zig entram nessa conversa, no blog. Link nos comentários. #Rust #Bun #SegurançaDeSoftware #SupplyChain #DesenvolvimentoDeSoftware