Skip to content

computerd: Report entry types and inodes from readdir - #249

Open
aron-cf wants to merge 3 commits into
mainfrom
fuse-bun-fix
Open

aron-cf wants to merge 3 commits into
mainfrom
fuse-bun-fix

Conversation

@aron-cf

@aron-cf aron-cf commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

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.


Devin Review

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-bot

changeset-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

馃 Changeset detected

Latest commit: 2c70b57

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@cloudflare/dofs Patch
@cloudflare/computerd Patch
@cloudflare/computer-rpc Patch
@cloudflare/computer Patch

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

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 3 potential issues.

Devin Review

Comment thread packages/computerd/src/fuse/driver.ts Outdated
Comment on lines +787 to +792
try {
entries.push([name, statToFuse(fs.lstatSync(join(directory, name)))]);
} catch {
// Gone since the listing.
}
}

@devin-ai-integration devin-ai-integration Bot Oct 19, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

馃煛 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.

Devin Review


Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

Comment thread packages/computerd/src/fuse/driver.test.ts Outdated
@pkg-pr-new

pkg-pr-new Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@249

commit: 2c70b57

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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

Comment on lines +506 to +525
// 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

馃攳 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.

Devin Review


Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant