Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
08892b9
fix: handle DT_UNKNOWN in dir_iterator for bind-mounted filesystems
mmitchellg5 Jan 5, 2026
7379c24
fix(glob): handle DT_UNKNOWN for Docker bind mounts and FUSE filesystems
mmitchellg5 Jan 7, 2026
5c69ef8
test: add FUSE filesystem test for glob (per PR #18172 pattern)
mmitchellg5 Jan 7, 2026
b23d261
test: make regression tests concurrent per guidelines
mmitchellg5 Jan 7, 2026
0b73bec
fix: propagate resolved kind to Dirent for withFileTypes
mmitchellg5 Jan 7, 2026
59b50bf
test: use exact path match instead of includes()
mmitchellg5 Jan 7, 2026
7107213
fix: improve FUSE test error handling and robustness
mmitchellg5 Jan 7, 2026
153193b
Merge origin/main into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 7, 2026
a0ae153
Update test/cli/run/glob-on-fuse.test.ts
mmitchellg5 Jan 7, 2026
04de59f
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 7, 2026
a5a9573
fix: avoid throw inside finally block in FUSE test
mmitchellg5 Jan 7, 2026
461a670
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 8, 2026
ec37dd8
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 8, 2026
25fca08
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 8, 2026
545ce01
fix: use lstatat for DT_UNKNOWN to preserve symlink identity
mmitchellg5 Jan 8, 2026
8d588c5
perf: use stack fallback for entry name in DT_UNKNOWN handler
mmitchellg5 Jan 8, 2026
2fba19a
docs: update comments to reflect lstatat usage
mmitchellg5 Jan 8, 2026
6d21bb2
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 8, 2026
e665293
fix(glob): add lstatat to DirEntryAccessor for DT_UNKNOWN handling
mmitchellg5 Jan 8, 2026
e5212df
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 8, 2026
2db051a
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 8, 2026
bba8729
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 9, 2026
a27b282
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 9, 2026
6da7d44
fix(sys): properly handle symlinks in lstatat on Windows
mmitchellg5 Jan 9, 2026
951f8ba
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 12, 2026
3dca3f4
Merge branch 'main' into fix-glob-bind-mount-dt-unknown
mmitchellg5 Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/bun.js/node/dir_iterator.zig
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,17 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type {
continue :start_over;
}

const entry_kind = switch (linux_entry.type) {
const entry_kind: Entry.Kind = switch (linux_entry.type) {
linux.DT.BLK => Entry.Kind.block_device,
linux.DT.CHR => Entry.Kind.character_device,
linux.DT.DIR => Entry.Kind.directory,
linux.DT.FIFO => Entry.Kind.named_pipe,
linux.DT.LNK => Entry.Kind.sym_link,
linux.DT.REG => Entry.Kind.file,
linux.DT.SOCK => Entry.Kind.unix_domain_socket,
// DT_UNKNOWN: Some filesystems (e.g., bind mounts, FUSE, NFS)
// don't provide d_type. Callers should use lstatat() to determine
// the type when needed (lazy stat pattern for performance).
else => Entry.Kind.unknown,
};
return .{
Expand Down
46 changes: 44 additions & 2 deletions src/bun.js/node/node_fs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4627,6 +4627,9 @@ pub const NodeFS = struct {
break :brk bun.path.joinZBuf(buf, &path_parts, .auto);
};

// Track effective kind - may be resolved from .unknown via stat
var effective_kind = current.kind;

enqueue: {
switch (current.kind) {
// a symlink might be a directory or might not be
Expand All @@ -4646,6 +4649,24 @@ pub const NodeFS = struct {

async_task.enqueue(name_to_copy);
},
// Some filesystems (e.g., Docker bind mounts, FUSE, NFS) return
// DT_UNKNOWN for d_type. Use lstatat to determine the actual type.
.unknown => {
if (current.name.len + 1 + name_to_copy.len > bun.MAX_PATH_BYTES) break :enqueue;

// Lazy stat to determine the actual kind (lstatat to not follow symlinks)
const stat_result = bun.sys.lstatat(fd, current.name.sliceAssumeZ());
switch (stat_result) {
.result => |st| {
const real_kind = bun.sys.kindFromMode(st.mode);
effective_kind = real_kind;
if (real_kind == .directory or real_kind == .sym_link) {
async_task.enqueue(name_to_copy);
}
},
.err => {}, // Skip entries we can't stat
}
},
else => {},
}
}
Expand All @@ -4662,7 +4683,7 @@ pub const NodeFS = struct {
entries.append(.{
.name = bun.String.cloneUTF8(utf8_name),
.path = dirent_path_prev,
.kind = current.kind,
.kind = effective_kind,
}) catch |err| bun.handleOom(err);
},
Buffer => {
Expand Down Expand Up @@ -4774,6 +4795,9 @@ pub const NodeFS = struct {
break :brk bun.path.joinZBuf(buf, &path_parts, .auto);
};

// Track effective kind - may be resolved from .unknown via stat
var effective_kind = current.kind;

enqueue: {
switch (current.kind) {
// a symlink might be a directory or might not be
Expand All @@ -4786,6 +4810,24 @@ pub const NodeFS = struct {
if (current.name.len + 1 + name_to_copy.len > bun.MAX_PATH_BYTES) break :enqueue;
stack.writeItem(basename_allocator.dupeZ(u8, name_to_copy) catch break :enqueue) catch break :enqueue;
},
// Some filesystems (e.g., Docker bind mounts, FUSE, NFS) return
// DT_UNKNOWN for d_type. Use lstatat to determine the actual type.
.unknown => {
if (current.name.len + 1 + name_to_copy.len > bun.MAX_PATH_BYTES) break :enqueue;

// Lazy stat to determine the actual kind (lstatat to not follow symlinks)
const stat_result = bun.sys.lstatat(fd, current.name.sliceAssumeZ());
switch (stat_result) {
.result => |st| {
const real_kind = bun.sys.kindFromMode(st.mode);
effective_kind = real_kind;
if (real_kind == .directory or real_kind == .sym_link) {
stack.writeItem(basename_allocator.dupeZ(u8, name_to_copy) catch break :enqueue) catch break :enqueue;
}
},
.err => {}, // Skip entries we can't stat
}
},
else => {},
}
}
Expand All @@ -4801,7 +4843,7 @@ pub const NodeFS = struct {
entries.append(.{
.name = jsc.WebCore.encoding.toBunString(utf8_name, args.encoding),
.path = dirent_path_prev,
.kind = current.kind,
.kind = effective_kind,
}) catch |err| bun.handleOom(err);
},
Buffer => {
Expand Down
107 changes: 107 additions & 0 deletions src/glob/GlobWalker.zig
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ pub const SyscallAccessor = struct {
};
}

/// Like statat but does not follow symlinks.
pub fn lstatat(handle: Handle, path: [:0]const u8) Maybe(bun.Stat) {
if (comptime bun.Environment.isWindows) return statatWindows(handle.value, path);
return Syscall.lstatat(handle.value, path);
}

pub fn openat(handle: Handle, path: [:0]const u8) !Maybe(Handle) {
return switch (Syscall.openat(handle.value, path, bun.O.DIRECTORY | bun.O.RDONLY, 0)) {
.err => |err| .{ .err = err },
Expand Down Expand Up @@ -247,6 +253,20 @@ pub const DirEntryAccessor = struct {
return Syscall.stat(path);
}

/// Like statat but does not follow symlinks.
pub fn lstatat(handle: Handle, path_: [:0]const u8) Maybe(bun.Stat) {
var path: [:0]const u8 = path_;
var buf: bun.PathBuffer = undefined;
if (!bun.path.Platform.auto.isAbsolute(path)) {
if (handle.value) |entry| {
const slice = bun.path.joinStringBuf(&buf, [_][]const u8{ entry.dir, path }, .auto);
buf[slice.len] = 0;
path = buf[0..slice.len :0];
}
}
return Syscall.lstat(path);
}

pub fn open(path: [:0]const u8) !Maybe(Handle) {
return openat(.empty, path);
}
Expand Down Expand Up @@ -902,6 +922,93 @@ pub fn GlobWalker_(

continue;
},
// Some filesystems (e.g., Docker bind mounts, FUSE, NFS) return
// DT_UNKNOWN for d_type. Use lazy stat to determine the real kind
// only when needed (PR #18172 pattern for performance).
.unknown => {
// First check if name might match pattern (avoid unnecessary stat)
const might_match = this.walker.matchPatternImpl(dir_iter_state.pattern, entry_name);
if (!might_match) continue;

// Need to stat to determine actual kind (lstatat to not follow symlinks)
// Use stack fallback for short names (typical case) to avoid arena allocation
const stackbuf_size = 256;
var stfb = std.heap.stackFallback(stackbuf_size, this.walker.arena.allocator());
const name_z = stfb.get().dupeZ(u8, entry_name) catch bun.outOfMemory();
const stat_result = Accessor.lstatat(dir.fd, name_z);
const real_kind: std.fs.File.Kind = switch (stat_result) {
.result => |st| bun.sys.kindFromMode(st.mode),
.err => continue, // Skip entries we can't stat
};

// Process based on actual kind
switch (real_kind) {
.file => {
const matches = this.walker.matchPatternFile(entry_name, dir_iter_state.component_idx, dir.is_last, dir_iter_state.pattern, dir_iter_state.next_pattern);
if (matches) {
const prepared = try this.walker.prepareMatchedPath(entry_name, dir.dir_path) orelse continue;
return .{ .result = prepared };
}
},
.directory => {
var add_dir: bool = false;
const recursion_idx_bump_ = this.walker.matchPatternDir(dir_iter_state.pattern, dir_iter_state.next_pattern, entry_name, dir_iter_state.component_idx, dir_iter_state.is_last, &add_dir);

if (recursion_idx_bump_) |recursion_idx_bump| {
const subdir_parts: []const []const u8 = &[_][]const u8{
dir.dir_path[0..dir.dir_path.len],
entry_name,
};

const subdir_entry_name = try this.walker.join(subdir_parts);

if (recursion_idx_bump == 2) {
try this.walker.workbuf.append(
this.walker.arena.allocator(),
WorkItem.new(subdir_entry_name, dir_iter_state.component_idx + recursion_idx_bump, .directory),
);
try this.walker.workbuf.append(
this.walker.arena.allocator(),
WorkItem.new(subdir_entry_name, dir_iter_state.component_idx, .directory),
);
} else {
try this.walker.workbuf.append(
this.walker.arena.allocator(),
WorkItem.new(subdir_entry_name, dir_iter_state.component_idx + recursion_idx_bump, .directory),
);
}
}

if (add_dir and !this.walker.only_files) {
const prepared_path = try this.walker.prepareMatchedPath(entry_name, dir.dir_path) orelse continue;
return .{ .result = prepared_path };
}
},
.sym_link => {
if (this.walker.follow_symlinks) {
const subdir_parts: []const []const u8 = &[_][]const u8{
dir.dir_path[0..dir.dir_path.len],
entry_name,
};
const entry_start: u32 = @intCast(if (dir.dir_path.len == 0) 0 else dir.dir_path.len + 1);
const subdir_entry_name = try this.walker.join(subdir_parts);

try this.walker.workbuf.append(
this.walker.arena.allocator(),
WorkItem.newSymlink(subdir_entry_name, dir_iter_state.component_idx, entry_start),
);
} else if (!this.walker.only_files) {
const matches = this.walker.matchPatternFile(entry_name, dir_iter_state.component_idx, dir_iter_state.is_last, dir_iter_state.pattern, dir_iter_state.next_pattern);
if (matches) {
const prepared_path = try this.walker.prepareMatchedPath(entry_name, dir.dir_path) orelse continue;
return .{ .result = prepared_path };
}
}
},
else => {}, // Skip other types (block devices, etc.)
}
continue;
},
else => continue,
}
},
Expand Down
23 changes: 23 additions & 0 deletions src/sys.zig
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,29 @@ pub fn fstatat(fd: bun.FileDescriptor, path: [:0]const u8) Maybe(bun.Stat) {
return Maybe(bun.Stat){ .result = stat_buf };
}

/// Like fstatat but does not follow symlinks (uses AT.SYMLINK_NOFOLLOW).
/// This is the "at" equivalent of lstat.
pub fn lstatat(fd: bun.FileDescriptor, path: [:0]const u8) Maybe(bun.Stat) {
if (Environment.isWindows) {
// Use O.NOFOLLOW to not follow symlinks (FILE_OPEN_REPARSE_POINT on Windows)
return switch (openatWindowsA(fd, path, O.NOFOLLOW, 0)) {
.result => |file| {
defer file.close();
return fstat(file);
},
.err => |err| Maybe(bun.Stat){ .err = err },
};
}
var stat_buf = mem.zeroes(bun.Stat);
const fd_valid = if (fd == bun.invalid_fd) std.posix.AT.FDCWD else fd.native();
if (Maybe(bun.Stat).errnoSysFP(syscall.fstatat(fd_valid, path, &stat_buf, std.posix.AT.SYMLINK_NOFOLLOW), .fstatat, fd, path)) |err| {
log("lstatat({f}, {s}) = {s}", .{ fd, path, @tagName(err.getErrno()) });
return err;
}
log("lstatat({f}, {s}) = 0", .{ fd, path });
return Maybe(bun.Stat){ .result = stat_buf };
}

pub fn mkdir(file_path: [:0]const u8, flags: mode_t) Maybe(void) {
return switch (Environment.os) {
.mac => Maybe(void).errnoSysP(syscall.mkdir(file_path, flags), .mkdir, file_path) orelse .success,
Expand Down
Loading