Pular para o conteúdo
blog.kristyan.dev
rust programação segurança open-source opinião

Rust: o culto da ferrugem

Rust resolve problemas reais de segurança de memória, mas transformar a linguagem em religião técnica é só trocar uma classe de bug por uma classe nova de ilusão.


Rust é uma ótima linguagem. O problema é que parte da comunidade fala dela como se fosse uma intervenção divina no desenvolvimento de software.

Você menciona C, alguém responde Rust. Você menciona C++, alguém responde Rust. Você menciona Go, GC, kernel, CLI, banco de dados, proxy reverso, terminal, torradeira, air fryer ou sistema de votação de condomínio, alguém aparece com a mesma liturgia: reescreve em Rust.

E aí mora o problema.

Não porque Rust seja ruim. Pelo contrário. Rust é uma das linguagens mais interessantes que surgiram nas últimas décadas para programação de sistemas. O modelo de ownership, borrowing e lifetimes resolve problemas reais. A ausência de garbage collector abre espaço para aplicações onde latência previsível importa. O ecossistema tem tooling excelente, uma comunidade ativa e adoção crescente em projetos grandes.

Mas linguagem nenhuma transforma código ruim em código bom por osmose. Trocar C, C++, Go ou qualquer outra coisa por Rust não elimina arquitetura ruim, domínio mal entendido, API mal desenhada, dependência demais, abstração demais, benchmark mentiroso, build quebrado, maintainer cansado ou time que não sabe por que está migrando.

Rust é ferramenta. O culto em volta dela é outra coisa.

O problema não é Rust, é a fé no compilador

O grande trunfo de Rust está no modelo de ownership. Cada valor tem um dono, referências precisam obedecer regras de empréstimo e o compilador tenta impedir acessos inválidos em tempo de compilação. A própria documentação oficial descreve ownership como o recurso mais característico da linguagem, justamente porque permite garantias de segurança de memória sem depender de garbage collector.

Isso é enorme.

Use-after-free, double free, data races e vários erros comuns de C e C++ deixam de ser algo que você precisa caçar no escuro depois que o processo explode em produção. O compilador vira uma espécie de colega chato, só que útil. Ele reclama cedo, reclama bastante e, em muitos casos, impede que uma classe inteira de bugs chegue ao binário.

Só que isso não é a mesma coisa que dizer que o programa está correto.

Rust não valida sua regra de negócio. Rust não sabe se seu cache invalida no momento certo. Rust não sabe se sua query está matando o banco. Rust não sabe se seu protocolo foi desenhado por alguém que confundiu simplicidade com ausência de requisitos. Rust não sabe se sua arquitetura virou um prato de macarrão com generics.

O compilador impede muita besteira. Ele não impede todas as besteiras. Infelizmente, programador é uma fonte renovável de criatividade destrutiva.

Borrowing e ownership não são magia, são trade-offs

Uma parte do discurso pró-Rust costuma vender ownership como se fosse apenas benefício. E sim, existe um benefício claro: a linguagem força você a pensar em quem possui um dado, quem pode modificar esse dado e por quanto tempo uma referência pode existir.

Isso é muito melhor do que sair passando ponteiro mutável para todo lado e rezar para ninguém pisar no mesmo terreno ao mesmo tempo.

Mas essa segurança não vem de graça. O custo muda de lugar.

Em linguagens com garbage collector, parte do custo vai para o runtime. Você aceita pausas, overhead de memória, comportamento do coletor e uma relação diferente com alocação. Em C e C++, parte do custo vai para disciplina manual, tooling, sanitizers, revisão e experiência. Em Rust, uma parte relevante do custo vai para o modelo mental.

Você precisa entender ownership. Precisa entender borrowing. Precisa entender lifetimes, mesmo quando eles são inferidos. Precisa entender quando usar Box, Rc, Arc, RefCell, Mutex, Cow, slices, iteradores, traits, generics e aquele tipo com assinatura que parece ter sido produzido por uma impressora com raiva.

E, claro, você precisa aceitar que às vezes o compilador está certo, mas sua vontade de terminar a task na sexta-feira às 17h também parecia muito convincente.

Essa é a parte que o culto ignora: Rust não remove complexidade. Ele explicita complexidade. Em muitos casos isso é ótimo. Em outros, isso transforma uma operação simples em uma negociação diplomática com o borrow checker.

Sem garbage collector não significa automaticamente rápido

