Armadilhas Comuns e Codificação Segura
Armadilhas em C são como buracos na estrada — parecem inofensivos, mas pise em um e seu programa falha. Aprenda a reconhecer esses buracos para poder desviar deles.
Armadilhas de Precedência de Operadores
C tem 15 níveis de precedência de operadores, e algumas combinações são propensas a erros:
O Erro de Precedência Mais Comum
int *p = malloc(10 * sizeof(int));
if (p == NULL)
Isso não é um erro, mas isto é:
int a = 1, b = 2, c = 3;
int result = a & b == 0;
== tem precedência maior que &, então isso é na verdade a & (b == 0), que é 1 & 0 = 0, não (a & b) == 0.
Referência Rápida de Precedência (Casos Propensos a Erro)
| Expressão Armadilha | Significado Real | Significado Pretendido | Correção |
|---|---|---|---|
a & b == 0 |
a & (b == 0) |
(a & b) == 0 |
Adicionar parênteses |
a << 2 + 1 |
a << (2+1) |
(a<<2)+1 |
Adicionar parênteses |
*p++ |
*(p++) |
(*p)++ |
Adicionar parênteses |
a | b + c |
a | (b+c) |
(a|b)+c |
Adicionar parênteses |
Acesso Fora dos Limites do Array
C não verifica índices de arrays — acesso fora dos limites é comportamento indefinido. Você pode ler lixo de memória, o programa pode falhar, ou pode "funcionar por acaso" (que é o resultado mais perigoso).
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 100;
arr[-1] = 99;
Ambas as linhas escrevem fora dos limites. O compilador não reclamará, mas isso pode corromper outras variáveis ou causar falha.
Fora dos Limites Clássico: Erro Off-by-One em Laços
int arr[5];
for (int i = 0; i <= 5; i++) {
arr[i] = 0;
}
i <= 5 deveria ser i < 5. Esse erro off-by-one faz com que arr[5] seja escrito.
String Fora dos Limites
char buf[5];
strcpy(buf, "Olá, Mundo!");
buf tem apenas 5 bytes de espaço, mas a string precisa de 14 bytes (incluindo '\0') — um estouro de buffer clássico.
Ponteiros Pendentes
Um ponteiro que ainda referencia memória que já foi liberada:
int *create_value(void) {
int x = 42;
return &x;
}
x é uma variável local. Após a função retornar, o quadro da pilha é liberado, tornando &x um ponteiro pendente. Acessá-lo pode ler lixo de memória.
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);
Após free(p), a memória apontada por p foi devolvida. Acessá-la através de p é comportamento indefinido.
free(p); p = NULL;. Assim, se você acidentalmente usá-lo depois, pelo menos terá uma falha de segmentação em vez de um erro aleatório difícil de rastrear.
Vazamentos de Memória
Memória alocada que nunca é liberada:
void leak_example(void) {
int *p = malloc(100 * sizeof(int));
if (p == NULL) return;
if (some_error) return;
free(p);
}
Quando some_error é verdadeiro, a função retorna cedo, free(p) é ignorado, e o equivalente a 100 ints de memória vaza.
Versão Correta
void no_leak(void) {
int *p = malloc(100 * sizeof(int));
if (p == NULL) return;
if (some_error) {
free(p);
return;
}
free(p);
}
Comportamento Indefinido
Comportamento indefinido (UB) é o conceito mais perigoso em C — o padrão diz "o comportamento é indefinido", o que significa que qualquer coisa pode acontecer.
Comportamentos Indefinidos Comuns
| Comportamento | Descrição |
|---|---|
| Acesso fora dos limites do array | Índice excede os limites do array |
| Desreferenciar um ponteiro nulo | *NULL |
| Usar um ponteiro pendente | Acessar memória liberada |
| Estouro de inteiro com sinal | INT_MAX + 1 |
| Deslocar além da largura de bits | 1 << 100 |
| Modificar a mesma variável duas vezes em uma expressão | i = i++ + 1 |
| Divisão por zero | int x = 1 / 0 |
| Ler uma variável não inicializada | int x; printf("%d", x); |
Exemplo
A armadilha de i = i++ + 1:
#include <stdio.h>
int main(void) {
int i = 3;
i = i++ + 1;
printf("%d\n", i);
return 0;
}
Diferentes compiladores podem exibir 4, 5 ou outros valores. Como i++ modifica i, e a atribuição também modifica i, modificar a mesma variável duas vezes em uma única instrução é comportamento indefinido.
Prevenção de Estouro de Buffer
Estouro de buffer é a vulnerabilidade de segurança mais famosa do C. O Morris Worm de 1988 explorou ataques de estouro de buffer.
Funções Perigosas vs. Alternativas Seguras
| Função Perigosa | Problema | Alternativa Segura |
|---|---|---|
gets(buf) |
Sem limite de comprimento | fgets(buf, size, stdin) |
strcpy(dst, src) |
Sem verificação do tamanho do destino | strncpy(dst, src, size-1) |
sprintf(buf, fmt, ...) |
Sem verificação do tamanho do destino | snprintf(buf, size, fmt, ...) |
strcat(dst, src) |
Sem verificação do espaço restante | strncat(dst, src, size-strlen(dst)-1) |
scanf("%s", buf) |
Sem limite de comprimento de entrada | scanf("%99s", buf) ou scanf_s |
Funções Seguras em Detalhe
strncpy
char *strncpy(char *dest, const char *src, size_t n);
Copia no máximo n caracteres. Se src tiver menos que n caracteres, o restante de dest é preenchido com '\0'. Mas se src tiver exatamente n caracteres, dest não será automaticamente terminado em nulo!
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[6];
strncpy(buf, "Hello World", sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
printf("%s\n", buf);
return 0;
}
Hello
strncpy não adiciona '\0' automaticamente! Você deve adicioná-lo manualmente após a cópia, ou operações de string subsequentes podem sair dos limites.
snprintf
int snprintf(char *str, size_t size, const char *format, ...);
Escreve no máximo size-1 caracteres e adiciona '\0' automaticamente.
#include <stdio.h>
int main(void) {
char buf[10];
int n = snprintf(buf, sizeof(buf), "O valor é %d", 12345);
printf("Saída: \"%s\"\n", buf);
printf("Comprimento necessário: %d, Espaço real: %d\n", n, (int)sizeof(buf));
return 0;
}
Saída: "O valor é"
Comprimento necessário: 16, Espaço real: 10
Exemplo
Padrão seguro de tratamento de entrada:
#include <stdio.h>
#include <string.h>
int main(void) {
char name[32];
printf("Digite seu nome: ");
if (fgets(name, sizeof(name), stdin) == NULL) {
fprintf(stderr, "Falha na entrada\n");
return 1;
}
name[strcspn(name, "\n")] = '\0';
if (strlen(name) == 0) {
fprintf(stderr, "O nome não pode estar vazio\n");
return 1;
}
printf("Olá, %s!\n", name);
return 0;
}
fgets + strcspn para remover a nova linha é o padrão seguro padrão para tratar entrada do usuário. Nunca use gets.
Exemplo
Concatenação segura de strings:
#include <stdio.h>
#include <string.h>
int main(void) {
char path[256] = "/home/usuario";
const char *subdir = "/documentos/trabalho/projetos/2024";
size_t current_len = strlen(path);
size_t remaining = sizeof(path) - current_len - 1;
if (strlen(subdir) < remaining) {
strncat(path, subdir, remaining);
} else {
fprintf(stderr, "Caminho muito longo, não é possível concatenar\n");
return 1;
}
printf("Caminho: %s\n", path);
return 0;
}
Caminho muito longo, não é possível concatenar
Uso Seguro do scanf
scanf é a função de entrada mais usada por iniciantes, e também a mais perigosa:
char buf[10];
scanf("%s", buf);
Se o usuário digitar mais de 9 caracteres, há estouro. Versão segura:
char buf[10];
scanf("%9s", buf);
Ou use scanf_s em compiladores que suportam C11:
char buf[10];
scanf_s("%9s", buf, (unsigned)sizeof(buf));
scanf_s faz parte do Anexo K opcional do C11. MSVC o suporta, mas gcc/clang podem não suportar. Para código multiplataforma, a combinação fgets + sscanf é recomendada.
Estouro de Inteiro
Estouro de inteiro com sinal é comportamento indefinido:
#include <stdio.h>
#include <limits.h>
int main(void) {
int a = INT_MAX;
int b = a + 1;
printf("%d + 1 = %d\n", a, b);
return 0;
}
A saída não é necessariamente INT_MIN — o compilador pode otimizar essa adição.
Método de verificação seguro:
#include <limits.h>
int safe_add(int a, int b) {
if (a > 0 && b > INT_MAX - a) return 0;
if (a < 0 && b < INT_MIN - a) return 0;
return a + b;
}
Outras Armadilhas Comuns
sizeof com Parâmetros de Array
void func(int arr[]) {
printf("%zu\n", sizeof(arr));
}
Quando um array é passado como parâmetro de função, ele decai em um ponteiro. sizeof(arr) fornece o tamanho do ponteiro (4 ou 8), não o tamanho do array. Você deve passar o comprimento como um parâmetro adicional.
Armadilhas de Definição de Macro
#define SQUARE(x) x * x
SQUARE(2 + 3)
Isso se expande para 2 + 3 * 2 + 3 = 11, não 25. Versão correta:
#define SQUARE(x) ((x) * (x))
Mas SQUARE(i++) ainda tem um problema (i é incrementado duas vezes). Uma função inline é mais segura.
== e =
if (x = 5) {
}
Isso é atribuição, não comparação! O valor da expressão de atribuição é 5 (não zero), então a condição é sempre verdadeira. Alguns compiladores alertam sobre isso, mas nem todos têm esse aviso ativado.
❓ Perguntas Frequentes
P: Por que C não verifica automaticamente os limites do array? R: Desempenho. Verificar o índice em cada acesso tornaria o programa mais lento. A filosofia de design do C é "confiar no programador", ao custo de exigir que os programadores garantam sua própria segurança.
P: free(NULL) causará falha? R: Não. O padrão C garante que free(NULL) é seguro e não faz nada. Então não há necessidade de verificar se um ponteiro é NULL antes de chamar free.
P: Como detectar vazamentos de memória? R: No Linux, use a ferramenta Valgrind:
valgrind --leak-check=full ./programa. No Windows, você pode usar o debug heap do Visual Studio ou o AddressSanitizer.
P: O que é AddressSanitizer? R: É uma ferramenta de detecção de erros de memória integrada ao compilador. Compile com
-fsanitize=addresspara ativá-lo. Ele pode detectar acesso fora dos limites, use-after-free, vazamentos de memória e outros problemas.
📖 Resumo
- Na dúvida sobre precedência de operadores, adicione parênteses — é a defesa mais simples
- Acesso fora dos limites do array e estouro de buffer são os problemas de segurança mais comuns em C
- Use
fgetsem vez degets,snprintfem vez desprintf,strncpyem vez destrcpy - Comportamento indefinido pode "funcionar por acaso" mas pode se comportar de forma diferente entre compiladores ou níveis de otimização
- Defina ponteiros como NULL após free, verifique valores de retorno do malloc e cuidado com estouro com sinal
📝 Exercícios
- Escreva uma função de cópia de string segura
safe_strcpy(char *dst, size_t dst_size, const char *src)que garanta que não haja estouro e sempre termine em nulo - Escreva um programa que cause intencionalmente acesso fora dos limites e estouro de inteiro, compile e execute com AddressSanitizer, e observe a saída
- Audite o código a seguir, encontre todos os problemas de segurança e corrija-os:
char buf[8]; gets(buf); sprintf(buf, "Resultado: %d", value);



