Seg faults using dir.Walker through interface

Hi all,

I’m trying to use std.fs.Dir.Walker through my Iterator interface but I run into a segfault every time on the second iteration. It segfaults in the walker and I don’t really know how to resolve it.

I’m developing this on Windows 11 with zig version 0.13.0, it might work on other operating systems but I haven’t tested that.

Any help here would be greatly appreciated!

Code
const std = @import("std");

pub fn main() !void {
    var buffer: [1000000]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const allocator = fba.allocator();

    const testPath = try std.process.getEnvVarOwned(allocator, "HOME");

    var fsStore = try FileSystemStore.init(testPath);
    var store = fsStore.store();
    var iter = try store.iterate(allocator);
    while (try iter.next()) |entry| {
        std.debug.print("{s}\n", .{entry.basename});
    }
}

const Store = struct {
    ptr: *anyopaque,
    iterateFn: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator) anyerror!Iterator,

    fn init(ptr: anytype) Store {
        const T = @TypeOf(ptr);
        const ptr_info = @typeInfo(T);

        if (ptr_info != .Pointer) @compileError("ptr must be a pointer");
        if (ptr_info.Pointer.size != .One) @compileError("ptr must be a single item pointer");

        const gen = struct {
            pub fn iterate(pointer: *anyopaque, allocator: std.mem.Allocator) anyerror!Iterator {
                const self: T = @ptrCast(@alignCast(pointer));
                return ptr_info.Pointer.child.iterate(self, allocator);
            }
        };

        return .{
            .ptr = ptr,
            .iterateFn = gen.iterate,
        };
    }

    fn iterate(self: Store, allocator: std.mem.Allocator) anyerror!Iterator {
        return self.iterateFn(self.ptr, allocator);
    }

    pub const Iterator = struct {
        ptr: *anyopaque,
        nextFn: *const fn (ptr: *anyopaque) anyerror!?std.fs.Dir.Walker.Entry,

        fn init(ptr: anytype) Iterator {
            const T = @TypeOf(ptr);
            const ptr_info = @typeInfo(T);

            if (ptr_info != .Pointer) @compileError("ptr must be a pointer");
            if (ptr_info.Pointer.size != .One) @compileError("ptr must be a single item pointer");

            const gen = struct {
                pub fn next(pointer: *anyopaque) anyerror!?std.fs.Dir.Walker.Entry {
                    const self: T = @ptrCast(@alignCast(pointer));
                    return ptr_info.Pointer.child.next(self);
                }
            };

            return .{
                .ptr = ptr,
                .nextFn = gen.next,
            };
        }

        fn next(self: Iterator) anyerror!?std.fs.Dir.Walker.Entry {
            return self.nextFn(self.ptr);
        }
    };
};

const FileSystemStore = struct {
    root: []const u8,

    pub fn init(root: []const u8) anyerror!FileSystemStore {
        std.fs.makeDirAbsolute(root) catch |err| switch (err) {
            error.PathAlreadyExists => {},
            else => return err,
        };
        return FileSystemStore{
            .root = root,
        };
    }

    pub fn store(self: *FileSystemStore) Store {
        return Store.init(self);
    }

    fn iterate(self: *FileSystemStore, allocator: std.mem.Allocator) !Store.Iterator {
        return Iterator.init(allocator, self.root);
    }

    fn deinit(self: *FileSystemStore) void {
        self.dir.close();
    }

    const Iterator = struct {
        walker: std.fs.Dir.Walker,
        dir: std.fs.Dir,

        fn init(allocator: std.mem.Allocator, path: []const u8) !Store.Iterator {
            var dir = try std.fs.openDirAbsolute(path, .{ .iterate = true });
            var iter = Iterator{
                .walker = try dir.walk(allocator),
                .dir = dir,
            };
            return Store.Iterator.init(&iter);
        }

        fn iterate(self: *Iterator) !Store.Iterator {
            return Store.Iterator.init(self);
        }

        fn next(self: *Iterator) !?std.fs.Dir.Walker.Entry {
            return self.walker.next();
        }

        fn deinit(self: *Iterator) void {
            self.walker.deinit();
            self.dir.close();
        }
    };
};
Segfault when running on my machine
> zig build-exe .\main.zig
> ./main.exe
<file from first iteration>
Segmentation fault at address 0x5cf447d17
C:\...\main.zig:122:36: 0xca5760 in next (main.exe.obj)
            return self.walker.next();
                                   ^
C:\...\main.zig:63:55: 0xc9cea4 in next (main.exe.obj)
                    return ptr_info.Pointer.child.next(self);
                                                      ^
C:\...\main.zig:74:31: 0xc41907 in next (main.exe.obj)
            return self.nextFn(self.ptr);
                              ^
C:\...\main.zig:13:25: 0xc41271 in main (main.exe.obj)
    while (try iter.next()) |entry| {
                        ^
C:\...\zig.zig_Microsoft.Winget.Source_8wekyb3d8bbwe\zig-windows-x86_64-0.13.0\lib\std\start.zig:363:53: 0xc41bbc in WinStartup (main.exe.obj)
    std.os.windows.ntdll.RtlExitUserProcess(callMain());
                                                    ^
???:?:?: 0x7ffaf27153df in ??? (KERNEL32.DLL)
???:?:?: 0x7ffaf2a6485a in ??? (ntdll.dll)

When running the walker on the same hierarchy the walker completes without issue.

Working walker example
pub fn main() !void {
    var buffer: [1000000]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const allocator = fba.allocator();

    const testPath = try std.process.getEnvVarOwned(allocator, "HOME");

    var dir = try std.fs.openDirAbsolute(testPath, .{ .iterate = true });
    var walker = try dir.walk(allocator);
    while (try walker.next()) |entry| {
        std.debug.print("{s}\n", .{entry.basename});
    }
}
fn init(allocator: std.mem.Allocator, path: []const u8) !Store.Iterator {
    var dir = try std.fs.openDirAbsolute(path, .{ .iterate = true });
    var iter = Iterator{
        .walker = try dir.walk(allocator),
        .dir = dir,
    };
    return Store.Iterator.init(&iter); // <- problem!
}

You are returning something that contains a reference to a variable on the init stack frame, which will no longer be valid after init returns. It might work the first iteration, but the location in memory it points to will be overwritten by garbage the next time the stack grows to that point.

Instead of having FileSystemStore.Iterator.init directly return a Store.Iterator, consider changing your design so it returns a FileSystemStore.Iterator, and then expose some kind of asStoreIterator method that takes a reference to FileSystemStore.Iterator (which will have a lifetime determined by the caller) and returns the type-erased Store.Iterator. This is similar to the pattern interfaces like Allocator and Reader/Writer use in the standard library:

// this is the impl-specific state, which lives on this stack frame
var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer std.debug.assert(gpa_state.deinit() == .ok);

// this is the type-erased context pointer and function pointer(s)
const allocator = gpa_state.allocator();
2 Likes

Thank you so much for the quick reply!
I’m still rather new to interface design in zig and will try to internalize your comment and adjust my code as per your suggestion!