Outra confusão comum: Rust não tem garbage collector, logo Rust é rápido, logo qualquer coisa em Rust é melhor.

Não é bem assim.

Ausência de GC remove uma fonte específica de custo e imprevisibilidade. Isso pode ser decisivo em software de baixa latência, sistemas embarcados, componentes de infraestrutura, engines, parsers, proxies, kernels, drivers e bibliotecas que precisam ser chamadas de outros ambientes sem carregar um runtime junto.

Mas performance não nasce apenas da linguagem. Performance nasce de dados, acesso à memória, algoritmo, contenção, I/O, alocação, layout, concorrência, cache locality, syscalls, protocolo, serialização, banco de dados e de uma longa lista de coisas que não cabem num post sem virar punição judicial.

Um algoritmo ruim em Rust continua sendo ruim. Uma estrutura de dados mal escolhida continua custando caro. Um serviço dominado por latência de rede não vira um foguete só porque a função que monta JSON agora compila para binário nativo. Um sistema que passa 80% do tempo esperando o banco não vai ganhar iluminação espiritual porque o backend foi ungido com cargo build --release.

Rust pode ajudar muito. Mas se o gargalo está em outro lugar, a linguagem vira cosmético caro.

Bonito no README, triste no Grafana.

unsafe é o lembrete de que o mundo real existe

Outro ponto importante: Rust tem unsafe.

Isso não invalida Rust. Muito pelo contrário. Sem unsafe, Rust não conseguiria fazer várias coisas que uma linguagem de sistemas precisa fazer: interagir com C, lidar com ponteiros crus, acessar hardware, implementar certas estruturas de baixo nível e construir abstrações que o compilador não consegue provar sozinho.

A documentação oficial é clara: unsafe dá poderes extras, mas também coloca responsabilidade extra no programador. Ele não desliga todo o borrow checker, mas permite operações que o compilador não consegue garantir como memory safe.

Na prática, isso significa que Rust seguro é uma coisa. Rust com blocos unsafe, FFI, dependências nativas e abstrações mal auditadas é outra.

Além disso, Rust também permite vazamento de memória em safe Rust. Ciclos com Rc e RefCell, por exemplo, podem manter objetos vivos para sempre se o grafo de referências for desenhado errado. Isso é memory safe, mas ainda é bug. O programa não acessa memória inválida, apenas carrega lixo vivo com muita segurança e educação.

Rust reduz brutalmente o espaço de erro em memória. Mas não torna o programador imune à física, ao domínio ou à própria arrogância.

Os casos reais são mais interessantes que o hype

O jeito mais honesto de defender Rust não é falar “reescreva tudo”. É olhar para como projetos grandes realmente adotaram Rust.

O Discord reescreveu o serviço Read States de Go para Rust porque tinha um problema concreto: picos de latência associados ao comportamento do garbage collector em um serviço de hot path. A troca funcionou bem, reduziu picos e melhorou métricas. Mas o detalhe mais importante do próprio post do Discord é outro: eles dizem que consideram Rust quando faz sentido. Não como mandamento universal.

A Cloudflare criou o Pingora em Rust para substituir uma arquitetura baseada em NGINX e Lua em um ponto crítico da infraestrutura. O resultado divulgado foi forte: menos CPU, menos memória e mais controle sobre o proxy. Mas de novo, não foi “pegamos qualquer serviço aleatório e colocamos Rust porque sim”. Era um componente específico, com requisitos específicos, em uma empresa que sabe medir carga real.

O Dropbox reescreveu o núcleo do mecanismo de sync no projeto Nucleus usando Rust. Só que o próprio relato deixa claro que a decisão não foi leviana. O problema era complexo, antigo, cheio de invariantes difíceis, e Rust ajudou tanto pela performance quanto pela capacidade de expressar correção e controlar estado.

O Android introduziu Rust em partes novas e sensíveis à segurança. O Chromium passou a suportar bibliotecas Rust de terceiros a partir de C++, mas com limitações bem explícitas: APIs estreitas, interoperabilidade controlada e nada de deixar Rust entrar de qualquer jeito no meio de um grafo gigante de ponteiros C++.

O kernel Linux também abriu espaço para Rust, mas de forma incremental. Não existe um plano sério de acordar amanhã e descobrir que o kernel virou um projeto Rust com uma pasta legacy_c escondida no canto. A adoção é gradual, discutida, politicamente difícil e tecnicamente cheia de implicações.

Esses casos mostram o contrário do culto.

