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
- Bugs catastróficos: Podem causar crashes aleatórios, corrupção de dados e vulnerabilidades de segurança.
- Difícil reprodução: O comportamento depende do estado da memória — pode funcionar em dev e falhar em produção.
- Segurança: Dangling pointers são a base de muitos exploits de segurança (use-after-free).
- 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ção | Modo | Efeito |
|---|---|---|
| GPA use-after-free | Debug | Panic ao acessar memória liberada |
| GPA double-free | Debug | Panic ao liberar duas vezes |
| Compilador | Todos | Erro ao retornar ponteiro para local |
| Bounds checking | Debug/ReleaseSafe | Panic em acesso fora dos limites |
Como Prevenir
- Use
defer: Libere recursos no final do escopo, não antes. - Não retenha ponteiros para internos de coleções: Após
append,resizeouensureCapacity, ponteiros antigos podem ser inválidos. - Prefira valores a ponteiros: Retorne cópias quando os dados são pequenos.
- Use arena allocator: Quando todos os dados têm o mesmo tempo de vida.
- Teste com GPA: O
GeneralPurposeAllocatordetecta 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_localem 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