Double Free — Como Resolver em Zig

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

Continue aprendendo Zig

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