Why does my code leak?

I want to pass a texture path to a C function:

fn addSentinelToStr(allocator: std.mem.Allocator, str: []u8) ![:0]u8 {
    const buf = try allocator.allocSentinel(u8, str.len, 0);
    @memcpy(buf, str);
    return buf;
}

fn joinPathSentinel(allocator: std.mem.Allocator, paths: []const []const u8) ![]u8 {
    const joined = try std.fs.path.join(allocator, paths);
    //defer allocator.free(joined);
    //return (try addSentinelToStr(allocator, joined));
    const res = try addSentinelToStr(allocator, joined);
    allocator.free(joined);
    return (res);
}

main () {
    [...]
    var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init;
    const allocator = general_purpose_allocator.allocator();

    const current_exe_dir: []u8 = try std.fs.selfExeDirPathAlloc(allocator);
    defer allocator.free(current_exe_dir);

    const texture_path = try joinPathSentinel(allocator, &.{current_exe_dir, "textures/container.jpg"});
    defer allocator.free(texture_path);
    [...]
}

It seems to me like I free everything correctly, but this is what I get:

error(gpa): Allocation size 89 bytes does not match free size 88. Allocation:
...\triangle.zig:53:44: 0x922337 in addSentinelToStr (triangle.exe.obj)
    const buf = try allocator.allocSentinel(u8, str.len, 0);
                                           ^
...\triangle.zig:62:37: 0x923efe in joinPathSentinel (triangle.exe.obj)
    const res = try addSentinelToStr(allocator, joined);
                                    ^
...\triangle.zig:459:46: 0x925ef5 in main (triangle.exe.obj)
    const texture_path = try joinPathSentinel(allocator, &.{current_exe_dir, "textures/container.jpg"});
                                             ^
...\lib\std\start.zig:590:75: 0x92901a in main (triangle.exe.obj)
    return callMainWithArgs(@as(usize, @intCast(c_argc)), @as([*][*:0]u8, @ptrCast(c_argv)), envp);
                                                                          ^
...\lib\libc\mingw\crt\crtexe.c:267:0: 0x9c0b70 in __tmainCRTStartup (crt2.obj)
    mainret = _tmain (argc, argv, envp);

Would someone know how I lose that 1 byte? I suspect it’s the sentinel? But why?

Not sure, but note that addSentinelToStr() is just reimplementing std.mem.Allocator.dupeZ() and joinPathSentinel() is just reimplementing std.fs.path.joinZ().

1 Like

Your code looks good, if between these calls there is no memory corruption then this is an allocator bug.

  • You can call joinZ instead of join to get a zero terminated string.
  • You can call dupeZ instead of allocSentinel and @memcpy.
  • Don’t forget to deinit the general_purpose_allocator.

Oh, I found the sneaky bug in your code…

fn joinPathSentinel(allocator: std.mem.Allocator, paths: []const []const u8) ![]u8 {

Notice how the returned slice doesn’t have a sentinel. The std.mem.Allocator.free() code only frees the bytes from the sentinel element if the pointer type that’s passed to it actually has a sentinel.

Change the type of the returned slice to [:0]u8 and it should work again. (But, as stated above, you can save yourself the pain and use the stdlib methods).

5 Likes

Thank you guys a lot!

You can call X instead

Yeees, I read the docs a lot, but still sometimes reinvent the wheel :smile: