Double Free — Como Resolver em Zig
O Que Este Erro Significa
O erro Double Free ocorre quando o programa tenta liberar (chamar free) a mesma região de memória duas vezes. Após o primeiro free, a memória volta ao pool do alocador e pode ser atribuída a outra alocação. Liberar novamente pode corromper as estruturas internas do alocador, causar crashes ou, pior, criar vulnerabilidades de segurança.
Em Zig, o GeneralPurposeAllocator detecta double frees e emite um panic:
thread 1 panic: Double free detected
Este é um dos erros de memória mais perigosos porque pode levar a corrupção silenciosa de dados.
Causas Comuns
1. Free Explícito Junto com defer
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 via defer
// ... uso de dados ...
allocator.free(dados); // ERRO: Double free!
// O defer vai tentar liberar novamente ao final do escopo
}
2. Dois Caminhos de Código que Liberam a Mesma Memória
const std = @import("std");
fn processar(allocator: std.mem.Allocator, dados: []u8) void {
// Libera se encontrar problema
if (dados.len < 10) {
allocator.free(dados);
return;
}
// ... processamento ...
allocator.free(dados); // OK aqui, mas se dados.len < 10 já foi liberado!
}
3. Struct com Método deinit Chamado Duas Vezes
const std = @import("std");
const Buffer = struct {
dados: []u8,
allocator: std.mem.Allocator,
fn deinit(self: *Buffer) void {
self.allocator.free(self.dados);
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var buf = Buffer{
.dados = try allocator.alloc(u8, 100),
.allocator = allocator,
};
buf.deinit();
buf.deinit(); // ERRO: Double free!
}
4. Compartilhar Referência sem Propriedade Clara
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);
var ref1 = dados;
var ref2 = dados; // Ambas apontam para a mesma memória
_ = ref1;
allocator.free(ref1);
allocator.free(ref2); // ERRO: Double free — mesma memória
_ = ref2;
}
5. Free em Tratamento de Erro e no Caminho Normal
const std = @import("std");
fn inicializar(allocator: std.mem.Allocator) ![]u8 {
const buf = try allocator.alloc(u8, 100);
// Se falhar aqui, libera
const resultado = fazerAlgo(buf) catch {
allocator.free(buf);
return error.FalhaInicializacao;
};
_ = resultado;
return buf;
}
fn fazerAlgo(buf: []u8) !void {
_ = buf;
return error.Falha;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const buf = try inicializar(allocator);
allocator.free(buf);
}
Como Corrigir
Solucao 1: Usar defer Consistentemente
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); // Único ponto de liberação
// Use dados sem se preocupar com free
dados[0] = 42;
// defer cuida do free automaticamente
}
Solucao 2: Propriedade Clara de Memória
const std = @import("std");
const Buffer = struct {
dados: ?[]u8,
allocator: std.mem.Allocator,
fn init(allocator: std.mem.Allocator, tamanho: usize) !Buffer {
return .{
.dados = try allocator.alloc(u8, tamanho),
.allocator = allocator,
};
}
fn deinit(self: *Buffer) void {
if (self.dados) |d| {
self.allocator.free(d);
self.dados = null; // Previne double free
}
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var buf = try Buffer.init(allocator, 100);
defer buf.deinit();
buf.deinit(); // Seguro: segunda chamada não faz nada
}
Solucao 3: errdefer para Limpeza em Caso de Erro
const std = @import("std");
fn inicializar(allocator: std.mem.Allocator) ![]u8 {
const buf = try allocator.alloc(u8, 100);
errdefer allocator.free(buf); // Libera SÓ se houver erro
try fazerAlgo(buf); // Se falhar, errdefer libera buf
return buf; // Se sucesso, quem chamou é responsável pelo free
}
fn fazerAlgo(buf: []u8) !void {
_ = buf;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const buf = try inicializar(allocator);
defer allocator.free(buf); // Único free no caminho de sucesso
_ = buf;
}
Solucao 4: ArenaAllocator Elimina Free Individual
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(); // Único ponto de liberação
const allocator = arena.allocator();
// Sem free individual — impossível ter double free
const a = try allocator.alloc(u8, 100);
const b = try allocator.alloc(u8, 200);
_ = a;
_ = b;
}
Solucao 5: Documentar Propriedade nos Tipos
const std = @import("std");
const Recurso = struct {
/// Memória pertence a esta struct. Chamar deinit() para liberar.
dados: []u8,
allocator: std.mem.Allocator,
liberado: bool = false,
fn deinit(self: *Recurso) void {
if (!self.liberado) {
self.allocator.free(self.dados);
self.liberado = true;
}
}
};
Padrão Recomendado: errdefer + defer
O padrão mais seguro em Zig para evitar double free e vazamentos é combinar errdefer e defer:
const std = @import("std");
fn criarRecurso(allocator: std.mem.Allocator) ![]u8 {
const a = try allocator.alloc(u8, 100);
errdefer allocator.free(a); // Libera se erro abaixo
const b = try allocator.alloc(u8, 200);
errdefer allocator.free(b); // Libera se erro abaixo
try validar(a, b);
return a; // Propriedade transferida — sem double free
}
fn validar(a: []u8, b: []u8) !void {
_ = a;
_ = b;
}
Erros Relacionados
- Use after free — Acessar memória após liberação
- Invalid free — Liberar ponteiro que não foi alocado
- Memory leak detected — Esquecer de liberar memória
- OutOfMemory — Memória insuficiente