Redes de Segurança Automáticas — Sanitizers e clang-tidy

Redes de Segurança Automáticas — Sanitizers e clang-tidy

Um observa o programa rodando, o outro lê o código sem executá-lo: sanitizers e análise estática pegam coisas diferentes e não se substituem. O artigo mostra o ASan diante de um acesso fora dos limites, o TSan diante de uma corrida de dados, o clang-tidy sobre o fonte, e por que nada disso vai para produção.
Linguagem C++

11 min de leitura

No artigo Do Comando Único ao Projeto Real — Construindo com CMake estruturamos projetos com CMake. Mas um projeto que compila e roda pode estar silenciosamente errado — cheio de bugs que não aparecem na compilação nem nos testes casuais, e que explodem em produção. Ao longo do curso, alertei sobre muitos desses perigos: acessos a memória fora dos limites (Fase 4), referências e ponteiros pendentes (Fases 1, 2, 4), vazamentos por gestão manual (Fase 2), corridas de dados (Fase 7). A boa notícia que fecha o ciclo é: existem ferramentas que detectam automaticamente esses erros, apontando o arquivo e a linha exatos. Os sanitizers instrumentam seu programa para flagrar erros de memória e concorrência em tempo de execução; o clang-tidy analisa seu código-fonte em busca de problemas e maus hábitos. Hoje você ganha essas redes de segurança — e elas transformam "parece funcionar" em "sei que está correto".

AddressSanitizer: caçando erros de memória

O AddressSanitizer (ASan) é talvez a ferramenta mais valiosa da aula. Ele instrumenta o programa para detectar, em tempo de execução, uma lista de erros de memória que de outra forma seriam comportamento indefinido silencioso: acesso fora dos limites de um array, uso de memória após liberação (use-after-free), vazamentos, e mais. Ativá-lo é uma flag de compilação. Veja um bug clássico que compila sem erro e roda "às vezes":

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3};
    // BUG: acesso fora dos limites. v[3] não existe (índices válidos: 0,1,2).
    std::cout << v[3] << '\n';   // comportamento indefinido — pode imprimir lixo ou travar
    return 0;
}

Compilado normalmente, isso pode imprimir lixo e seguir em frente, escondendo o bug. Compilado com o AddressSanitizer, ele para no exato ponto do erro, com um relatório detalhado:

# Ative o ASan com -fsanitize=address (e -g para nomes/linhas no relatório):
g++ -std=c++20 -fsanitize=address -g programa.cpp -o programa
./programa
# ASan imprime: "heap-buffer-overflow ... READ of size 4 ... programa.cpp:7"
# apontando o arquivo e a LINHA exata do acesso inválido.

Em vez de um bug fantasma que se manifesta aleatoriamente, você recebe uma mensagem precisa: "leitura inválida na linha 7". O ASan torna visíveis exatamente os erros que o C++ deixa como comportamento indefinido — os mais difíceis de depurar sem ele. O custo: o programa roda mais devagar (tipicamente 2x) e usa mais memória, então o ASan é para desenvolvimento e testes, não para produção. Mas rodar seus testes sob ASan pega uma classe enorme de bugs antes que cheguem ao usuário.

ThreadSanitizer: caçando corridas de dados

Lembra da corrida de dados da Fase 7 — o contador que dava resultados errados e imprevisíveis? Corridas de dados são especialmente traiçoeiras porque são não-determinísticas: podem não aparecer em mil execuções de teste. O ThreadSanitizer (TSan) foi feito para caçá-las, detectando acessos concorrentes não sincronizados mesmo que a corrida não chegue a causar erro visível naquela execução:

#include <thread>
#include <iostream>

int main() {
    int contador = 0;   // compartilhado, sem sincronização
    std::thread a([&]{ for (int i = 0; i < 10000; ++i) ++contador; });
    std::thread b([&]{ for (int i = 0; i < 10000; ++i) ++contador; });
    a.join();
    b.join();
    std::cout << contador << '\n';
    return 0;
}

Compilado com -fsanitize=thread, o TSan detecta que contador é acessado por duas threads sem proteção e reporta a corrida, apontando as duas linhas conflitantes — mesmo que naquela execução o número final parecesse plausível:

g++ -std=c++20 -fsanitize=thread -g programa.cpp -o programa
./programa
# TSan imprime: "WARNING: data race ... write ... previous write ..."
# identificando as threads e as linhas do acesso não sincronizado.

Isso é ouro para código concorrente. Um bug que só apareceria uma vez a cada milhão de execuções em produção é flagrado no desenvolvimento. (Nota: ASan e TSan não podem ser usados juntos na mesma compilação — você roda seus testes uma vez sob cada um.)

clang-tidy: análise estática do código-fonte

