Dangling Pointer em Zig — O que é e Como Evitar

Dangling Pointer em Zig — O que é e Como Evitar

Definição

Um dangling pointer (ponteiro pendente) é um ponteiro que referencia uma posição de memória que já foi liberada ou não é mais válida. Desreferenciar um dangling pointer é comportamento indefinido: o programa pode crashar, retornar lixo ou, pior, parecer funcionar corretamente enquanto corrompe dados silenciosamente.

Em Zig, dangling pointers podem surgir de três formas principais: use-after-free (usar após liberar), retorno de ponteiro para variável local e invalidação de ponteiro por realocação.

Por que Dangling Pointers Importam

  1. Bugs catastróficos: Podem causar crashes aleatórios, corrupção de dados e vulnerabilidades de segurança.
  2. Difícil reprodução: O comportamento depende do estado da memória — pode funcionar em dev e falhar em produção.
  3. Segurança: Dangling pointers são a base de muitos exploits de segurança (use-after-free).
  4. Sem garbage collector: Em Zig, o programador é responsável por evitá-los.

Exemplo Prático

Use-After-Free

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const dados = try allocator.alloc(u8, 100);
    allocator.free(dados);

    // PERIGO: dados agora é um dangling pointer!
    // dados[0] = 'X'; // Comportamento indefinido!
    // Em Debug, GPA detecta e causa panic
}

Ponteiro para Variável Local

fn perigoso() *u32 {
    var local: u32 = 42;
    return &local; // ERRO: local é destruída ao sair da função!
}

// Correto: retornar o valor, não o ponteiro
fn seguro() u32 {
    var local: u32 = 42;
    return local; // Cópia do valor
}

// Ou: alocar no heap
fn seguro_heap(allocator: std.mem.Allocator) !*u32 {
    const ptr = try allocator.create(u32);
    ptr.* = 42;
    return ptr; // Dados vivem no heap
}

Invalidação por Realocação

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var lista = std.ArrayList(u32).init(gpa.allocator());
    defer lista.deinit();

    try lista.append(10);
    try lista.append(20);

    // Pegamos um ponteiro para o buffer interno
    const ptr = &lista.items[0];

    // Append pode realocar o buffer interno!
    try lista.ensureTotalCapacity(1000);

    // PERIGO: ptr pode ser dangling se houve realocação!
    // _ = ptr.*; // Potencialmente inválido
    _ = ptr;
}

Slice de Array Local

fn perigoso2() []const u8 {
    var buffer: [100]u8 = undefined;
    @memcpy(buffer[0..5], "Hello");
    return buffer[0..5]; // PERIGO: buffer é destruído!
}

fn seguro2(allocator: std.mem.Allocator) ![]u8 {
    const buffer = try allocator.alloc(u8, 5);
    @memcpy(buffer, "Hello");
    return buffer; // OK: memória vive no heap
}

Proteções do Zig

ProteçãoModoEfeito
GPA use-after-freeDebugPanic ao acessar memória liberada
GPA double-freeDebugPanic ao liberar duas vezes
CompiladorTodosErro ao retornar ponteiro para local
Bounds checkingDebug/ReleaseSafePanic em acesso fora dos limites

Como Prevenir

  1. Use defer: Libere recursos no final do escopo, não antes.
  2. Não retenha ponteiros para internos de coleções: Após append, resize ou ensureCapacity, ponteiros antigos podem ser inválidos.
  3. Prefira valores a ponteiros: Retorne cópias quando os dados são pequenos.
  4. Use arena allocator: Quando todos os dados têm o mesmo tempo de vida.
  5. Teste com GPA: O GeneralPurposeAllocator detecta muitos casos de dangling pointer.

Armadilhas Comuns

  • Funciona em Debug, falha em Release: O GPA preenche memória liberada com padrão detectável. Em Release, a memória pode ainda conter os dados antigos, mascarando o bug.
  • Slices são ponteiros: Um []u8 é um ponteiro + length. Se a memória apontada for liberada, o slice é dangling.
  • Closures com ponteiros locais: Capturar &variavel_local em um contexto que sobrevive ao escopo cria dangling pointer.
  • Confundir com null pointer: Dangling pointers têm endereço “válido” (não-zero), mas apontam para memória inválida. ?*T (optional pointer) resolve o problema de null, não de dangling.

Termos Relacionados

  • Memory Leak — Memória alocada nunca liberada
  • Allocator — Interface de gerenciamento de memória
  • Defer — Limpeza garantida ao sair do escopo
  • Slice — Referência a sequência de memória
  • Stack vs Heap — Tempo de vida da memória

Tutoriais Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.