Repository navigation
Conversation
readdir returned names alone, so libfuse filled each directory entry with a zero mode and FUSE_UNKNOWN_INO, and every entry reached getdents(2) as DT_UNKNOWN. POSIX allows that, and Node falls back to lstat, but readers that trust d_type skip such entries. bun's hardlink install walks its cache that way: with BUN_INSTALL_CACHE_DIR under a local-only path it linked nothing into node_modules and then failed with "failed to enqueue lifecycle scripts ...: ENOENT". The VFS driver now returns a stat per entry, built by the same code as getattr so the type and inode match a lookup of the name. The local-only layer lstats entries on disk, and when it merges local-only children into a synced listing, the local stat replaces a synced entry of the same name, as getattr does. An entry that disappears between the listing and its stat is left out.
馃 Changeset detectedLatest commit: 2c70b57 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| try { | ||
| entries.push([name, statToFuse(fs.lstatSync(join(directory, name)))]); | ||
| } catch { | ||
| // Gone since the listing. | ||
| } | ||
| } |
There was a problem hiding this comment.
馃煛 Readable local directories appear empty
For a readable local directory without search permission, statLocalEntries drops every name when child lstatSync returns EACCES. The listing appears empty even though directory reads can return those names.
Learn more
Reading a directory requires read permission, but looking up its children also requires search permission. The local listing obtains names using readdirSync, then this helper discards any child that cannot be stat'd. For a mode 0444 directory accessed by a nonprivileged daemon user, readdirSync succeeds but every child lstatSync fails with EACCES, so FUSE reports no children.
Example: A local-only /cache directory containing pkg has mode 0444. Its owner can enumerate pkg, but cannot look up /cache/pkg. The FUSE listing incorrectly returns [] rather than ['pkg'].
Recommended fix: Only omit a child when its stat fails because it disappeared (ENOENT/ENOTDIR); keep other names with a stat fallback that reports unknown type, or propagate a suitable error instead of silently hiding them. Ensure the merge path handles a missing stat without losing names.
Was this helpful? React with 馃憤 or 馃憥 to provide feedback.
commit: |
readdir built each entry's stat with the same code as getattr, which resolves the child's path one component at a time and runs separate queries for its size and link count. A listing of 2,000 files went from about 1 ms to 18 ms, and to 39 ms six directories deep, a cost that callers wanting only names had never paid. dofs's listing query already joins each entry to its node, so readdirEntriesSync returns the type, permission bits and inode from that one query, with a pending-create buffer shadowing a committed name as it does for lstat. The driver builds each entry's stat from it. Only the type and inode of a plain readdir entry reach getdents(2), and libfuse caches none of it as attributes, so the other fields are placeholders. A VFS without the method falls back to a stat per entry. The same listing now takes about 2.5 ms at any depth.
The unit tests check the stats computerd hands libfuse, not the d_type a program reads back through getdents(2). fs-tests now reads entries with readdir(3) through Python's ctypes and expects a file, a directory and a symlink to come back as such, once in a plain directory and once under node_modules. Node cannot make this check, because it lstats any entry whose type is unknown. run-fs-tests.sh starts computerd with MOUNT_IGNORE=**/node_modules, so the second check covers the local-only layer, and installs python3, as does script/shell.
| // A listing entry's stat. libfuse puts only the type and the inode of a | ||
| // plain readdir entry into getdents(2) and caches none of it as | ||
| // attributes, so the remaining fields are placeholders. With readdirplus | ||
| // these would become cached attributes and need statPath's real values. | ||
| function direntStat(path: string, entry: WorkspaceDirentEntry): FuseStat { | ||
| let mode = DIRENT_TYPE_BITS[entry.type] | entry.mode; | ||
| const override = meta.get(path); | ||
| if (override?.mode !== undefined) mode = (mode & 0o170000) | (override.mode & 0o7777); | ||
| return { | ||
| mtime: EPOCH, | ||
| atime: EPOCH, | ||
| ctime: EPOCH, | ||
| size: 0, | ||
| mode, | ||
| uid: typeof process.getuid === "function" ? process.getuid() : 0, | ||
| gid: typeof process.getgid === "function" ? process.getgid() : 0, | ||
| nlink: 1, | ||
| ino: entry.inode, | ||
| blksize: PREFERRED_IO_BLOCK_SIZE, | ||
| blocks: 0, |
There was a problem hiding this comment.
馃攳 Listing stats differ from lookup stats
The batched readdir path supplies placeholder attributes rather than the values returned by getattr. Plain readdir uses only type and inode, but readdirplus would cache those placeholders; the stated contract needs clarification.
Was this helpful? React with 馃憤 or 馃憥 to provide feedback.
readdir returned names alone, so libfuse filled each directory entry with a zero mode and FUSE_UNKNOWN_INO, and every entry reached getdents(2) as DT_UNKNOWN. POSIX allows that, and Node falls back to lstat, but readers that trust d_type skip such entries. bun's hardlink install walks its cache that way: with BUN_INSTALL_CACHE_DIR under a local-only path it linked nothing into node_modules and then failed with "failed to enqueue lifecycle scripts ...: ENOENT".
The VFS driver now returns a stat per entry, built by the same code as getattr so the type and inode match a lookup of the name. The local-only layer lstats entries on disk, and when it merges local-only children into a synced listing, the local stat replaces a synced entry of the same name, as getattr does. An entry that disappears between the listing and its stat is left out.