C Sanitizers (-fsanitize)
Em C, muitos erros de memória não derrubam o programa: ler uma posição depois do fim do array, usar memória já devolvida com free, somar além do maior int. O programa segue, mostra um número estranho (ou até o número certo, por sorte) e o erro fica escondido até o dia em que causa um estrago. Os sanitizers são fiscais que o compilador coloca dentro do seu programa: com uma opção a mais no gcc, cada acesso à memória e cada conta passam a ser conferidos, e o primeiro erro vira um relatório com o arquivo e a linha.
- O que é um sanitizer e por que ele acha erros que o compilador e o teste "rodou e deu certo" não acham.
- A ligar o AddressSanitizer (
-fsanitize=address) e ler o relatório de: estouro de array na pilha e no heap, uso depois dofree,freeduplo e vazamento de memória. - A ligar o UndefinedBehaviorSanitizer (
-fsanitize=undefined) e ler osruntime errorde estouro deinte de deslocamento de bits. - Por que compilar com
-gjunto, quanto o sanitizer custa e quando usar. - O que funciona (e o que não funciona) no editor do guia.
Todos os relatórios desta aula são reais, feitos com o gcc 15 na máquina fictícia da Ana, na pasta ~/c/san. Os sanitizers fazem parte do gcc e do Clang no Linux; não precisa instalar nada. No editor do guia eles não funcionam (o motivo e o que dá para fazer lá estão na seção 9): as caixas "Experimente" servem para consertar os erros que os relatórios encontraram.
A ideia: o fiscal dentro do programa
Pense numa lojinha sem câmeras: se alguém pega um produto da prateleira do vizinho, ninguém vê, e a falta só aparece no fim do mês, sem pista de quem foi. Agora coloque um fiscal ao lado de cada prateleira, anotando tudo: no primeiro produto pego do lugar errado, ele apita, diz qual prateleira, quem pegou e quando aquele produto chegou. A loja fica mais lenta (o fiscal confere tudo), mas o erro é pego na hora.
O sanitizer é esse fiscal. Com -fsanitize=…, o compilador acrescenta uma conferência antes de cada leitura e escrita na memória (ou antes de cada conta), e o programa passa a ser ligado a uma biblioteca que guarda um mapa de qual memória pode ser usada. O nome vem do inglês sanitize, "higienizar": o programa sai "limpo" de erros que antes passavam calados.
| Sanitizer | Opção | Acha |
|---|---|---|
| AddressSanitizer (ASan, "fiscal de endereços") | -fsanitize=address | Estouro de array (pilha e heap), uso depois do free, free duplo, vazamentos (com o LeakSanitizer embutido). |
| UndefinedBehaviorSanitizer (UBSan, "fiscal de comportamento indefinido") | -fsanitize=undefined | Estouro de int com sinal, deslocamento de bits grande demais, divisão por zero, índice fora do array declarado, ponteiro nulo e outros comportamentos indefinidos. |
1. Um erro que passa calado
A Ana quer somar as 5 notas de um aluno. Repare no <= do for:
#include <stdio.h>
int main(void) {
int notas[5] = {7, 8, 10, 6, 9};
int soma = 0;
for (int i = 0; i <= 5; i++) {
soma += notas[i];
}
printf("Soma: %d\n", soma);
return 0;
}
$ gcc -std=c23 -Wall -Wextra -g notas.c -o notas $ ./notas Soma: 40
Nenhum aviso, nenhum crash, e a soma está até certa (7 + 8 + 10 + 6 + 9 = 40). Mas o laço roda com i de 0 a 5, e notas[5] não existe: o array vai de notas[0] a notas[4] (veja Arrays). O programa leu a memória que vem logo depois do array, que por acaso valia 0. Em outro computador, com outro compilador ou depois de uma mudança qualquer no código, ali pode estar outro número, e a soma sai errada.
2. AddressSanitizer: -fsanitize=address
Compile de novo, com uma opção a mais:
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address notas.c -o notas
$ ./notas
=================================================================
==35068==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x6c184b3f0034 at pc 0x57c20fdee477 bp 0x7ffed26363e0 sp 0x7ffed26363d0
READ of size 4 at 0x6c184b3f0034 thread T0
#0 0x57c20fdee476 in main /home/ana/c/san/notas.c:7
#1 0x70184d42a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#2 0x70184d42a717 in __libc_start_main_impl ../csu/libc-start.c:360
#3 0x57c20fdee184 in _start (/home/ana/c/san/notas+0x1184) (BuildId: 5215d4f164e32298b14ee4699a42560f9944ebbc)
Address 0x6c184b3f0034 is located in stack of thread T0 at offset 52 in frame
#0 0x57c20fdee258 in main /home/ana/c/san/notas.c:3
This frame has 1 object(s):
[32, 52) 'notas' (line 4) <== Memory access at offset 52 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/ana/c/san/notas.c:7 in main
Shadow bytes around the buggy address:
0x6c184b3efd80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
«… mais 30 linhas: o mapa da memória (shadow bytes) e a legenda dele …»
==35068==ABORTING
$ echo $?
1
Assusta, mas só umas poucas linhas importam. Vamos ler de cima para baixo:
- ==35068==ERROR: AddressSanitizer: stack-buffer-overflow
- O tipo do erro.
35068é o número do processo (o PID), que muda a cada execução. stack-buffer-overflow = "estouro de um buffer na pilha": um array local (as variáveis locais moram na pilha) foi acessado fora dos limites. Os números0x…da linha são endereços; não precisa deles. - READ of size 4
- "Leitura de 4 bytes": o programa tentou ler um
int(4 bytes). Se fosseWRITE, seria uma escrita, que é ainda mais perigosa. - #0 … in main /home/ana/c/san/notas.c:7
- A pilha de chamadas do erro, como o
backtracedo gdb. O#0é onde aconteceu: funçãomain, arquivonotas.c, linha 7 (soma += notas[i];). As linhas#1a#3são da biblioteca do C, que chamou omain: pode ignorar. - This frame has 1 object(s): [32, 52) 'notas' (line 4)
- O ASan diz qual variável foi estourada: o array
notas, declarado na linha 4, ocupa os bytes 32 a 51 (são 20 bytes: 5intde 4 bytes). O[32, 52)quer dizer "do 32 até antes do 52". - <== Memory access at offset 52 overflows this variable
- "O acesso no byte 52 passa do fim desta variável": exatamente o primeiro byte depois de
notas[4], ou seja,notas[5]. - HINT: this may be a false positive…
- Uma dica genérica ("pode ser alarme falso se o seu programa usa truques especiais com a pilha"). Não é o seu caso; pode ignorar.
- SUMMARY: … notas.c:7 in main
- O resumo em uma linha: tipo do erro, arquivo, linha e função. Se for ler só uma linha, leia esta.
- Shadow bytes around the buggy address
- O "mapa da sombra": como o ASan marca cada pedaço da memória (
00= pode usar,f1/f3= zonas proibidas em volta do array). É útil para quem desenvolve o ASan; você pode pular. - ==35068==ABORTING e o código 1
- O ASan encerra o programa no primeiro erro, com código de saída 1. O
printf("Soma: …")nem chegou a rodar.
Receita de leitura: procure a linha ERROR: (o tipo), a primeira linha #0 que aponta para um arquivo seu (o lugar) e o SUMMARY. O resto é detalhe.
Missão: corrija a condição do for para o laço passar só por notas[0] a notas[4]. Repare que, no editor, o programa com erro também mostra Soma: 40: o erro passa calado aqui também (o editor não tem sanitizers). Por isso esta missão confere o seu código, não só a saída.
#include <stdio.h>
int main(void) {
int notas[5] = {7, 8, 10, 6, 9};
int soma = 0;
for (int i = 0; i <= 5; i++) {
soma += notas[i];
}
printf("Soma: %d\n", soma);
return 0;
}
#include <stdio.h>
int main(void) {
int notas[5] = {7, 8, 10, 6, 9};
int soma = 0;
for (int i = 0; i < 5; i++) {
soma += notas[i];
}
printf("Soma: %d\n", soma);
return 0;
}
3. Estouro no heap: o + 1 esquecido
O erro mais clássico de string em C: reservar espaço para as letras e esquecer o '\0' do fim (veja Strings Dinâmicas):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
const char *texto = "Ana Maria";
char *copia = malloc(strlen(texto));
if (copia == NULL) {
return 1;
}
strcpy(copia, texto);
printf("%s\n", copia);
free(copia);
return 0;
}
$ gcc -std=c23 -Wall -Wextra -g nome.c -o nome
$ ./nome
Ana Maria
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address nome.c -o nome
$ ./nome
=================================================================
==44079==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x74de027e0019 at pc 0x78be038bd2a9 bp 0x7ffd5aa52dd0 sp 0x7ffd5aa52588
WRITE of size 10 at 0x74de027e0019 thread T0
#0 0x78be038bd2a8 in strcpy ../../../../src/libsanitizer/asan/asan_interceptors.cpp:575
#1 0x5ce8fbcbc298 in main /home/ana/c/san/nome.c:11
#2 0x78be0342a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#3 0x78be0342a717 in __libc_start_main_impl ../csu/libc-start.c:360
#4 0x5ce8fbcbc184 in _start (/home/ana/c/san/nome+0x1184) (BuildId: 875af048c2b69d7a2eaaab3145f97a83eb855f6b)
0x74de027e0019 is located 0 bytes after 9-byte region [0x74de027e0010,0x74de027e0019)
allocated by thread T0 here:
#0 0x78be0392b60f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67
#1 0x5ce8fbcbc273 in main /home/ana/c/san/nome.c:7
#2 0x78be0342a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#3 0x78be0342a717 in __libc_start_main_impl ../csu/libc-start.c:360
#4 0x5ce8fbcbc184 in _start (/home/ana/c/san/nome+0x1184) (BuildId: 875af048c2b69d7a2eaaab3145f97a83eb855f6b)
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/ana/c/san/nome.c:11 in main
«… o mapa da memória …»
- heap-buffer-overflow
- "Estouro de buffer no heap": a memória estourada veio do
malloc(que reserva no heap). - WRITE of size 10
- Uma escrita de 10 bytes: "Ana Maria" tem 9 letras + o
'\0'. - #0 … in strcpy …asan_interceptors.cpp / #1 … in main …nome.c:11
- Quem escreveu foi o
strcpy(o ASan troca ostrcpyda biblioteca por uma versão que confere os tamanhos, um interceptador). A primeira linha com um arquivo seu é a#1:nome.c, linha 11. - located 0 bytes after 9-byte region
- "Fica 0 bytes depois de uma região de 9 bytes": o bloco tinha 9 bytes, e a escrita passou do fim logo no primeiro byte a mais (o lugar do
'\0'). - allocated by thread T0 here: … nome.c:7
- E o ASan ainda diz onde o bloco foi reservado: linha 7, o
malloc(strlen(texto)). Agora você sabe o erro: faltou o+ 1.
Missão: conserte o malloc da linha 7 para reservar espaço também para o '\0'. Mais uma vez, no editor o programa com erro parece funcionar: só o sanitizer (no seu Linux) prova que está errado.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
const char *texto = "Ana Maria";
char *copia = malloc(strlen(texto));
if (copia == NULL) {
return 1;
}
strcpy(copia, texto);
printf("%s\n", copia);
free(copia);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
const char *texto = "Ana Maria";
char *copia = malloc(strlen(texto) + 1);
if (copia == NULL) {
return 1;
}
strcpy(copia, texto);
printf("%s\n", copia);
free(copia);
return 0;
}
4. Usar a memória depois do free
Depois do free, a memória volta para o sistema e pode ser entregue a outro malloc. O ponteiro, porém, continua guardando o endereço antigo (veja malloc e free):
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *pontos = malloc(3 * sizeof *pontos);
if (pontos == NULL) {
return 1;
}
pontos[0] = 10;
free(pontos);
printf("Pontos: %d\n", pontos[0]);
return 0;
}
$ gcc -std=c23 -Wall -Wextra -g depois.c -o depois
depois.c: In function ‘main’:
depois.c:11:5: warning: pointer ‘pontos’ used after ‘free’ [-Wuse-after-free]
11 | printf("Pontos: %d\n", pontos[0]);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
depois.c:10:5: note: call to ‘free’ here
10 | free(pontos);
| ^~~~~~~~~~~~
$ ./depois
Pontos: -1245621546
Aqui o gcc 15 até avisou ("ponteiro pontos usado depois do free"), porque o free e o uso estão juntos, na mesma função. Em programas de verdade, o free costuma estar longe, em outra função ou outro arquivo, e o compilador não vê. O sanitizer vê sempre, porque confere enquanto o programa roda:
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address depois.c -o depois
«… o mesmo aviso …»
$ ./depois
=================================================================
==35681==ERROR: AddressSanitizer: heap-use-after-free on address 0x778cc45e0010 at pc 0x57e711ab92f8 bp 0x7ffccbd1d500 sp 0x7ffccbd1d4f0
READ of size 4 at 0x778cc45e0010 thread T0
#0 0x57e711ab92f7 in main /home/ana/c/san/depois.c:11
#1 0x7b6cc522a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#2 0x7b6cc522a717 in __libc_start_main_impl ../csu/libc-start.c:360
#3 0x57e711ab9184 in _start (/home/ana/c/san/depois+0x1184) (BuildId: 4af4807020acc7da00f6d89bc7cdee8b4b8782d1)
0x778cc45e0010 is located 0 bytes inside of 12-byte region [0x778cc45e0010,0x778cc45e001c)
freed by thread T0 here:
#0 0x7b6cc572a3ff in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:51
#1 0x57e711ab92c0 in main /home/ana/c/san/depois.c:10
#2 0x7b6cc522a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#3 0x7b6cc522a717 in __libc_start_main_impl ../csu/libc-start.c:360
#4 0x57e711ab9184 in _start (/home/ana/c/san/depois+0x1184) (BuildId: 4af4807020acc7da00f6d89bc7cdee8b4b8782d1)
previously allocated by thread T0 here:
#0 0x7b6cc572b60f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67
#1 0x57e711ab925e in main /home/ana/c/san/depois.c:5
#2 0x7b6cc522a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#3 0x7b6cc522a717 in __libc_start_main_impl ../csu/libc-start.c:360
#4 0x57e711ab9184 in _start (/home/ana/c/san/depois+0x1184) (BuildId: 4af4807020acc7da00f6d89bc7cdee8b4b8782d1)
SUMMARY: AddressSanitizer: heap-use-after-free /home/ana/c/san/depois.c:11 in main
«… o mapa da memória …»
O relatório conta a história inteira em três pilhas: onde o programa leu a memória morta (linha 11, READ of size 4), onde ela foi liberada (freed by thread T0 here: linha 10) e onde tinha sido reservada (previously allocated by thread T0 here: linha 5). heap-use-after-free = "uso do heap depois do free". 12-byte region: os 3 int do malloc. O "T0" é a thread 0, a única do programa (veja Threads).
5. free duas vezes
#include <stdlib.h>
#include <string.h>
int main(void) {
char *nome = malloc(20);
if (nome == NULL) {
return 1;
}
strcpy(nome, "Ana");
free(nome);
free(nome);
return 0;
}
Este caso a própria biblioteca do C às vezes pega, sem sanitizer nenhum, mas só diz que aconteceu, não onde:
$ gcc -std=c23 -Wall -Wextra -g duplo.c -o duplo «… aviso -Wuse-after-free na linha 11 …» $ ./duplo free(): double free detected in tcache 2 Abortado (imagem do núcleo gravada) ./duplo
"free(): liberação dupla detectada no tcache" (o tcache é uma parte interna do malloc da glibc) e o programa é abortado (código 134). Com o ASan, o relatório aponta a linha:
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address duplo.c -o duplo
«… o mesmo aviso …»
$ ./duplo
=================================================================
==35695==ERROR: AddressSanitizer: attempting double-free on 0x739b47fe0040 in thread T0:
#0 0x776b4912a3ff in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:51
#1 0x5a32d7102263 in main /home/ana/c/san/duplo.c:11
#2 0x776b48c2a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#3 0x776b48c2a717 in __libc_start_main_impl ../csu/libc-start.c:360
#4 0x5a32d7102144 in _start (/home/ana/c/san/duplo+0x1144) (BuildId: 7b49ba4a8a560fd325ffba21141050c77842c3ff)
0x739b47fe0040 is located 0 bytes inside of 20-byte region [0x739b47fe0040,0x739b47fe0054)
freed by thread T0 here:
#0 0x776b4912a3ff in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:51
#1 0x5a32d7102257 in main /home/ana/c/san/duplo.c:10
«… e onde foi reservado: duplo.c:5 …»
attempting double-free = "tentando liberar duas vezes": o segundo free é o da linha 11; o primeiro (freed by thread T0 here) foi o da linha 10. Um costume que evita o problema: depois do free(p), faça p = NULL;. Um free(NULL) não faz nada, então um segundo free deixa de ser um erro.
6. Vazamentos: o LeakSanitizer
Um vazamento é memória reservada com malloc que nunca é devolvida com free (veja Vazamentos de Memória). O ASan traz junto um terceiro fiscal, o LeakSanitizer ("fiscal de vazamentos"), que confere tudo quando o programa termina:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *copiar(const char *texto) {
char *copia = malloc(strlen(texto) + 1);
if (copia != NULL) {
strcpy(copia, texto);
}
return copia;
}
int main(void) {
char *item = copiar("arroz");
printf("Item: %s\n", item);
return 0;
}
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address vaza.c -o vaza
$ ./vaza
Item: arroz
=================================================================
==35718==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 6 byte(s) in 1 object(s) allocated from:
#0 0x73429b72b60f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67
#1 0x5a7e14f0e250 in copiar /home/ana/c/san/vaza.c:6
#2 0x5a7e14f0e28f in main /home/ana/c/san/vaza.c:14
#3 0x73429b22a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#4 0x73429b22a717 in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x5a7e14f0e164 in _start (/home/ana/c/san/vaza+0x1164) (BuildId: 40dcf9e3fb0fd1920489b7d96f582f96583bd114)
SUMMARY: AddressSanitizer: 6 byte(s) leaked in 1 allocation(s).
$ echo $?
1
- Item: arroz
- O programa rodou inteiro. O relatório só aparece no fim, quando o LeakSanitizer confere o que não foi devolvido.
- Direct leak of 6 byte(s) in 1 object(s)
- "Vazamento direto de 6 bytes em 1 bloco":
"arroz"tem 5 letras + o'\0'. Direto quer dizer que nenhum ponteiro aponta mais para o bloco. - #1 in copiar …vaza.c:6 / #2 in main …vaza.c:14
- Onde o bloco foi reservado: pelo
mallocda linha 6, dentro decopiar, que foi chamada pela linha 14 domain. O conserto é umfree(item);nomain, quando o item não for mais usado. - código de saída 1
- Mesmo com o programa tendo funcionado, o código de saída vira 1: um vazamento conta como erro.
7. UndefinedBehaviorSanitizer: -fsanitize=undefined
O segundo fiscal confere as contas e outras operações que o C deixa sem regra (o comportamento indefinido). Dois exemplos clássicos: passar do maior int (veja Inteiros) e deslocar bits demais (veja Operadores de Bits):
#include <stdio.h>
#include <limits.h>
int main(void) {
int visitas = INT_MAX;
visitas = visitas + 1;
printf("Visitas: %d\n", visitas);
int bits = 40;
unsigned int mascara = 1u << bits;
printf("Máscara: %u\n", mascara);
return 0;
}
$ gcc -std=c23 -Wall -Wextra -g conta.c -o conta $ ./conta Visitas: -2147483648 Máscara: 256 $ gcc -std=c23 -Wall -Wextra -g -fsanitize=undefined conta.c -o conta $ ./conta conta.c:6:13: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' Visitas: -2147483648 conta.c:10:31: runtime error: shift exponent 40 is too large for 32-bit type 'unsigned int' Máscara: 256 $ echo $? 0
- conta.c:6:13:
- Arquivo, linha e coluna, no mesmo formato das mensagens do compilador. A coluna 13 é o
+devisitas + 1. - runtime error:
- "Erro em tempo de execução": foi achado enquanto o programa rodava, não na compilação.
- signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
- "Estouro de inteiro com sinal: 2147483647 + 1 não cabe no tipo
int". O resultado,-2147483648, foi a máquina "dando a volta", mas o C não promete isso: com otimização, o compilador pode fazer qualquer coisa. - shift exponent 40 is too large for 32-bit type 'unsigned int'
- "O deslocamento de 40 é grande demais para um tipo de 32 bits". Deslocar um
unsigned intpor 32 ou mais é indefinido. O256que saiu é o que o processador da Ana faz (ele usa só os 5 bits de baixo de 40, que dão 8, e1 << 8= 256); outro processador pode dar 0. - echo $? → 0
- Diferente do ASan, o UBSan avisa e deixa o programa seguir: por isso os dois
printfrodaram e o código de saída é 0.
Para o programa parar no primeiro erro do UBSan (útil em testes automáticos, que olham o código de saída), acrescente -fno-sanitize-recover=undefined ("não se recupere"):
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=undefined -fno-sanitize-recover=undefined conta.c -o conta $ ./conta conta.c:6:13: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' $ echo $? 1
8. Os dois juntos, e o papel do -g
O ASan e o UBSan combinam bem. Separe os nomes por vírgula:
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address,undefined depois.c -o depois
$ ./depois
=================================================================
==40823==ERROR: AddressSanitizer: heap-use-after-free on address 0x6d11d31e0010 at pc 0x5f0a08f9b368 bp 0x7ffc60270680 sp 0x7ffc60270670
READ of size 4 at 0x6d11d31e0010 thread T0
#0 0x5f0a08f9b367 in main /home/ana/c/san/depois.c:11
«… o resto do relatório …»
E o -g? Sem ele, o sanitizer ainda acha o erro, mas não sabe dizer a linha: só o nome da função e uma posição dentro do executável.
$ gcc -std=c23 -Wall -Wextra notas.c -fsanitize=address -o notas-semg
$ ./notas-semg
=================================================================
==36762==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x76a27e1f0034 at pc 0x5f5e2c7ff477 bp 0x7ffec2236c00 sp 0x7ffec2236bf0
READ of size 4 at 0x76a27e1f0034 thread T0
#0 0x5f5e2c7ff476 in main (/home/ana/c/san/notas-semg+0x1476) (BuildId: c6b40295fd84665b28944a56e02ba72824e1cc60)
«… o resto, também sem linhas …»
Em vez de notas.c:7, apareceu (notas-semg+0x1476): "o byte 0x1476 do executável", que não ajuda ninguém. Sanitizer sempre com -g.
9. O custo, e o editor do guia
O fiscal confere tudo, e isso custa. Um programa que soma um milhão de números 200 vezes, com e sem sanitizers:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 1000000;
int *v = malloc(n * sizeof *v);
if (v == NULL) {
return 1;
}
long long soma = 0;
for (int volta = 0; volta < 200; volta++) {
for (int i = 0; i < n; i++) {
v[i] = i % 7;
soma += v[i];
}
}
printf("Soma: %lld\n", soma);
free(v);
return 0;
}
$ gcc -std=c23 -Wall -Wextra -g pesado.c -o pesado $ gcc -std=c23 -Wall -Wextra -g -fsanitize=address,undefined pesado.c -o pesado-san $ time ./pesado Soma: 599999400 real 0m1,081s user 0m1,077s sys 0m0,004s $ time ./pesado-san Soma: 599999400 real 0m2,558s user 0m2,549s sys 0m0,009s $ ls -l pesado pesado-san -rwxrwxr-x 1 ana ana 17632 out 17 10:35 pesado -rwxrwxr-x 1 ana ana 28472 out 17 10:35 pesado-san
Mais de duas vezes mais lento e com um executável maior (e, enquanto roda, o ASan usa bem mais memória, para o mapa da sombra). Em geral, o ASan deixa o programa umas 2 vezes mais lento; o UBSan, menos. É um preço ótimo enquanto você desenvolve e testa, e um preço ruim para o programa que vai para os usuários. Por isso a regra é: sanitizers no desenvolvimento e nos testes; a versão final sem eles (veja CMake e Makefile para ter as duas versões).
No editor do guia
O editor roda o seu programa em WebAssembly, e as bibliotecas dos sanitizers não existem lá. Pedir -fsanitize=address num data-flags dá erro na hora de ligar:
wasm-ld: error: /tmp/notas-62bf3f.o: undefined symbol: __asan_option_detect_stack_use_after_return wasm-ld: error: /tmp/notas-62bf3f.o: undefined symbol: __asan_stack_malloc_1 «… mais linhas iguais …» clang: error: linker command failed with exit code 1 (use -v to see invocation)
Mas há um modo do UBSan que funciona no editor: o modo armadilha, -fsanitize=undefined -fsanitize-trap=undefined. Nele não existe relatório: o fiscal só para o programa no primeiro erro. No editor, a parada aparece como Falha de segmentação (139). No Linux, como Instrução ilegal:
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=undefined -fsanitize-trap=undefined conta.c -o conta-trap $ ./conta-trap Instrução ilegal (imagem do núcleo gravada) ./conta-trap $ echo $? 132
Missão: esta caixa compila com o UBSan em modo armadilha. Rode e veja o programa parar logo no primeiro erro. Depois conserte os dois erros que o relatório da seção 7 apontou: use long long para visitas (com %lld no printf) e unsigned long long para mascara, com 1ull << bits e %llu (veja Inteiros). A saída deve ser Visitas: 2147483648 e Máscara: 1099511627776.
#include <stdio.h>
#include <limits.h>
int main(void) {
int visitas = INT_MAX;
visitas = visitas + 1;
printf("Visitas: %d\n", visitas);
int bits = 40;
unsigned int mascara = 1u << bits;
printf("Máscara: %u\n", mascara);
return 0;
}
#include <stdio.h>
#include <limits.h>
int main(void) {
long long visitas = INT_MAX;
visitas = visitas + 1;
printf("Visitas: %lld\n", visitas);
int bits = 40;
unsigned long long mascara = 1ull << bits;
printf("Máscara: %llu\n", mascara);
return 0;
}
Formas diferentes e qual usar
| Ferramenta | Como usa | Pontos fortes | Limites |
|---|---|---|---|
-Wall -Wextra | Sempre, na compilação. | De graça; acha erros antes de rodar. | Só vê o que está à vista no código (como o -Wuse-after-free na mesma função). |
-fsanitize=address | Recompila tudo com a opção. | Rápido (umas 2 vezes mais lento); pega estouros na pilha e no heap; diz onde a memória foi reservada e liberada; vazamentos. | Precisa recompilar; não pega variável sem valor inicial. |
-fsanitize=undefined | Recompila tudo com a opção. | Pega contas e deslocamentos inválidos; quase não pesa. | Por padrão avisa e segue. |
| Valgrind | valgrind ./programa, sem recompilar. | Pega uso de variável sem valor inicial; funciona com qualquer executável. | Muito mais lento (10 a 50 vezes); não pega estouro de array na pilha. |
| gdb | gdb ./programa. | Anda linha a linha e mostra variáveis. | Não acha erros sozinho: você precisa saber onde olhar. |
Qual usar: durante o desenvolvimento, compile com -g -fsanitize=address,undefined e rode os seus testes. Quando aparecer um erro estranho que o ASan não explica (um valor aleatório que muda a cada execução), tente o Valgrind, que pega variáveis sem valor inicial. O Clang tem ainda outros fiscais: -fsanitize=memory (variáveis sem valor inicial, só no Clang) e -fsanitize=thread (erros entre threads; não combina com o address).
Quando dá errado
-fsanitize em partes e o ligador reclamou
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address -c notas.c -o notas.o $ gcc notas.o -o notas2 /usr/bin/x86_64-linux-gnu-ld.bfd: notas.o: aviso: relocalização contra "__asan_option_detect_stack_use_after_return" em secção só de leitura ".text" /usr/bin/x86_64-linux-gnu-ld.bfd: notas.o: na função "main": /home/ana/c/san/notas.c:3:(.text+0x20): referência não definida para "__asan_option_detect_stack_use_after_return" /usr/bin/x86_64-linux-gnu-ld.bfd: /home/ana/c/san/notas.c:3:(.text+0x2d): referência não definida para "__asan_stack_malloc_1" «… várias outras referências a __asan_ …» collect2: error: ld returned 1 exit status
O que aconteceu: o notas.o foi compilado com o fiscal, mas a ligação (a linha sem -c) foi feita sem -fsanitize=address, então a biblioteca do ASan não entrou. "Referência não definida" é o undefined reference to do ligador em inglês (veja Vários Arquivos).
Como resolver: passe o mesmo -fsanitize=… em todas as etapas: na compilação de cada .c e na ligação.
gcc: error: unrecognized argument to ‘-fsanitize=’ option: ‘adress’; did you mean ‘address’?
O que aconteceu: erro de digitação no nome do sanitizer ("argumento não reconhecido; você quis dizer address?").
Como resolver: address tem dois d e dois s.
cc1: error: ‘-fsanitize=thread’ is incompatible with ‘-fsanitize=address’
O que aconteceu: o ASan e o TSan usam a memória de jeitos que não combinam ("é incompatível").
Como resolver: compile duas versões e teste uma de cada vez.
$ valgrind ./notas ==46557== Memcheck, a memory error detector «…» ==46557==ASan runtime does not come first in initial library list; you should either link runtime to your application or manually preload it with LD_PRELOAD. «… e um relatório vazio: ERROR SUMMARY: 0 errors …»
O que aconteceu: os dois fiscais querem controlar a memória ao mesmo tempo. O programa nem rodou de verdade.
Como resolver: para o Valgrind, compile sem -fsanitize.
(programa+0x1476)
O que aconteceu: faltou o -g.
Como resolver: recompile com -g -fsanitize=….
cannot specify -static with -fsanitize=address
O que aconteceu: o ASan precisa da sua biblioteca carregada à parte e não funciona em executáveis estáticos ("não dá para pedir -static com -fsanitize=address").
Como resolver: tire o -static da versão de teste (veja Bibliotecas).
Referência rápida
| Opção ou mensagem | O que é |
|---|---|
-fsanitize=address | Liga o ASan (e o LeakSanitizer). |
-fsanitize=undefined | Liga o UBSan. |
-fsanitize=address,undefined | Os dois juntos (o mais comum). |
-fno-sanitize-recover=undefined | O UBSan para o programa no primeiro erro (código 1). |
-fsanitize-trap=undefined | Modo armadilha: para sem relatório; funciona no editor. |
-g | Necessário para o relatório ter arquivo e linha. |
stack-buffer-overflow | Array local acessado fora dos limites. |
heap-buffer-overflow | Bloco do malloc acessado fora dos limites. |
global-buffer-overflow | Array global acessado fora dos limites. |
heap-use-after-free | Memória usada depois do free. |
attempting double-free | free duas vezes no mesmo bloco. |
LeakSanitizer: detected memory leaks | Vazamento: malloc sem free. |
READ of size N / WRITE of size N | Leitura / escrita de N bytes. |
runtime error: signed integer overflow | Estouro de inteiro com sinal. |
runtime error: shift exponent … is too large | Deslocamento de bits grande demais. |
runtime error: division by zero | Divisão inteira por zero. |
Exercícios
1. Ligar o fiscal de endereços
Complete a opção que liga o AddressSanitizer:
2. Os dois juntos
Complete para ligar o ASan e o UBSan numa opção só:
3. Sem linhas
O relatório mostra in main (/home/ana/c/prog+0x11a9) em vez de prog.c:12. Qual opção faltou na compilação?
4. O relatório diz heap-use-after-free. O que o programa fez?
5. Num relatório do ASan, qual linha diz onde no seu código está o erro?
6. Por que não deixar -fsanitize=address ligado na versão que vai para os usuários?
Resumo
- Muitos erros de memória em C não derrubam o programa: o resultado pode até parecer certo. Os sanitizers colocam um fiscal dentro do programa, que para (ou avisa) no primeiro erro.
-fsanitize=address(ASan): estouro de array na pilha e no heap, uso depois dofree,freeduplo e, no fim, vazamentos. Ele aborta no primeiro erro, com código 1.-fsanitize=undefined(UBSan): estouro deint, deslocamento grande demais, divisão por zero… Ele avisa comruntime errore segue (a não ser com-fno-sanitize-recover=undefined).- Para ler o relatório: o tipo na linha
ERROR, a primeira linha#Ncom um arquivo seu e oSUMMARY. Compile sempre com-g. - Custa velocidade e memória: use no desenvolvimento e nos testes, não na versão final. Passe a opção em todas as etapas (compilação e ligação).
- No editor do guia, só o modo armadilha do UBSan funciona; o resto, no seu Linux.