Search code examples
zig

Zig `Segmentation fault at address` in Loop When Allocating and Copying Struct


I am trying to read babel options.json file from directory, but encountering a segmentation fault in the code when attempting to allocate a struct within an inner loop and then copy that struct to another location during the final iteration of the loop.

const std = @import("std");
const fs = std.fs;
const Allocator = std.mem.Allocator;

const string = []const u8;
const BabelOptions = struct {
    source_type: ?string = null,
    throws: ?string = null,
    plugins: ?[]string = null,

    pub fn from_test_path(path: string, allocator: Allocator) !*BabelOptions {
        var options = try allocator.create(BabelOptions);
        options.* = .{};

        var dir = try std.fs.cwd().openDir(path, .{});
        defer dir.close();

        for (0..3) |_| {
            const realpath = try dir.realpathAlloc(allocator, ".");
            defer allocator.free(realpath);

            const file: ?std.fs.File = dir.openFile("options.json", .{}) catch null;

            if (file == null) {
                std.debug.print("No options.json found in {s}\n", .{realpath});
                continue;
            }
            const new_json = try readJsonFile(BabelOptions, file.?, allocator);
            defer new_json.deinit();

            const _new_json = new_json.value;

            if (options.source_type == null and _new_json.source_type != null) {
                options.source_type = _new_json.source_type;
            }
            if (options.throws == null and _new_json.throws != null) {
                options.throws = _new_json.throws;
            }
            if (options.plugins == null) {
                options.plugins = _new_json.plugins;
            } else if (_new_json.plugins) |plugins| {
                var list = std.ArrayList([]const u8).init(allocator);
                try list.appendSlice(options.plugins.?);
                try list.appendSlice(plugins);
                const new_plugins = try list.toOwnedSlice();
                options.plugins = new_plugins;
            }
            dir = try dir.openDir("..", .{});
        }
        return options;
    }
};

fn readJsonFile(T: type, file: std.fs.File, allocator: Allocator) !std.json.Parsed(T) {
    const file_size = try file.getEndPos();
    const source = try allocator.alloc(u8, file_size);
    _ = try file.readAll(source);
    defer allocator.free(source);

    return try std.json.parseFromSlice(T, allocator, source, .{ .ignore_unknown_fields = true, .allocate = .alloc_always });
}

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

    const path = "src/test/fixtures/babel/t0/t1/t2";
    const options =
        try BabelOptions.from_test_path(path, allocator);
    defer allocator.destroy(options);

    std.log.debug("source_type: {any}", .{options.source_type});
}

The error message I'm receiving is:

PS D:\project> zig build-exe .\src\test\main.zig
PS D:\project> .\main.exe
Segmentation fault at address 0x1e3c0cb0010

It seems like the segmentation fault might be due to memory being freed or accessed improperly. I'm not entirely sure where the issue lies, but it likely involves how memory is being managed within the loop.

I’m new to Zig and don’t have much experience with languages that don’t have garbage collection. Could someone help me identify where the problem is and how to fix it? Also, what would be a good approach to debug similar issues in the future?

Thanks in advance!


Solution

  • Your mistake is that you're storing the strings from the JSON in the result, but those strings are freed along with the JSON.

    For example:

    options.source_type = _new_json.source_type;
    

    This should be:

    options.source_type = try allocator.dupe(u8, _new_json.source_type);
    

    You will need to free not only options, but also individual strings from the options. You can add a function to your BabelOptions to do this.

    And I would recommend:

    • Just return a struct from the from_test_path function instead of returning a pointer to the struct. Returning a pointer limits what you can do with the struct.
    • Add errdefer after every allocation that does not otherwise have defer.
    • Use only one ArrayList for plugins instead of allocating multiple in the if.
    • In the readJsonFile, the defer for source should appear right after the allocation, not after the read. Otherwise, if the read fails, the source won't be freed.
    • Check for memory leaks with defer std.debug.assert(gpa.deinit() == .ok);

    It would look something like this:

    const std = @import("std");
    const fs = std.fs;
    const Allocator = std.mem.Allocator;
    
    const string = []const u8;
    const BabelOptions = struct {
        source_type: ?string = null,
        throws: ?string = null,
        plugins: ?[]string = null,
    
        pub fn free(self: BabelOptions, allocator: Allocator) void {
            if (self.source_type) |source_type| {
                allocator.free(source_type);
            }
            if (self.throws) |throws| {
                allocator.free(throws);
            }
            if (self.plugins) |plugins| {
                for (plugins) |plugin| {
                    allocator.free(plugin);
                }
                allocator.free(plugins);
            }
        }
    
        pub fn from_test_path(path: string, allocator: Allocator) !BabelOptions {
            var options: BabelOptions = .{};
            errdefer options.free(allocator);
            var plugin_list = std.ArrayList([]const u8).init(allocator);
            errdefer {
                for (plugin_list.items) |plugin| {
                    allocator.free(plugin);
                }
                plugin_list.deinit();
            }
    
            var dir = try std.fs.cwd().openDir(path, .{});
            defer dir.close();
    
            for (0..3) |_| {
                const realpath = try dir.realpathAlloc(allocator, ".");
                defer allocator.free(realpath);
                const file: ?std.fs.File = dir.openFile("options.json", .{}) catch null;
                if (file == null) {
                    std.debug.print("No options.json found in {s}\n", .{realpath});
                    continue;
                }
    
                const new_json = try readJsonFile(BabelOptions, file.?, allocator);
                defer new_json.deinit();
                const _new_json = new_json.value;
    
                if (options.source_type == null and _new_json.source_type != null) {
                    options.source_type = try allocator.dupe(u8, _new_json.source_type.?);
                }
                if (options.throws == null and _new_json.throws != null) {
                    options.throws = try allocator.dupe(u8, _new_json.throws.?);
                }
                if (_new_json.plugins) |plugins| {
                    for (plugins) |plugin| {
                        try plugin_list.append(try allocator.dupe(u8, plugin));
                    }
                }
                dir = try dir.openDir("..", .{});
            }
            options.plugins = try plugin_list.toOwnedSlice();
            return options;
        }
    };
    
    fn readJsonFile(T: type, file: std.fs.File, allocator: Allocator) !std.json.Parsed(T) {
        const file_size = try file.getEndPos();
        const source = try allocator.alloc(u8, file_size);
        defer allocator.free(source);
        _ = try file.readAll(source);
    
        return try std.json.parseFromSlice(T, allocator, source, .{ .ignore_unknown_fields = true, .allocate = .alloc_always });
    }
    
    pub fn main() !void {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer std.debug.assert(gpa.deinit() == .ok);
        const allocator = gpa.allocator();
    
        const path = "src/test/fixtures/babel/t0/t1/t2";
        const options = try BabelOptions.from_test_path(path, allocator);
        defer options.free(allocator);
    
        std.log.debug("source_type: {any}", .{options.source_type});
    }