Buffer Overrun — Como Resolver em Zig
O Que Este Erro Significa
O buffer overrun (ou buffer overflow) ocorre quando o programa escreve ou lê dados além dos limites de um buffer alocado. Em Zig, builds Debug e ReleaseSafe detectam esses acessos e emitem um panic ao invés de permitir corrupção de memória silenciosa.
A mensagem de panic:
thread 1 panic: index out of bounds
Ou, em caso de estouro de escrita detectado pelo alocador:
error(gpa): buffer overrun detected
Buffer overruns são uma das classes mais perigosas de bugs de segurança — em C, são a causa de inúmeras vulnerabilidades. Zig os previne com verificações automáticas de limites.
Causas Comuns
1. Indexação Além do Tamanho do Array
pub fn main() void {
var buffer: [10]u8 = undefined;
buffer[10] = 42; // PANIC: índice 10 em array de tamanho 10 (0..9)
}
2. Loop com Condição de Parada Incorreta
pub fn main() void {
var buffer: [5]u8 = undefined;
var i: usize = 0;
while (i <= 5) : (i += 1) { // <= ao invés de <
buffer[i] = @intCast(i); // PANIC quando i == 5
}
}
3. Cópia com Tamanho Incorreto
const std = @import("std");
pub fn main() void {
var destino: [5]u8 = undefined;
const origem = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// PANIC: origem é maior que destino
@memcpy(&destino, &origem);
}
4. Buffer Fixo Recebendo Dados de Tamanho Variável
const std = @import("std");
pub fn main() !void {
var buffer: [64]u8 = undefined;
const stdin = std.io.getStdIn().reader();
// Se o usuário digitar mais de 64 caracteres...
const line = try stdin.readUntilDelimiterOrEof(&buffer, '\n');
// readUntilDelimiter retorna erro se o buffer for pequeno demais
_ = line;
}
5. Escrever Além do Final de Slice Alocado
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const buffer = try allocator.alloc(u8, 10);
defer allocator.free(buffer);
// PANIC: escrevendo além do buffer de 10 bytes
buffer[15] = 0xFF;
}
6. Formatação em Buffer Insuficiente
const std = @import("std");
pub fn main() void {
var buf: [5]u8 = undefined;
// "valor: 12345" precisa de mais de 5 bytes
const resultado = std.fmt.bufPrint(&buf, "valor: {d}", .{@as(u32, 12345)});
// Retorna erro NoSpaceLeft, mas se ignorado...
_ = resultado;
}
Como Corrigir
Solucao 1: Verificar Limites Antes de Acessar
const std = @import("std");
pub fn main() void {
var buffer: [10]u8 = undefined;
const indice: usize = 15;
if (indice < buffer.len) {
buffer[indice] = 42;
} else {
std.debug.print("Índice {} fora dos limites (max: {})\n", .{ indice, buffer.len - 1 });
}
}
Solucao 2: Usar Loops for Idiomáticos
pub fn main() void {
var buffer: [5]u8 = undefined;
// CORRETO: for itera exatamente sobre os índices válidos
for (&buffer, 0..) |*byte, i| {
byte.* = @intCast(i);
}
}
Solucao 3: Usar @min para Limitar Tamanho
const std = @import("std");
fn copiarSeguro(destino: []u8, origem: []const u8) usize {
const tamanho = @min(destino.len, origem.len);
@memcpy(destino[0..tamanho], origem[0..tamanho]);
return tamanho;
}
pub fn main() void {
var destino: [5]u8 = undefined;
const origem = "dados muito longos para o buffer";
const copiados = copiarSeguro(&destino, origem);
std.debug.print("Copiados {} bytes\n", .{copiados});
}
Solucao 4: Usar ArrayList para Tamanho Dinâmico
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var buffer = std.ArrayList(u8).init(allocator);
defer buffer.deinit();
// ArrayList cresce automaticamente — sem buffer overrun
try buffer.appendSlice("dados que podem ser de qualquer tamanho");
try buffer.appendSlice(" e mais dados aqui");
std.debug.print("Total: {} bytes\n", .{buffer.items.len});
}
Solucao 5: Alocar Buffer de Tamanho Adequado
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const dados = "texto de entrada";
// Alocar exatamente o tamanho necessário
const buffer = try allocator.alloc(u8, dados.len);
defer allocator.free(buffer);
@memcpy(buffer, dados);
}
Solucao 6: Usar bufPrint com Verificação de Erro
const std = @import("std");
pub fn main() void {
var buf: [100]u8 = undefined; // Buffer generoso
const resultado = std.fmt.bufPrint(&buf, "valor: {d}, nome: {s}", .{
@as(u32, 42),
"Zig Brasil",
}) catch |err| {
std.debug.print("Buffer muito pequeno: {}\n", .{err});
return;
};
std.debug.print("{s}\n", .{resultado});
}
Slicing Seguro
Zig oferece slicing com verificação automática de limites:
pub fn main() void {
const dados = [_]u8{ 1, 2, 3, 4, 5 };
// Slicing com verificação
const parte = dados[0..3]; // OK: [0, 1, 2]
_ = parte;
// PANIC: 6 > dados.len (5)
// const invalido = dados[0..6];
}
Diferenca entre Build Modes
| Modo | Verificação de Limites | Comportamento |
|---|---|---|
| Debug | Sim | Panic com stack trace |
| ReleaseSafe | Sim | Panic com stack trace |
| ReleaseFast | Nao | Comportamento indefinido |
| ReleaseSmall | Nao | Comportamento indefinido |
Sempre desenvolva em Debug e teste em ReleaseSafe antes de usar ReleaseFast.
Padrão Seguro para Buffers
const std = @import("std");
const BufferError = error{BufferCheio};
const SafeBuffer = struct {
dados: []u8,
posicao: usize = 0,
fn escrever(self: *SafeBuffer, byte: u8) BufferError!void {
if (self.posicao >= self.dados.len) return BufferError.BufferCheio;
self.dados[self.posicao] = byte;
self.posicao += 1;
}
fn espacoRestante(self: *const SafeBuffer) usize {
return self.dados.len - self.posicao;
}
};
Erros Relacionados
- Index out of bounds — Acesso fora dos limites de array
- Segmentation fault — Consequência em builds sem verificação
- Stack overflow — Buffer grande demais na stack
- OutOfMemory — Memória insuficiente para buffer maior