Skip to content

vfs: add mount names and make the reserved root readable - #66119

Open
pipobscure wants to merge 3 commits into
nodejs:mainfrom
pipobscure:vfs-provider-name
Open

pipobscure wants to merge 3 commits into
nodejs:mainfrom
pipobscure:vfs-provider-name

Conversation

@pipobscure

@pipobscure pipobscure commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Nothing identified a mounted virtual file system beyond its reserved mount point, which is an opaque implementation detail. A mount made with --vfs-mount in particular could not be told apart from the others, nor be found by the program it was mounted for.

Mount names

vfs.mount([name]) takes an optional name. A named mount can also be reached as path.join(os.devNull, 'vfs', name), a symbolic link to its mount point. A later mount under the same name takes the name over, and unmounting removes the names linking to that mount. Invalid names throw ERR_INVALID_ARG_VALUE. A name must be a single path segment other than . and .., and must not be spelled like a layer id, which is a non-negative integer in canonical decimal form. So 17 is reserved, while 07, -1 and NaN are valid names.

--vfs-mount and --vfs-load accept name=source and pass the name to vfs.mount(). Text before the first = is only a name if it has no path separator, so a path containing = can still be mounted by writing it as ./a=b. --vfs-load takes the prefix too, because a worker re-mounts every source without knowing which one --vfs-load contributed, and must split each value the same way.

The reserved root is a directory

While anything is mounted, ${os.devNull}/vfs is a read-only directory that plain node:fs code can read. It lists every mount point by its layer id, and every name as a symbolic link to that id:

fs.readdirSync(root);                  // [ '0', 'assets' ]
fs.readlinkSync(`${root}/assets`);     // '0'
fs.realpathSync(`${root}/assets`);     // '/dev/null/vfs/0'
fs.readFileSync(`${root}/assets/logo.svg`);

This came out of the discussion with @bakkot on #65748 and #66116 and a short chat to discuss the shape of it.

Nothing identified a mounted virtual file system beyond its reserved
mount point, which is an opaque implementation detail. A mount made
with --vfs-mount in particular could not be told apart from the others,
nor be found by the program it was mounted for.

VirtualProvider now takes an options bag whose `name` is exposed as
`provider.name`, and MemoryProvider, RealFSProvider and ZipProvider
pass their options on to it. A VirtualFileSystem exposes the name of
its provider as `vfs.name`.

--vfs-mount and --vfs-load accept `name=source`, and the name is
handed to the provider through a new third argument to a registered
provider's create(). Text before the first `=` is only a name if it
has no path separator, so a path containing `=` can still be mounted
by writing it as `./a=b`. --vfs-load takes the prefix too because a
worker re-mounts every source without knowing which one --vfs-load
contributed, and must split each value the same way.

Add vfs.mounted(), which returns a null-prototype object mapping the
name of each mounted file system to its VirtualFileSystem, so code can
find a mount by name without being handed the instance. Unnamed mounts
are left out, and when several share a name the earliest mount wins.

Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/config

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Sep 18, 2026
@bakkot

bakkot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Thanks!

Some thoughts:

  • It's kind of weird to support the unnamed form --vfs-mount=foo.zip if there's no way to get it. The docs even say "[the name] is how a program finds such a file system, since it has no other reference to it".
    • For --vfs-load that can make sense because you're mounting it for its behavior (and the code inside can use import.meta.dirname). Although even for --vfs-load I am not clear on the utility of making unnamed vfs mounts (even those from --vfs-load) available in workers - how is the worker supposed to get to them? I guess if the worker is itself running from a file in a vfs-load'd vfs (does that work) it can use import.meta.dirname?
  • Adding a new globally accessible/mutable namespace, wherein any bit of code can mount a named VFS or look one up by name, is odd the absence of a particular reason to do so. I'd only imagined exposing the CLI-provided ones, not arbitrary programatic ones mounted later. I'm not mortally against this but would want to see documented use cases; new code-accessible global registries shouldn't be added lightly.
  • It is surprising to me to make the name a fixed property of the VFS provider itself. I would expect it to be an argument to .mount() (assuming we continue to have names at all, per the last bullet).
  • You mentioned wanting to be able to determine the order of --vfs-mounts. This does that for names that are not nonnegative integers. If we intend people to rely on this order, that should be documented (and tested).
  • Command line options are almost without exception last-wins, not first-wins as you've done here in the case of duplicate names.

