Use After Free — Como Resolver em Zig

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

  1. Quem aloca, libera — ou documente explicitamente a transferência de propriedade.
  2. Use defer imediatamente após alocar para garantir que o free aconteça.
  3. Nunca guarde ponteiros para memória que outro código pode liberar.
  4. Evite ponteiros internos para estruturas que podem realocar (como ArrayList).
  5. Use ArenaAllocator quando múltiplas alocações compartilham o mesmo ciclo de vida.

Erros Relacionados

Continue aprendendo Zig

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