AddressSanitizer e UndefinedBehaviorSanitizer

1. Introdução aos Sanitizers no Ecossistema C

Programar em C exige cuidado redobrado com gerenciamento de memória e comportamento indefinido. Erros como buffer overflow, uso de memória liberada e overflow de inteiros podem passar despercebidos durante testes convencionais, mas causam falhas catastróficas em produção. É aqui que entram os sanitizers: ferramentas de instrumentação em tempo de compilação que detectam esses problemas dinamicamente.

Diferentemente do Valgrind, que executa o programa em um ambiente simulado e impõe overhead elevado (tipicamente 10-20x mais lento), os sanitizers como AddressSanitizer (ASan) e UndefinedBehaviorSanitizer (UBSan) instrumentam o código durante a compilação, resultando em overhead menor (cerca de 2x para ASan) e integração mais natural com o fluxo de desenvolvimento. Enquanto ferramentas como gprof focam em profiling de desempenho, os sanitizers são dedicados à detecção de bugs.

Tanto GCC (versão 4.8+) quanto Clang (versão 3.1+) oferecem suporte completo a ASan e UBSan, bastando adicionar flags específicas durante a compilação e linkedição.

2. AddressSanitizer (ASan): Detecção de Erros de Memória

O AddressSanitizer detecta uma ampla gama de erros de memória em tempo de execução:

  • Buffer overflow em stack, heap e variáveis globais
  • Use-after-free (uso de memória já liberada)
  • Double-free e memory leaks (em versões recentes)
  • Stack use-after-return e buffer overflow em variáveis locais

Para ativar o ASan, compile com a flag -fsanitize=address. Exemplo:

gcc -fsanitize=address -g -o programa programa.c

A flag -g é essencial para gerar informações de depuração nos backtraces. O ASan também requer linkedição com a biblioteca de runtime, o que ocorre automaticamente ao usar essa flag.

3. Funcionamento Interno do ASan

O ASan funciona através de dois mecanismos principais: shadow memory e redzones.

A shadow memory é uma região de memória especial que mapeia cada byte do programa para um byte de metadados. Cada byte da shadow memory indica se os 8 bytes correspondentes da memória real são acessíveis ou não. A cada operação de leitura/escrita, o código instrumentado verifica a shadow memory — se o acesso for inválido, um erro é reportado.

As redzones são regiões de memória inacessíveis colocadas ao redor de objetos alocados (arrays, buffers). Quando um buffer overflow ocorre, o programa tenta acessar a redzone, que está marcada como inacessível na shadow memory, disparando o erro.

A instrumentação ocorre em tempo de compilação: o compilador insere verificações antes de cada acesso à memória. Em tempo de execução, a biblioteca do ASan gerencia a shadow memory e as redzones.

Limitações: o ASan impõe overhead de aproximadamente 2x em tempo de execução e aumenta o consumo de memória em cerca de 3-4x. Não detecta vazamentos de memória em todos os cenários (para isso, use -fsanitize=leak adicionalmente).

4. UndefinedBehaviorSanitizer (UBSan): Pegando Comportamentos Indefinidos

O UBSan detecta comportamentos indefinidos (UB) da linguagem C, que muitas vezes passam despercebidos mas podem ser explorados por atacantes ou causar falhas intermitentes. Entre os UB detectáveis:

  • Overflow de inteiros com sinal
  • Shift com deslocamento maior que a largura do tipo
  • Divisão por zero
  • Violações de aliasing (acessar um objeto via ponteiro de tipo incompatível)
  • Acesso a memória não inicializada (com -fsanitize=memory)
  • Conversões inválidas entre tipos ponteiro

Ativação básica:

gcc -fsanitize=undefined -g -o programa programa.c

Subconjuntos específicos podem ser ativados, como -fsanitize=integer (apenas UB com inteiros) ou -fsanitize=null (apenas desreferenciamento de ponteiro nulo).

5. Combinando ASan e UBSan na Prática

ASan e UBSan podem ser usados simultaneamente, oferecendo cobertura abrangente:

gcc -fsanitize=address,undefined -g -o programa programa.c

Em projetos existentes, recomenda-se criar um perfil de compilação específico para debug, com essas flags ativadas. Para release, remova os sanitizers, pois eles impactam desempenho e aumentam o binário.

Os backtraces gerados apontam exatamente para a linha de código onde o erro ocorre. Para análise mais detalhada, use ASAN_OPTIONS=log_path=/tmp/asan para salvar logs em arquivo.

6. Casos de Uso e Exemplos em C

Exemplo 1: Buffer overflow detectado por ASan

#include <stdio.h>

int main() {
    int arr[5] = {0, 1, 2, 3, 4};
    // Acesso fora dos limites - buffer overflow
    arr[10] = 42;
    printf("%d\n", arr[10]);
    return 0;
}

Compilando com -fsanitize=address, ao executar o programa, o ASan reporta:

==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc...
WRITE of size 4 at 0x7ffc... thread T0
    #0 0x... in main /tmp/exemplo1.c:5

Exemplo 2: Undefined behavior com shift de signed int

#include <stdio.h>

int main() {
    int x = 1;
    // Shift de signed int para além do limite - UB
    int y = x << 31;
    printf("%d\n", y);
    return 0;
}

Com -fsanitize=undefined, o UBSan captura:

/tmp/exemplo2.c:5:13: runtime error: left shift of 1 by 31 places cannot be represented in type 'int'

Exemplo 3: Uso de ponteiro após free

#include <stdlib.h>

int main() {
    int *p = malloc(sizeof(int));
    *p = 42;
    free(p);
    // Uso de memória já liberada - use-after-free
    *p = 100;
    return 0;
}

O ASan detecta:

==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000...
WRITE of size 4 at 0x602000... thread T0
    #0 0x... in main /tmp/exemplo3.c:7

7. Limitações, Performance e Boas Práticas

O ASan impõe overhead típico de 2x em tempo de execução e 3-4x em memória, enquanto o UBSan tem overhead menor (tipicamente 10-30%). Em projetos grandes, compile apenas módulos críticos com sanitizers.

Falsos positivos podem ocorrer em código que intencionalmente acessa memória de maneiras não convencionais (como garbage collectors ou alocadores personalizados). Para suprimir warnings específicos, use atributos como __attribute__((no_sanitize("address"))).

Boas práticas:
- Integre sanitizers em testes unitários e pipelines de CI/CD
- Use -fsanitize-recover=address para continuar a execução após erros (útil em testes)
- Combine com -fno-omit-frame-pointer para backtraces mais precisos

8. Comparação com Outras Ferramentas e Considerações Finais

ASan vs. Valgrind: Valgrind (com Memcheck) detecta os mesmos tipos de erros, mas com overhead muito maior (10-20x). ASan é preferível para desenvolvimento diário; Valgrind pode ser usado para análise mais aprofundada ou quando ASan não está disponível.

UBSan e otimizações: compiladores frequentemente assumem que UB não ocorre ao aplicar otimizações agressivas. UBSan detecta essas situações, mas o código otimizado pode se comportar de forma imprevisível antes do erro ser reportado. Sempre teste com UBSan em modo debug.

Fluxo recomendado:
1. Desenvolva com -fsanitize=address,undefined -g -O0
2. Execute testes unitários e de integração
3. Corrija todos os erros reportados
4. Para release, compile sem sanitizers, mas mantenha testes com sanitizers no CI

Os sanitizers transformaram a depuração em C, tornando acessível a detecção de bugs que antes exigiam horas de análise manual. Adotá-los é passo fundamental para código C seguro e confiável.

Referências