@pipobscure
pipobscure marked this pull request as draft September 19, 2026 07:40
@pipobscure

pipobscure commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

@bakkot something you said last night and your comment above ruminated with me and I think you’re right.

You said something like “it’s not like you can just fs.readdir /dev/null/vfs”

I think that might actually be the natural solution here.

  • make the name an argument of mount
  • mount creates a virtual symlink of name to its layer
  • and we simply have fs calls to /dev/null/vfs handled so that you can actually just list them.

Thoughts?

@mcollina this is probably something you should be aware of to vociferously object if you disagree.

@pipobscure

Copy link
Copy Markdown
Contributor Author
  • For --vfs-load that can make sense because you're mounting it for its behavior (and the code inside can use import.meta.dirname). Although even for --vfs-load I am not clear on the utility of making unnamed vfs mounts (even those from --vfs-load) available in workers - how is the worker supposed to get to them? I guess if the worker is itself running from a file in a vfs-load'd vfs (does that work) it can use import.meta.dirname?

Workers get the same —vfs-mount flags in the same order. Since they are run before startup in he same order and layer numbers are deterministic by order it will have the same initial mounts as the main process. So a worker can be pointed at the same import.meta.dirname relative to the main calling it. This already works and has tests.

@pipobscure

This comment was marked as outdated.

The previous commit named a mounted file system through its provider,
and added vfs.mounted() to look one up by that name. A name identifies a
mount, though, not a provider, and a lookup table beside the file system
is something only VFS-aware code can use.

Take the name as an optional argument to vfs.mount() instead, and drop
provider.name, vfs.name, vfs.mounted() and the options argument added to
the provider constructors and to a registered provider's create(). A
later mount under a name takes it over; unmounting removes the names
linking to that mount. --vfs-mount and --vfs-load pass their `name=`
prefix to vfs.mount().

While anything is mounted, the reserved root `${os.devNull}/vfs` is now a
read-only directory served by the new ReservedRootProvider. It lists
each mount point by its layer id, and each name as a symbolic link to
that id, so plain fs code finds a mount with readdir, readlink and
realpath. The dispatcher follows a name at the start of a path to its
layer, except for the operations that act on a link itself (lstat,
readlink, unlink, rm, rename, symlink and the l* variants), so require(),
import and the fs functions all work through a name, and the loader
identifies modules by their real mount point paths. Loader caches under
a name are purged when the name moves or goes away.

A name must be a single path segment other than `.` and `..`, and must
not be spelled the way a number is, since those segments are layer ids;
`07` is a valid name while `7` is not.

realpath() of a mount point returned it with a trailing separator, from
path.join(mountPoint, '/'); it now returns the mount point itself.

Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
A mount point could be removed: rmdir() of an empty mount point went to
the provider, which dropped a nonexistent child of its own root and
reported success, and a recursive rm() did the same once the file system
was empty. Renaming onto or away from it reached the provider as well.
Like any mount point, the root of a VirtualFileSystem now cannot be
removed or renamed, nor replaced by a rename: rmdir() and rename() fail
with EBUSY, and a recursive rm() empties the file system and then fails
the same way.

A mount name was refused if it was spelled as any number, which kept
names like `-1`, `NaN` and `Infinity` that can never be a layer id.
Reserve only the canonical spelling of a non-negative integer.

Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
@pipobscure

pipobscure commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

Also notice to @jasnell as you were the one to originally critique the version of names I had proposed. I think this actually meets the issues you had as well, but worth double checking.

And thanks again to @bakkot ! Pushback makes things better. It did for the original ZipArchives re wide tests, and it does here. I appreciate you taking the time.

@pipobscure pipobscure changed the title vfs: add provider names and vfs.mounted() vfs: add mount names and make vfs accessible via readdir Sep 19, 2026
@pipobscure pipobscure changed the title vfs: add mount names and make vfs accessible via readdir vfs: add mount names and make the reserved root readable Sep 19, 2026
@pipobscure
pipobscure marked this pull request as ready for review September 19, 2026 10:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants