Compare commits

...

1 Commits

Author SHA1 Message Date
Jarred Sumner
fa9b2a83fc fix(install): handle DT_UNKNOWN entries on NFS/FUSE filesystems
On NFS, FUSE, and some bind mounts, getdents64 returns DT_UNKNOWN for
d_type instead of DT_DIR/DT_REG. The directory walker and all install
backends (hardlink, copyfile, clonefile) were silently skipping these
entries, causing `bun install` to produce incomplete node_modules.

Walker: add `resolve_unknown_entry_types` option that falls back to
fstatat() to resolve unknown entry types.

Hardlinker: handle .unknown by optimistically trying linkat, falling
back to makePath on EPERM/EISDIR (the entry was a directory).

FileCopier (isolated): handle .unknown by trying openat as a file,
falling back to makePath on EISDIR.

PackageInstall (clonefile): handle .unknown by trying clonefileat,
falling back to mkdirat on EPERM/EISDIR.

PackageInstall (copyfile): handle .unknown by trying openat as a file,
falling back to makePath on EISDIR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-15 06:54:39 +01:00
4 changed files with 41 additions and 15 deletions

View File

@@ -383,6 +383,7 @@ pub const PackageInstall = struct {
&[_]bun.OSPathSlice{},
&[_]bun.OSPathSlice{},
) catch |err| return Result.fail(err, .opening_cache_dir, @errorReturnTrace());
walker_.resolve_unknown_entry_types = true;
defer walker_.deinit();
const FileCopier = struct {
@@ -520,6 +521,7 @@ pub const PackageInstall = struct {
else
&[_]bun.OSPathSlice{},
) catch |err| bun.handleOom(err);
state.walker.resolve_unknown_entry_types = true;
if (!Environment.isWindows) {
state.subdir = destbase.makeOpenPath(bun.span(destpath), .{

View File

@@ -12,12 +12,16 @@ pub const FileCopier = struct {
return .{
.src_path = src_path,
.dest_subpath = dest_subpath,
.walker = try .walk(
src_dir,
bun.default_allocator,
&.{},
skip_dirnames,
),
.walker = walker: {
var w = try Walker.walk(
src_dir,
bun.default_allocator,
&.{},
skip_dirnames,
);
w.resolve_unknown_entry_types = true;
break :walker w;
},
};
}

View File

@@ -15,12 +15,16 @@ pub fn init(
.src_dir = folder_dir,
.src = src,
.dest = dest,
.walker = try .walk(
folder_dir,
bun.default_allocator,
&.{},
skip_dirnames,
),
.walker = walker: {
var w = try Walker.walk(
folder_dir,
bun.default_allocator,
&.{},
skip_dirnames,
);
w.resolve_unknown_entry_types = true;
break :walker w;
},
};
}

View File

@@ -6,6 +6,7 @@ skip_filenames: []const u64 = &[_]u64{},
skip_dirnames: []const u64 = &[_]u64{},
skip_all: []const u64 = &[_]u64{},
seed: u64 = 0,
resolve_unknown_entry_types: bool = false,
const NameBufferList = std.array_list.Managed(bun.OSPathChar);
@@ -38,7 +39,22 @@ pub fn next(self: *Walker) bun.sys.Maybe(?WalkerEntry) {
.err => |err| return .initErr(err),
.result => |res| {
if (res) |base| {
switch (base.kind) {
// Some filesystems (NFS, FUSE, bind mounts) don't provide
// d_type and return DT_UNKNOWN. Optionally resolve via
// fstatat so callers get accurate types for recursion.
// This only affects POSIX; Windows always provides types.
const kind: std.fs.Dir.Entry.Kind = if (comptime !Environment.isWindows)
(if (base.kind == .unknown and self.resolve_unknown_entry_types) brk: {
const dir_fd = top.iter.iter.dir;
break :brk switch (bun.sys.fstatat(dir_fd, base.name.sliceAssumeZ())) {
.result => |stat_buf| bun.sys.kindFromMode(stat_buf.mode),
.err => continue, // skip entries we can't stat
};
} else base.kind)
else
base.kind;
switch (kind) {
.directory => {
if (std.mem.indexOfScalar(
u64,
@@ -78,7 +94,7 @@ pub fn next(self: *Walker) bun.sys.Maybe(?WalkerEntry) {
const cur_len = self.name_buffer.items.len;
bun.handleOom(self.name_buffer.append(0));
if (base.kind == .directory) {
if (kind == .directory) {
const new_dir = switch (bun.openDirForIterationOSPath(top.iter.iter.dir, base.name.slice())) {
.result => |fd| fd,
.err => |err| return .initErr(err),
@@ -95,7 +111,7 @@ pub fn next(self: *Walker) bun.sys.Maybe(?WalkerEntry) {
.dir = top.iter.iter.dir,
.basename = self.name_buffer.items[dirname_len..cur_len :0],
.path = self.name_buffer.items[0..cur_len :0],
.kind = base.kind,
.kind = kind,
});
} else {
var item = self.stack.pop().?;