Enquanto os sanitizers analisam o programa rodando, o clang-tidy analisa o código-fonte sem executá-lo — é um analisador estático. Ele detecta padrões suspeitos, possíveis bugs e violações de boas práticas, muitas delas exatamente as que discutimos no curso: usar new/delete cru onde um ponteiro inteligente caberia, funções que deveriam ser const, conversões perigosas, e centenas de outras. Ele até sugere correções automáticas para muitos casos:

# Analisa um arquivo, verificando várias categorias de checagem:
clang-tidy programa.cpp -- -std=c++20
# Exemplo de saída:
# warning: use 'auto' when initializing with a cast [modernize-use-auto]
# warning: variable 'x' is not initialized [cppcoreguidelines-init-variables]
# warning: prefer 'make_unique' to 'new' [modernize-make-unique]

Repare que os nomes das checagens (cppcoreguidelines-*, modernize-*) remetem diretamente às práticas que ensinamos — o clang-tidy é, em boa medida, as C++ Core Guidelines e o C++ moderno codificados numa ferramenta automática. Rodá-lo sobre seu código é como ter um revisor experiente apontando onde você poderia aplicar melhor o que aprendeu. Integrá-lo ao CMake (via a opção CMAKE_CXX_CLANG_TIDY) faz a análise rodar a cada compilação.

A honestidade: ferramentas ajudam, não substituem o julgamento

Prometo sempre a medida certa. Essas ferramentas são transformadoras, mas não são mágicas, e vender isso seria desonesto. Os sanitizers só pegam bugs nos caminhos que seus testes efetivamente exercitam — se um teste nunca aciona a corrida de dados, o TSan não a vê; por isso eles se combinam com boa cobertura de testes (próximo artigo). O clang-tidy gera, às vezes, falsos positivos ou avisos irrelevantes ao seu contexto, e configurá-lo (escolher quais checagens ligar) faz parte do uso maduro. E nenhuma ferramenta substitui entender por que algo é um bug — o conhecimento que você construiu ao longo do curso é o que dá sentido aos relatórios delas. A postura correta: rode ASan e TSan nos seus testes rotineiramente, use clang-tidy como revisor automático, mas trate os resultados com julgamento, não como oráculos. Ferramentas amplificam um bom programador; não criam um.

Sanitizer e análise estática não competem, olham momentos diferentes: o ASan observa o programa rodando e aponta o acesso inválido no instante em que acontece, enquanto o clang-tidy lê o código-fonte e aponta padrões suspeitos sem executar nada. Os sanitizers custam caro — memória e tempo de execução várias vezes maiores —, o que os torna ferramenta de desenvolvimento e de testes, não de produção. Ligá-los na integração contínua é onde rendem mais.

Fontes e leituras recomendadas

  • clang.llvm.org/docs/AddressSanitizer.html e ThreadSanitizer.html: a documentação oficial dos sanitizers, com a lista de erros que cada um detecta.
  • clang.llvm.org/extra/clang-tidy: a documentação do clang-tidy, com o catálogo completo de checagens (incluindo as cppcoreguidelines-*).
  • "Sanitizers" (github.com/google/sanitizers/wiki): o wiki do projeto original, com detalhes técnicos e exemplos de uso.
  • ISO C++ Core Guidelines, seção sobre ferramentas de suporte: como as guidelines se conectam a analisadores estáticos como o clang-tidy.
  • Documentação do GCC e do Clang sobre -fsanitize: as flags exatas e suas combinações, incluindo -fsanitize=undefined (UBSan) para comportamento indefinido.

Exercícios

Exercício 1

Escreva um pequeno programa com um acesso fora dos limites de um std::vector (como v[v.size()]), compile-o com -fsanitize=address -g, rode e descreva o relatório do ASan.

Ver resposta

✓ Resposta: Programa e relatório:

#include <vector>
#include <iostream>
int main() {
    std::vector<int> v = {10, 20, 30};
    std::cout << v[v.size()] << '\n';   // acesso ao índice 3, inválido
    return 0;
}
g++ -std=c++20 -fsanitize=address -g bug.cpp -o bug
./bug

O ASan interrompe o programa e imprime um relatório do tipo heap-buffer-overflow, indicando que houve uma leitura (READ of size 4) além do fim do bloco alocado, apontando o arquivo e a linha do acesso (a linha do v[v.size()]), além de mostrar onde a memória foi alocada. Em vez de imprimir lixo e continuar, ele localiza o erro com precisão cirúrgica.

Exercício 2

Explique a diferença fundamental entre um sanitizer (como o ASan) e um analisador estático (como o clang-tidy) quanto a quando e como cada um detecta problemas.

Ver resposta

