| name | memory-leak-detector |
| description | 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:
Common allocation patterns:
allocator.alloc() -> requires allocator.free()
allocator.create() -> requires allocator.destroy()
Type.init(allocator) -> requires instance.deinit()
ArrayList.init() -> requires list.deinit()
HashMap.init() -> requires map.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
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:
When reviewing code: