Detects potential memory leaks in Zig code by checking allocation/deallocation patterns, defer statements, and test cleanup. Use when reviewing code changes, writing new code with allocations, or debugging memory issues.
Detects potential memory leaks in Zig code by checking allocation/deallocation patterns, defer statements, and test cleanup. Use when reviewing code changes, writing new code with allocations, or debugging memory issues.
Memory Leak Detector
This skill helps identify potential memory leaks in Zig code by analyzing allocation patterns and ensuring proper cleanup.
What This Skill Checks
1. Allocation/Deallocation Pairing
Every allocation must have a corresponding deallocation:
All test functions must clean up allocated resources:
Proper test cleanup:
test "my feature" {
const allocator = std.testing.allocator;
var server = try Server.init(allocator);
defer server.deinit(); // Cleanup before test ends
const value = try Value.create(allocator, "test");
defer value.deinit(); // Cleanup allocated value
// Test logic here
}
Missing test cleanup:
test "my feature" {
const allocator = std.testing.allocator;
var server = try Server.init(allocator);
// Missing: defer server.deinit()
const value = try Value.create(allocator, data);
// Missing: defer value.deinit()
// Test will leak memory!
}
3. Conditional Allocations
Handle cleanup in error paths:
Proper error path cleanup:
// GOOD: Arena allocator for automatic cleanup
var arena = std.heap.ArenaAllocator.init(parent_allocator);
defer arena.deinit();
const allocator = arena.allocator();
// All allocations automatically freed on arena.deinit()
Or manual error handling:
// GOOD: Manual cleanup on error
const a = try allocator.alloc(u8, 10);
errdefer allocator.free(a);
const b = try allocator.alloc(u8, 20);
errdefer allocator.free(b);
// If second allocation fails, first is cleaned up
Missing error cleanup:
// BAD: Leak on error
const a = try allocator.alloc(u8, 10);
const b = try allocator.alloc(u8, 20); // If this fails, 'a' leaks
defer allocator.free(a);
defer allocator.free(b);
4. Struct/Type Lifecycle
Types with resources must implement proper cleanup:
var instance = try MyType.init(allocator, 100);
defer instance.deinit(); // Required
5. Arena Allocator Usage
For temporary allocations, prefer arena allocators:
Recommended pattern:
pub fn processData(parent_allocator: Allocator) !Result {
var arena = std.heap.ArenaAllocator.init(parent_allocator);
defer arena.deinit(); // Single cleanup for all allocations
const allocator = arena.allocator();
// All temporary allocations use arena
const temp1 = try allocator.alloc(u8, 100);
const temp2 = try allocator.alloc(u8, 200);
// No need for individual defer statements
// All freed automatically on arena.deinit()
return result;
}
Red Flags to Watch For
1. Missing defer After Allocation
// RED FLAG
const data = try allocator.alloc(u8, size);
// Next line should be: defer allocator.free(data)
2. Allocation in Loop Without Cleanup
// RED FLAG: Allocates on every iteration
for (items) |item| {
const temp = try allocator.alloc(u8, item.size);
// Missing cleanup - leaks on each iteration
}
// BETTER: Use arena or defer inside loop
for (items) |item| {
const temp = try allocator.alloc(u8, item.size);
defer allocator.free(temp); // Cleanup each iteration
}
3. Early Returns Without Cleanup
// This is OK - defer runs on all exit paths
const data = try allocator.alloc(u8, 100);
defer allocator.free(data);
if (condition) {
return error.Failed; // defer will run
}
But watch for:
// ACTUAL RED FLAG
const data = try allocator.alloc(u8, 100);
if (condition) {
return error.Failed; // Leaks data
}
defer allocator.free(data); // Too late for early returns
4. Storing Allocations in Containers
// WATCH OUT: Who owns the memory?
var list = ArrayList(*MyType).init(allocator);
defer list.deinit(); // Only frees the list, not items
const item = try allocator.create(MyType);
try list.append(item); // Must free item separately
// PROPER CLEANUP
defer {
for (list.items) |item| {
allocator.destroy(item);
}
list.deinit();
}
When Reviewing Code
Questions to Ask:
For every allocation:
Is there a corresponding defer statement?
Is the defer immediately after the allocation?
Does the cleanup happen on all exit paths?
For every test:
Does it use std.testing.allocator?
Are all allocated resources freed before test ends?
Would this test pass under leak detection?
For every struct with resources:
Does it have a deinit() method?
Does deinit() free all owned resources?
Is ownership clearly documented?
For complex functions:
Should this use an arena allocator?
Are error paths handled correctly?
Is errdefer used appropriately?
Action Items When Leak Detected
Identify the allocation - What function allocates the memory?
Find the owner - Who is responsible for cleanup?
Add defer - Place it immediately after allocation
Verify all paths - Check error returns and early exits
Test - Run under leak detection to verify fix
Testing for Leaks
Zig's test allocator detects leaks automatically:
test "no leaks" {
const allocator = std.testing.allocator;
// If any allocation isn't freed, test fails
const data = try allocator.alloc(u8, 100);
defer allocator.free(data);
}
Run tests to detect leaks:
zig build test
Leaks will show as test failures with allocation tracking info.
Summary Checklist
When writing new code:
Every alloc/create/init has corresponding free/destroy/deinit
defer statement immediately follows allocation
Test functions clean up all resources
Error paths handled with errdefer
Consider arena allocator for temporary allocations