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

C
int *p = malloc(10 * sizeof(int));
if (p == NULL)

Isso não é um erro, mas isto é:

C
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
💡 Dica: Regra de ouro: na dúvida, adicione parênteses! Parênteses são de graça; bugs são caros.

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).

C
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

C
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

C
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:

C
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.

C
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.

⚠️ Atenção: Sempre defina um ponteiro como NULL imediatamente após liberá-lo: 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:

C
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

C
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:

C
#include <stdio.h>

int main(void) {
    int i = 3;
    i = i++ + 1;
    printf("%d\n", i);
    return 0;
}
▶ Experimente

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.

💡 Dica: O aspecto aterrorizante do comportamento indefinido é que ele pode funcionar corretamente no modo de depuração, mas falhar em builds otimizados. O compilador pode assumir que UB nunca ocorre e realizar transformações de código inesperadas.

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

C
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!

C
#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;
}
TEXT
Hello
⚠️ Atenção: 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

C
int snprintf(char *str, size_t size, const char *format, ...);

Escreve no máximo size-1 caracteres e adiciona '\0' automaticamente.

C
#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;
}
TEXT
Saída: "O valor é"
Comprimento necessário: 16, Espaço real: 10

Exemplo

Padrão seguro de tratamento de entrada:

C
#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;
}
▶ Experimente
💡 Dica: 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:

C
#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;
}
▶ Experimente
TEXT
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:

C
char buf[10];
scanf("%s", buf);

Se o usuário digitar mais de 9 caracteres, há estouro. Versão segura:

C
char buf[10];
scanf("%9s", buf);

Ou use scanf_s em compiladores que suportam C11:

C
char buf[10];
scanf_s("%9s", buf, (unsigned)sizeof(buf));
⚠️ Atenção: 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:

C
#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:

C
#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

C
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

C
#define SQUARE(x) x * x
SQUARE(2 + 3)

Isso se expande para 2 + 3 * 2 + 3 = 11, não 25. Versão correta:

C
#define SQUARE(x) ((x) * (x))

Mas SQUARE(i++) ainda tem um problema (i é incrementado duas vezes). Uma função inline é mais segura.

== e =

C
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=address para ativá-lo. Ele pode detectar acesso fora dos limites, use-after-free, vazamentos de memória e outros problemas.

📖 Resumo

📝 Exercícios

  1. 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
  2. Escreva um programa que cause intencionalmente acesso fora dos limites e estouro de inteiro, compile e execute com AddressSanitizer, e observe a saída
  3. 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);
100%