Invalid free error with version 0.14 that did not appear in 0.13, for aligned memory that is assigned via `memcpy`

I am porting a small codebase from Zig 0.13 to Zig 0.14, and in doing so I have encountered a problem. I am unsure whether this is an expected error related to what I am doing or maybe a bug in the new version.

I need to initialize a structure with a field whose memory is aligned. The alignment is done in the initialization function for ease of use, where the user passes an arbitrary slice as a parameter. I create an aligned buffer (on the page size, for example) and then I memcpy. I add a deinitialization function. Here is a minimal reproducible code snippet:

const std = @import("std");

const page_size = std.heap.pageSize();

const MyStruct = struct {
    allocator: std.mem.Allocator,
    bytes: []u8,

    pub fn init(allocator: std.mem.Allocator, bytes: []const u8) !@This() {
        const buffer align(page_size) = try allocator.alignedAlloc(u8, page_size, bytes.len);
        @memcpy(buffer[0..bytes.len], bytes);

        return .{
            .allocator = allocator,
            .bytes = buffer,
        };
    }

    pub fn deinit(self: *@This()) void {
        self.allocator.free(self.bytes);
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var myStruct = try MyStruct.init(allocator, &[_]u8{ 1, 2, 3, 4 });
    defer myStruct.deinit();
}

This program compiles and runs correctly with Zig 0.13 (you just need to change std.heap.pageSize() to std.mem.page_size). In Zig 0.14, this program compiles correctly, but at runtime, there is an error during deinitialization:

thread 514165 panic: Invalid free
<zig-cache>/lib/std/heap/debug_allocator.zig:870:49: 0x10e2857 in free (myProgram)
            if (bucket.canary != config.canary) @panic("Invalid free");
                                                ^
<zig-cache>/lib/std/mem/Allocator.zig:147:25: 0x107029b in free__anon_10944 (myProgram)
    return a.vtable.free(a.ptr, memory, alignment, ret_addr);
                        ^
<project-path>/src/main.zig:20:28: 0x10e0993 in deinit (myProgram)
        self.allocator.free(self.bytes);
                           ^
<project-path>/src/main.zig:31:26: 0x10e0854 in main (myProgram)
    defer myStruct.deinit();
                         ^
<zig-cache>/lib/std/start.zig:656:37: 0x10e01ca in posixCallMainAndExit (myProgram)
            const result = root.main() catch |err| {
                                    ^
<zig-cache>/lib/std/start.zig:271:5: 0x10dfd7d in _start (myProgram)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)

I found a solution to the problem. Just replace free with rawFree:

    pub fn deinit(self: *@This()) void {
        self.allocator.rawFree(self.bytes, std.mem.Alignment.fromByteUnits(page_size), @returnAddress());
    }

By doing this, the behavior is similar to what happened in Zig 0.13, so it seems to work.

However, I’m not sure if this is the right way to do it, mainly because rawFree should not be used outside of an allocator implementation and also because I am not sure about the parameters I passed to it. This leads me to believe that it is potentially a bug, because free is what should work, right?. However, I am not very advanced in Zig yet, so I am here to ask for your opinion and information on the right way to do this. I know that in 0.14 @memcpy has changed, so I think it is possible that my implementation is not fully compliant with the changes made (but I don’t know how), hence the error.

Thanks!

Missing alignment of the field?

bytes: []align(page_size) u8,

That does indeed solve the initial problem. I don’t remember seeing that documented anywhere, but maybe I didn’t look hard enough. I hadn’t thought of doing that either, though.

Thanks!

This doesn’t do anything different than:

const buffer = 

If you wanted to declare a slice with an alignment you would have to do this:

const buffer: []align(page_size) const u8 = 

But this isn’t necessary, because the called function already returns the slice with that alignment.


This @memcpy(buffer[0..bytes.len], bytes); is equivalent to @memcpy(buffer, bytes); because you asked alignedAlloc to give you a slice of length bytes.len.


The main problem with the code is that you don’t call free with the same pointer you received from alignedAlloc, one way you could do that is to declare the bytes field with the correct alignment so that it doesn’t get lost by the time free is called:

    bytes: []align(page_size) u8,

    pub fn init(allocator: std.mem.Allocator, bytes: []const u8) !@This() {
        const buffer = try allocator.alignedAlloc(u8, page_size, bytes.len);
        @memcpy(buffer, bytes);
        return .{
            .allocator = allocator,
            .bytes = buffer,
        };
    }

However this only works on targets that have a compile time known pageSize().
Since:

Zig supports runtime known page sizes.

To make it also work there, we need a function that can dispatch between runtime known alignments, I tried to adapt a solution from an older topic to recent Zig here:

Everything is clearer now. Thank you for these informations.

1 Like