Mostram que Rust funciona muito bem quando existe um motivo técnico forte, uma fronteira clara, métricas reais, equipe capaz e integração cuidadosa com o legado.

Reescrever tudo é a fantasia favorita de quem nunca manteve o “tudo”

A frase “reescreve em Rust” é sedutora porque reescrita é a forma mais socialmente aceitável de procrastinar manutenção.

Todo sistema legado parece horroroso por dentro. E quase sempre ele é mesmo. Tem camada esquecida, decisão histórica, comportamento não documentado, função que ninguém apaga porque uma integração misteriosa depende dela, bug que virou feature e feature que deveria ser crime.

A tentação é olhar para isso e pensar: agora faremos direito.

Só que “fazer direito” raramente sobrevive ao primeiro contato com o domínio real.

O código antigo carrega conhecimento acumulado. Nem todo conhecimento está bonito, testado ou documentado. Muitas vezes ele está codificado em ifs constrangedores, nomes ruins e comentários que começam com “não remover”. Uma reescrita completa joga fora bugs antigos, mas também joga fora correções antigas. Troca dívida técnica conhecida por dívida técnica nova, só que com camiseta bonita e mascote de caranguejo.

Rust ajuda quando o problema é segurança de memória, concorrência difícil, performance crítica ou necessidade de integração nativa sem runtime pesado. Rust atrapalha quando a equipe não domina a linguagem, quando o build vira um inferno, quando a fronteira com o legado é larga demais ou quando o projeto está apenas tentando resolver crise existencial com ferramenta nova.

A pergunta não deveria ser “dá para escrever em Rust?”. Quase sempre dá.

A pergunta deveria ser: “qual problema real isso resolve, qual custo adiciona e quem vai manter isso daqui a três anos?”.

Essa pergunta é menos divertida. Por isso aparece menos em thread.

WD-40 e o outro culto

Do outro lado da ferrugem também existe teatro.

As iniciativas e forks que se vendem como “sem Rust”, às vezes embalados na piada do WD-40, são uma reação previsível. A brincadeira funciona porque Rust significa ferrugem, WD-40 remove ferrugem e a internet nunca perdeu uma oportunidade de transformar discordância técnica em adesivo de notebook.

Mas existe um ponto legítimo no meio da piada.

Adicionar Rust a um projeto grande não é apenas adicionar uma linguagem. É adicionar uma toolchain, um gerenciador de pacotes, um modelo de build, uma política de dependências, uma história de bootstrap, compatibilidade com arquiteturas, empacotamento em distribuições, suporte para sistemas antigos e uma camada nova de conhecimento para maintainers.

Para quem mantém distribuição, sistema legado, plataforma exótica ou projeto com política conservadora de dependências, isso importa. Não é só birra.

Ao mesmo tempo, “remover Rust porque Rust” pode virar o mesmo tipo de culto invertido. Se um componente em Rust resolve uma classe real de bug, tem interface pequena, build reprodutível e manutenção ativa, arrancar aquilo só para preservar pureza ideológica também é engenharia ruim.

O culto pró-Rust e o culto anti-Rust têm mais em comum do que gostariam de admitir. Os dois começam pela identidade e só depois procuram argumento técnico.

Segurança de memória importa, mas segurança não termina nela

É impossível falar de Rust sem falar de segurança de memória. E aqui os defensores da linguagem têm um argumento forte.

Uma quantidade absurda de vulnerabilidades históricas em sistemas escritos em C e C++ vem de problemas como out-of-bounds read, out-of-bounds write, use-after-free e outros erros de acesso à memória. Rust ataca exatamente esse território. Não é marketing vazio.

Mas segurança não é uma coluna booleana chamada memory_safe.

Você ainda pode ter autenticação quebrada. Autorização ruim. Injeção. SSRF. Race condition lógica. Criptografia mal usada. Dependência comprometida. Build pipeline frágil. Segredo vazado. Configuração aberta. Política de atualização inexistente. Maintainer burnout. Supply chain com quinhentos crates que ninguém auditou.

Rust reduz uma classe de problema. Uma classe importante, mas uma classe.

Transformar isso em “Rust é seguro” é preguiça técnica. O correto é dizer: Rust oferece garantias fortes contra várias falhas de memória em código seguro, mas o sistema final ainda depende de arquitetura, revisão, testes, dependências, deploy, observabilidade e manutenção.

Menos slogan, mais engenharia.

O ecossistema também tem custo

