I am trying to figure out how to detect a double free. The following code crashes with a segmentation fault when I run the test. I would like it to fail the test with information about where the problem might be.
const std = @import("std");
fn allocate_memory(allocator: std.mem.Allocator) ![]u8 {
const memory = try allocator.alloc(u8, 100);
errdefer allocator.free(memory);
return memory;
}
fn memoryTest(allocator: std.mem.Allocator, result: usize) !void {
// Allocate 100 units of 8 bits (u8)
const memory = try allocate_memory(allocator);
defer allocator.free(memory);
// Comment in the following line to create a double free
defer allocator.free(memory);
try std.testing.expectEqual(@as(usize, result), memory.len);
}
test "allocation" {
// Select an allocator to use
const allocator = std.testing.allocator;
try std.testing.checkAllAllocationFailures(
allocator,
memoryTest,
.{ 100 },
);
}
Can anyone help me write a test that reports a double free?
My next challenge will be a test for use after free.
std.mem.Allocator.free before actually freeing the memory, sets the bytes to undefined.
When the second free happens it tries to write to the freed memory and a segmentation fault happens, because this is a use after free.
Thanks for your response and you comment on the issue 4298.
My goal is to see if I can get a test to detect a double free. If I can’t then I will open an issue in the issue tracker. There should be good test tooling for
Use after free is hard because it exists outside of the context of the allocator itself. You’d have to store some state that says whether or not something has been freed and then check onsite that the thing is being used. Unfortunately, that’s very hard for raw slices/pointers. What checks the following?
_ = slice[i]; // slice has already been freed
You’d have to wrap it with an accessor that does some sort of stateful debug solution…
inline pub fn access(slice: anytype, index: usize) std.meta.Child(@TypeOf(slice)) {
if (debug) { // you can get this info in @import("builtin")
// do your check here
}
return slice[i];
}
// later...
const x = access(slice, i);
You’d need a setter for this approach too (or return a pointer to the element)… not very ergonomic but you could do checks with this.
Like @dimdin pointed out, if you’re checking for a double free at the time you’re passing the slice to the allocator, it’s too late. That’s a known issue with the allocator interface as was mentioned.
A segmentation fault is generated with a stack trace from the location that used the free memory.
Segmentation fault at address 0x7f21e1e1c000
/home/din/test.zig:10:8: 0x103642a in use_after_free (test)
buf[0] = 0;
^
/home/din/test.zig:30:23: 0x10369cc in main (test)
try use_after_free(allocator);
^
/home/din/zig-0.12.0/lib/std/start.zig:511:37: 0x10362c5 in posixCallMainAndExit (test)
const result = root.main() catch |err| {
^
/home/din/zig-0.12.0/lib/std/start.zig:253:5: 0x1035de1 in _start (test)
asm volatile (switch (native_arch) {
^
The normal double_free case is using allocator.free(buf); twice; it is also a segmentation fault that gives a stack trace.
When calling rawFree:
thread 765670 panic: Invalid free
/home/din/zig-0.12.0/lib/std/heap/general_purpose_allocator.zig:643:21: 0x106c07c in freeLarge (test)
@panic("Invalid free");
^
/home/din/zig-0.12.0/lib/std/heap/general_purpose_allocator.zig:854:31: 0x103b050 in free (test)
self.freeLarge(old_mem, log2_old_align, ret_addr);
^
/home/din/zig-0.12.0/lib/std/mem/Allocator.zig:98:28: 0x1036492 in double_free (test)
return self.vtable.free(self.ptr, buf, log2_buf_align, ret_addr);
^
/home/din/test.zig:31:20: 0x10369dc in main (test)
try double_free(allocator);
^
/home/din/zig-0.12.0/lib/std/start.zig:511:37: 0x10362c5 in posixCallMainAndExit (test)
const result = root.main() catch |err| {
^
/home/din/zig-0.12.0/lib/std/start.zig:253:5: 0x1035de1 in _start (test)
asm volatile (switch (native_arch) {
^
a panic “Invalid free” with a stack trace showing the double_free call. Note: You must not call rawFree! allocator.free is the correct API. I am using rawFree to demonstrate that zig allocator knows about the double free, but currently it is not working correctly.
I think taht if you try all these within a test "foo" { ... } block using the std.testing.allocator it should behave the same since that’s just a pre-configured instance of the GPA, no?
Yes, std.testing.allocator is an instance of GeneralPurposeAllocator usable only when testing.
Actual source in std.testing:
pub const allocator = allocator_instance.allocator()
pub var allocator_instance = b: {
if (!builtin.is_test)
@compileError("Cannot use testing allocator outside of test block");
break :b std.heap.GeneralPurposeAllocator(.{}){};
}
Thanks for your response and your code and verifying that both double frees and use after free create segmentation faults. I think detection should be added to the tooling because they are difficult bugs to track down. AndrewCodeDev’s idea should work, but is not built in and not ergonomic, which means it is not a general purpose solution.
Also note that zig integrates with valgrind.
Zig by default sends valgrind client requests to annotate memory marked as undefined (undefined=0xAA=alternating 0 and 1 bits).