Use After Free — Como Resolver em Zig
O Que Este Erro Significa
O erro Use After Free ocorre quando o programa tenta acessar (ler ou escrever) uma região de memória que já foi devolvida ao alocador com free. Após a liberação, aquela região pode ser reutilizada para outras alocações ou desmapeada pelo sistema operacional. Acessar essa memória resulta em comportamento indefinido: pode funcionar por acaso, retornar lixo, ou causar um Segmentation Fault.
Em Zig, o GeneralPurposeAllocator em modo Debug detecta este erro e emite um panic:
thread 1 panic: Use of freed memory
O GeneralPurposeAllocator preenche a memória liberada com um padrão especial (0xaa) justamente para facilitar a detecção deste problema.
Causas Comuns
1. Acessar Slice Após 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);
// ERRO: Use After Free — dados já foi liberado
dados[0] = 42;
}
2. Guardar Referência a Memória Liberada
const std = @import("std");
var referencia_global: []u8 = undefined;
fn criarDados(allocator: std.mem.Allocator) !void {
const dados = try allocator.alloc(u8, 100);
referencia_global = dados;
allocator.free(dados);
// referencia_global agora aponta para memória liberada!
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
try criarDados(gpa.allocator());
// ERRO: Use After Free via referência global
referencia_global[0] = 42;
}
3. Retornar Ponteiro para Memória Liberada
const std = @import("std");
fn obterBuffer(allocator: std.mem.Allocator) ![]u8 {
const buffer = try allocator.alloc(u8, 256);
// Faz algum processamento...
@memset(buffer, 0);
// BUG: libera antes de retornar
allocator.free(buffer);
return buffer; // Retorna ponteiro inválido!
}
4. Invalidação por Realocação de ArrayList
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var lista = std.ArrayList(u32).init(allocator);
defer lista.deinit();
try lista.append(1);
try lista.append(2);
// Salva ponteiro para os itens internos
const ptr = &lista.items[0];
// Muitos appends podem causar realocação interna
var i: u32 = 0;
while (i < 1000) : (i += 1) {
try lista.append(i);
}
// ERRO: ptr pode ser inválido após realocação
_ = ptr.*;
}
5. Captura de Slice em Closure/Callback
const std = @import("std");
fn registrarCallback(dados: []const u8) *const fn () void {
// dados pode ser liberado antes do callback ser executado
return struct {
fn callback() void {
// ERRO: dados pode já ter sido liberado
_ = dados;
}
}.callback;
}
Como Corrigir
Solucao 1: Usar defer para Gerenciar Ciclo de Vida
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);
defer allocator.free(dados); // Free acontece ao final do escopo
// Usa dados com segurança durante todo o escopo
dados[0] = 42;
dados[99] = 255;
// Ao sair de main, defer libera a memória
}
Solucao 2: Transferência Clara de Propriedade
const std = @import("std");
/// Quem chama esta função é responsável por liberar o resultado
fn criarDados(allocator: std.mem.Allocator) ![]u8 {
const dados = try allocator.alloc(u8, 100);
@memset(dados, 0);
return dados; // Transfere propriedade para quem chamou
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const dados = try criarDados(allocator);
defer allocator.free(dados); // Quem recebeu é responsável pelo free
dados[0] = 42;
}
Solucao 3: Evitar Ponteiros Internos de ArrayList
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var lista = std.ArrayList(u32).init(allocator);
defer lista.deinit();
// Pré-aloca para evitar realocações
try lista.ensureTotalCapacity(1000);
var i: u32 = 0;
while (i < 1000) : (i += 1) {
lista.appendAssumeCapacity(i);
}
// Agora é seguro acessar por índice
const valor = lista.items[0];
_ = valor;
}
Solucao 4: Usar ArenaAllocator para Ciclo de Vida Compartilhado
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
const allocator = arena.allocator();
// Todas as alocações vivem até arena.deinit()
const a = try allocator.alloc(u8, 100);
const b = try allocator.alloc(u8, 200);
// Seguro: ambos são válidos até o defer arena.deinit()
a[0] = 1;
b[0] = 2;
}
Solucao 5: Copiar Dados Quando Necessário
const std = @import("std");
fn copiarTexto(allocator: std.mem.Allocator, original: []const u8) ![]u8 {
const copia = try allocator.alloc(u8, original.len);
@memcpy(copia, original);
return copia; // Retorna cópia independente
}
Detecção com GeneralPurposeAllocator
O GPA detecta use-after-free automaticamente em builds Debug:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const buf = try allocator.alloc(u8, 10);
allocator.free(buf);
// O GPA detecta isso e emite panic com stack trace
buf[0] = 1;
}
Regras de Ouro
- Quem aloca, libera — ou documente explicitamente a transferência de propriedade.
- Use
deferimediatamente após alocar para garantir que o free aconteça. - Nunca guarde ponteiros para memória que outro código pode liberar.
- Evite ponteiros internos para estruturas que podem realocar (como ArrayList).
- Use ArenaAllocator quando múltiplas alocações compartilham o mesmo ciclo de vida.
Erros Relacionados
- Double free — Liberar a mesma memória duas vezes
- Segmentation fault — Acesso a memória inválida
- Memory leak detected — Memória nunca liberada
- Invalid free — Liberar ponteiro inválido