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:
Common allocation patterns:
- •
allocator.alloc()-> requiresallocator.free() - •
allocator.create()-> requiresallocator.destroy() - •
Type.init(allocator)-> requiresinstance.deinit() - •
ArrayList.init()-> requireslist.deinit() - •
HashMap.init()-> requiresmap.deinit()
Proper cleanup pattern:
// GOOD: Immediate defer after allocation const buffer = try allocator.alloc(u8, 100); defer allocator.free(buffer); const instance = try allocator.create(MyType); defer allocator.destroy(instance); var list = ArrayList(u8).init(allocator); defer list.deinit();
Missing cleanup:
// BAD: No cleanup const buffer = try allocator.alloc(u8, 100); // Missing: defer allocator.free(buffer) const instance = MyType.init(allocator); // Missing: defer instance.deinit()
2. Test Function Cleanup
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:
Required pattern:
pub const MyType = struct {
allocator: Allocator,
buffer: []u8,
pub fn init(allocator: Allocator, size: usize) !MyType {
const buffer = try allocator.alloc(u8, size);
return MyType{
.allocator = allocator,
.buffer = buffer,
};
}
pub fn deinit(self: *MyType) void {
self.allocator.free(self.buffer);
self.* = undefined; // Safety: prevent use-after-free
}
};
Usage:
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
deferstatement? - •Is the
deferimmediately after the allocation? - •Does the cleanup happen on all exit paths?
- •Is there a corresponding
- •
For every test:
- •Does it use
std.testing.allocator? - •Are all allocated resources freed before test ends?
- •Would this test pass under leak detection?
- •Does it use
- •
For every struct with resources:
- •Does it have a
deinit()method? - •Does
deinit()free all owned resources? - •Is ownership clearly documented?
- •Does it have a
- •
For complex functions:
- •Should this use an arena allocator?
- •Are error paths handled correctly?
- •Is
errdeferused 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/inithas correspondingfree/destroy/deinit - •
deferstatement immediately follows allocation - • Test functions clean up all resources
- • Error paths handled with
errdefer - • Consider arena allocator for temporary allocations
- • Document ownership in comments
- • Run tests with leak detection
When reviewing code:
- • Check for missing
deferstatements - • Verify test cleanup
- • Look for allocations in loops
- • Check error path cleanup
- • Verify struct
deinitimplementations - • Run tests to confirm no leaks