Cargo é excelente. Talvez uma das melhores experiências de tooling que uma linguagem moderna oferece. Criar projeto, rodar teste, formatar, publicar pacote e gerenciar dependências é muito melhor em Rust do que em muito ecossistema mais antigo.

Mas todo ecossistema com dependências fáceis também cria o risco de dependências fáceis demais.

Crates pequenos resolvem problemas pequenos. Crates demais criam árvores grandes. Árvores grandes criam superfície de supply chain, custo de auditoria, risco de abandono, conflitos de versão e aquele momento bonito em que uma biblioteca simples para lidar com string puxa metade da civilização ocidental.

Isso não é exclusivo de Rust. Digo com propriedade: JavaScript já mostrou esse filme em versão estendida, com cenas pós-créditos e trauma coletivo.

Mas Rust não está imune.

Se o argumento para sair de C ou C++ é segurança, faz pouco sentido entrar em Rust ignorando procedência, manutenção e auditoria das dependências. Trocar buffer overflow por pacote abandonado não é exatamente evolução da espécie.

Go, Java, C# e C++ não desapareceram porque Rust existe

Outra coisa que o culto costuma ignorar: outras linguagens também fazem escolhas coerentes.

Go aceita um runtime simples, garbage collector e uma linguagem menos expressiva para ganhar produtividade, build rápido, deploy fácil e simplicidade operacional. Para muita API, serviço interno, ferramenta de infraestrutura e sistema de rede, isso é uma troca excelente.

Java e C# têm runtimes pesados, mas também têm ecossistemas maduros, tooling absurdo, GCs sofisticados, observabilidade excelente e décadas de batalha em produção. Dá para fazer software horrível nelas, obviamente. Também dá para sustentar sistemas gigantes com produtividade alta.

C++ continua sendo uma monstruosidade poderosa. É fácil criticar C++, e muitas críticas são merecidas, mas a quantidade de software crítico, rápido e sofisticado escrito nele não desaparece porque Rust tem um compilador mais protetor.

Rust entra muito bem onde você quer controle baixo nível, segurança de memória sem GC, binário nativo e possibilidade de expressar invariantes fortes no tipo. Isso não torna todas as outras escolhas burras. Torna Rust uma escolha boa para certos problemas.

O nome disso é trade-off. Uma palavra que fanboy normalmente trata como ofensa pessoal.

A pergunta certa para adotar Rust

Antes de colocar Rust em uma codebase existente, algumas perguntas valem mais do que qualquer benchmark de blog:

  • O problema real é segurança de memória, latência, consumo de memória ou concorrência?
  • O componente pode ser isolado por uma interface pequena?
  • A equipe sabe Rust o suficiente para manter o código depois da empolgação inicial?
  • O build, empacotamento e CI suportam essa toolchain sem virar gambiarra permanente?
  • As dependências serão auditadas?
  • Existe teste suficiente para provar que a versão nova preserva comportamento antigo?
  • O ganho esperado foi medido ou só imaginado?

Se a resposta for boa, Rust pode ser uma excelente escolha.

Se a resposta for “porque Rust é mais seguro”, “porque está todo mundo usando” ou “porque C++ é feio”, talvez o problema não esteja na linguagem antiga. Talvez esteja na falta de critério.

E falta de critério compila em qualquer linguagem.

Rust não precisa de culto

O mais irônico é que Rust não precisa desse culto.

A linguagem já tem méritos técnicos suficientes. Ela não precisa ser vendida como bala de prata, substituta universal, purificação de codebase ou caminho espiritual para transcender o garbage collector.

Rust é boa porque força clareza sobre propriedade, reduz riscos de memória, permite performance previsível, entrega tooling decente e torna certas classes de erro muito mais difíceis. Isso já é bastante coisa.

Quando alguém exagera e diz que tudo deveria ser Rust, enfraquece o próprio argumento. Porque quem mantém software de verdade sabe que tecnologia boa também tem custo. Sabe que migração dói. Sabe que build importa. Sabe que equipe importa. Sabe que legado não é só código velho, é conhecimento acumulado em estado bruto.

O culto da ferrugem tenta transformar uma linguagem excelente em religião técnica.

Eu prefiro tratar Rust como ferramenta. Uma ferramenta muito boa, muitas vezes melhor que as alternativas, mas ainda uma ferramenta.

Porque no fim, código ruim em Rust continua sendo código ruim.

Só que agora ele compila com mais dignidade.

Referências