✓ Resposta: Um sanitizer como o ASan detecta problemas em tempo de execução: ele instrumenta o programa compilado e observa o comportamento real enquanto ele roda, pegando erros no momento em que ocorrem — mas só nos caminhos que a execução efetivamente percorre. Um analisador estático como o clang-tidy detecta problemas em tempo de análise do código-fonte, sem executar o programa: ele examina o texto do código procurando padrões suspeitos e violações de regras, cobrindo todo o código escrito (não só o executado), mas sem "ver" o comportamento real em tempo de execução. o sanitizer pega bugs reais que acontecem ao rodar (a um custo de execução e limitado aos caminhos testados); o analisador estático aponta problemas potenciais lendo o código (sem rodar, cobrindo tudo, mas sujeito a falsos positivos). São complementares.

Exercício 3

Compile o exemplo do contador com corrida de dados (dois threads, sem mutex) com -fsanitize=thread -g e descreva o que o TSan reporta. Depois adicione um mutex e confirme que o aviso desaparece.

Ver resposta

✓ Resposta: Com o TSan:

g++ -std=c++20 -fsanitize=thread -g contador.cpp -o contador
./contador

O TSan reporta um WARNING: data race, identificando que duas threads acessam contador sem sincronização — mostrando a escrita de uma thread e a escrita/leitura anterior conflitante da outra, com as linhas e pilhas de chamada de cada uma. Ao adicionar um std::mutex com std::lock_guard protegendo o ++contador (como no artigo O Cadeado que Protege — std::mutex e lock_guard), o acesso passa a ser sincronizado, e o TSan não encontra mais corrida — o aviso desaparece, confirmando que a proteção foi efetiva.

Exercício 4

Explique por que os sanitizers são adequados para desenvolvimento/testes mas não para produção. Cite o custo que eles impõem e por que isso é aceitável durante o desenvolvimento.

Ver resposta

✓ Resposta: Os sanitizers são adequados para desenvolvimento/testes mas não para produção porque impõem um custo significativo: o AddressSanitizer tipicamente torna o programa cerca de 2x mais lento e aumenta bastante o uso de memória (mantém metadados sobre cada região alocada); o ThreadSanitizer pode desacelerar ainda mais. Em produção, onde desempenho e uso de recursos importam para o usuário final, essa sobrecarga é inaceitável. Durante o desenvolvimento, porém, o custo é perfeitamente aceitável: a lentidão extra de rodar a suíte de testes sob sanitizer é um preço pequeno frente ao valor de flagrar, cedo, bugs de memória e concorrência que seriam catastróficos e quase impossíveis de depurar em produção. A prática é: compile builds instrumentados para testar, e builds normais (otimizados, sem sanitizer) para distribuir.

Exercício 5

Discuta a afirmação "ferramentas ajudam, não substituem o julgamento". Dê um exemplo de bug que o ASan pegaria facilmente, e outro de problema de design que nenhuma dessas ferramentas detectaria, exigindo o conhecimento do programador.

Ver resposta

✓ Resposta: A afirmação reconhece que as ferramentas detectam categorias específicas de erros mecânicos, mas não entendem a intenção nem o design do seu programa. Exemplo que o ASan pegaria facilmente: um use-after-free — usar um objeto através de um ponteiro depois que ele foi deletado; o ASan flagra o acesso à memória liberada com arquivo e linha exatos. Exemplo de problema que nenhuma dessas ferramentas detectaria: um erro de lógica ou de design — por exemplo, uma função calcula_juros que aplica a fórmula errada (juros simples onde deveria ser composto), ou uma hierarquia de classes que modela mal o domínio (herança onde deveria ser composição, como no artigo Uma Classe que Nasce de Outra — Herança e a Cadeia de Construção). O código compila, não tem erro de memória nem corrida, roda sem travar — e está conceitualmente errado. Nenhum sanitizer ou analisador estático sabe qual era a fórmula certa ou qual o design adequado; isso exige o entendimento do programador sobre o problema e sobre boas práticas. As ferramentas garantem que o código faça corretamente o que está escrito; só o julgamento humano garante que esteja escrito a coisa certa.

Comentários

Mais em Linguagem C++

A Filosofia por Trás do C++ e a Saída Elegante do printf
A Filosofia por Trás do C++ e a Saída Elegante do printf

Três detalhes aparecem na primeira linha de qualquer programa C++: o iostream…

Extensão — Dados que Sobrevivem: Integrando SQLite com RAII
Extensão — Dados que Sobrevivem: Integrando SQLite com RAII

Envolver a API C do SQLite numa classe é o exercício de RAII mais próximo do…

Extensão — Páginas Dinâmicas: HTML Gerado com Templates
Extensão — Páginas Dinâmicas: HTML Gerado com Templates

Montar HTML concatenando string quebra por dois motivos ao mesmo tempo: fica…