Skip to content

Commit 49f2d1f

Browse files
committed
vfs: drop --vfs-mount, pin the load mount point
--vfs-mount mounted a source without running it, and shared one ordered list of sources with --vfs-load, so neither option could say which entry it had contributed: the entry point was recovered from the position of --vfs-load among the mounts, in a list NODE_OPTIONS could prepend to. Nothing needs more than one mount from the command line: a program that wants more can mount them itself through node:vfs, where it also gets the instance. Remove --vfs-mount, leaving --vfs-load with the single source it mounts and runs, and reserve layer 0 for that source, numbering the file systems a program mounts itself from 1. The source is then at the same mount point in every thread, whatever else that thread mounts - including a thread where a --require preload mounted a file system of its own first - so a path into it stays valid in a worker. A worker still does not run that entry point: it inherits the source but not the decision to load from it. A worker created with its own execArgv inherits neither, so the documentation now says that such a worker must be given --experimental-vfs and --vfs-load again to run a script from the mount, and that --experimental-vfs is also what makes node:vfs available to the worker's own code. ERR_VFS_INVALID_TARGET now names --vfs-load as the source's origin, and the startup test moves to test-vfs-load.js, with the cases that covered mounting without loading removed and cases for the reserved mount point added. Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
1 parent 44ff2db commit 49f2d1f

15 files changed

Lines changed: 248 additions & 381 deletions

‎doc/api/cli.md‎

Lines changed: 31 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3795,56 +3795,18 @@ added: REPLACEME
37953795

37963796
Requires [`--experimental-vfs`][]. May be given at most once.
37973797

3798-
Mounts `source` exactly as [`--vfs-mount`][] does, and additionally runs the
3799-
entry point and all subsequent `require()`/`import` resolution against that
3800-
mount rather than the real file system. The entry point is taken from the mount
3801-
the same way `node <directory>` takes one: the mount's own `package.json`
3802-
`"main"`, or `index.js`. Any positional command-line argument is the program's
3803-
own (available from `process.argv[2]` onward), never an entry-point override.
3798+
Mounts `source` as a virtual file system ([`node:vfs`][]), and runs the entry
3799+
point and all subsequent `require()`/`import` resolution against that mount
3800+
rather than the real file system. The mount is placed at a reserved mount point
3801+
assigned by Node.js, so it never shadows real paths and no target can be
3802+
chosen. The entry point is taken from the mount the same way `node <directory>`
3803+
takes one: the mount's own `package.json` `"main"`, or `index.js`. Any
3804+
positional command-line argument is the program's own (available from
3805+
`process.argv[2]` onward), never an entry-point override.
38043806

38053807
`process.argv[1]` reports `source` rather than the reserved mount point, since
38063808
the mount point is an opaque implementation detail.
38073809

3808-
Mounting the same source twice mounts it twice, at two separate mount points.
3809-
The entry point then comes from the mount `--vfs-load` itself contributed, not
3810-
from an earlier `--vfs-mount` of the same source.
3811-
3812-
In worker threads `--vfs-load` mounts but does not load: a worker inherits the
3813-
same mounts, in the same order, and runs its own entry point.
3814-
3815-
`--vfs-load` is not permitted in [`NODE_OPTIONS`][]: which entry point runs is
3816-
the command line's decision, and the environment must not be able to redirect
3817-
it.
3818-
3819-
```console
3820-
$ node --experimental-vfs --vfs-load=app.zip
3821-
$ node --experimental-vfs --vfs-mount=lib.zip --vfs-load=app.zip
3822-
```
3823-
3824-
### `--vfs-mount=source`
3825-
3826-
<!-- YAML
3827-
added: REPLACEME
3828-
-->
3829-
3830-
* `source` {string} A directory or an archive file to mount.
3831-
3832-
Requires [`--experimental-vfs`][]. May be repeated to mount several sources.
3833-
3834-
Mounts `source` as a virtual file system ([`node:vfs`][]). Each mount is placed
3835-
at a reserved mount point assigned by Node.js, so mounts never shadow real
3836-
paths and no target can be chosen. Mounting alone does not change the entry
3837-
point; use [`--vfs-load`][] for the source to run from.
3838-
3839-
`--vfs-mount` and [`--vfs-load`][] mount in the order they are written, so
3840-
3841-
```console
3842-
$ node --experimental-vfs --vfs-mount=a --vfs-load=b --vfs-mount=c
3843-
```
3844-
3845-
mounts `a`, `b` and `c` in that order and runs `b`. Mounts contributed by
3846-
[`NODE_OPTIONS`][] are mounted before the command line's.
3847-
38483810
The provider backing a source is chosen from the source itself rather than from
38493811
its file name:
38503812

@@ -3857,6 +3819,29 @@ preloaded with [`--require`][] or [`--import`][]) are consulted first, in
38573819
reverse registration order, and may claim directories as well as files. If no
38583820
provider claims the source, Node.js exits with an error.
38593821

3822+
In worker threads `--vfs-load` mounts but does not load: a worker inherits the
3823+
mount and runs its own entry point, which may itself live in the mount.
3824+
3825+
The source is mounted at the same reserved mount point in every thread that
3826+
mounts it, whatever else that thread mounts, so a path into the mount means the
3827+
same thing in all of them.
3828+
3829+
A worker created with its own `execArgv` inherits none of the parent's options,
3830+
and so does not mount the source at all. To run a script from the mount, such a
3831+
worker must be given the same options again, `--experimental-vfs` and
3832+
`--vfs-load`; without them, that thread has no mount for the script to come
3833+
from, and the worker fails to load it. `--experimental-vfs` is also what makes
3834+
[`node:vfs`][] available to the worker's own code. A worker whose script comes
3835+
from anywhere else, such as the real file system, needs nothing added.
3836+
3837+
`--vfs-load` is not permitted in [`NODE_OPTIONS`][]: which entry point runs is
3838+
the command line's decision, and the environment must not be able to redirect
3839+
it.
3840+
3841+
```console
3842+
$ node --experimental-vfs --vfs-load=app.zip
3843+
```
3844+
38603845
### `--watch`
38613846

38623847
<!-- YAML
@@ -4299,7 +4284,6 @@ one is included in the list below.
42994284
* `--use-openssl-ca`
43004285
* `--use-system-ca`
43014286
* `--v8-pool-size`
4302-
* `--vfs-mount`
43034287
* `--watch-kill-signal`
43044288
* `--watch-path`
43054289
* `--watch-preserve-output`
@@ -4825,8 +4809,6 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
48254809
[`--require`]: #-r---require-module
48264810
[`--use-env-proxy`]: #--use-env-proxy
48274811
[`--use-system-ca`]: #--use-system-ca
4828-
[`--vfs-load`]: #--vfs-loadsource
4829-
[`--vfs-mount`]: #--vfs-mountsource
48304812
[`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage
48314813
[`Buffer`]: buffer.md#class-buffer
48324814
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html

‎doc/api/errors.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3569,7 +3569,7 @@ entry types are found.
35693569

35703570
### `ERR_VFS_INVALID_TARGET`
35713571

3572-
A `--vfs-mount` source does not exist, is neither a regular file nor a
3572+
A `--vfs-load` source does not exist, is neither a regular file nor a
35733573
directory, or is a source no provider claims.
35743574

35753575
<a id="ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING"></a>

‎doc/api/vfs.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ added: REPLACEME
106106
* `create` {Function} Called with the resolved path and its [`fs.Stats`][].
107107
Returns the {VirtualProvider} backing the source.
108108

109-
Registers a provider that [`--vfs-mount`][] can select for a source it
109+
Registers a provider that [`--vfs-load`][] can select for a source it
110110
recognizes, so a file format Node.js has no built-in provider for can still be
111111
mounted.
112112

@@ -118,7 +118,7 @@ source, the built-in providers handle it: a directory with
118118
[`RealFSProvider`][], and a file whose bytes are a ZIP archive with
119119
[`ZipProvider`][].
120120

121-
Providers must be registered before the mounts are created. Register from a
121+
Providers must be registered before the source is mounted. Register from a
122122
module preloaded with [`--require`][] or [`--import`][]:
123123

124124
```cjs
@@ -702,7 +702,7 @@ fields use synthetic but stable values:
702702
[Single Executable Application]: single-executable-applications.md
703703
[`--import`]: cli.md#--importmodule
704704
[`--require`]: cli.md#-r---require-module
705-
[`--vfs-mount`]: cli.md#--vfs-mountsource
705+
[`--vfs-load`]: cli.md#--vfs-loadsource
706706
[`MemoryProvider`]: #class-memoryprovider
707707
[`RealFSProvider`]: #class-realfsprovider
708708
[`VirtualFileSystem`]: #class-virtualfilesystem

‎doc/node.1‎

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1888,43 +1888,16 @@ Print node's version.
18881888
\fBsource\fR \fB{string}\fR A directory or an archive file to mount and run.
18891889
.El
18901890
Requires \fB--experimental-vfs\fR. May be given at most once.
1891-
Mounts \fBsource\fR exactly as \fB--vfs-mount\fR does, and additionally runs the
1892-
entry point and all subsequent \fBrequire()\fR/\fBimport\fR resolution against that
1893-
mount rather than the real file system. The entry point is taken from the mount
1894-
the same way \fBnode <directory>\fR takes one: the mount's own \fBpackage.json\fR
1895-
\fB"main"\fR, or \fBindex.js\fR. Any positional command-line argument is the program's
1896-
own (available from \fBprocess.argv[2]\fR onward), never an entry-point override.
1891+
Mounts \fBsource\fR as a virtual file system (\fBnode:vfs\fR), and runs the entry
1892+
point and all subsequent \fBrequire()\fR/\fBimport\fR resolution against that mount
1893+
rather than the real file system. The mount is placed at a reserved mount point
1894+
assigned by Node.js, so it never shadows real paths and no target can be
1895+
chosen. The entry point is taken from the mount the same way \fBnode <directory>\fR
1896+
takes one: the mount's own \fBpackage.json\fR \fB"main"\fR, or \fBindex.js\fR. Any
1897+
positional command-line argument is the program's own (available from
1898+
\fBprocess.argv[2]\fR onward), never an entry-point override.
18971899
\fBprocess.argv[1]\fR reports \fBsource\fR rather than the reserved mount point, since
18981900
the mount point is an opaque implementation detail.
1899-
Mounting the same source twice mounts it twice, at two separate mount points.
1900-
The entry point then comes from the mount \fB--vfs-load\fR itself contributed, not
1901-
from an earlier \fB--vfs-mount\fR of the same source.
1902-
In worker threads \fB--vfs-load\fR mounts but does not load: a worker inherits the
1903-
same mounts, in the same order, and runs its own entry point.
1904-
\fB--vfs-load\fR is not permitted in \fBNODE_OPTIONS\fR: which entry point runs is
1905-
the command line's decision, and the environment must not be able to redirect
1906-
it.
1907-
.Bd -literal
1908-
$ node --experimental-vfs --vfs-load=app.zip
1909-
$ node --experimental-vfs --vfs-mount=lib.zip --vfs-load=app.zip
1910-
.Ed
1911-
.
1912-
.It Fl -vfs-mount Ns = Ns Ar source
1913-
.Bl -bullet
1914-
.It
1915-
\fBsource\fR \fB{string}\fR A directory or an archive file to mount.
1916-
.El
1917-
Requires \fB--experimental-vfs\fR. May be repeated to mount several sources.
1918-
Mounts \fBsource\fR as a virtual file system (\fBnode:vfs\fR). Each mount is placed
1919-
at a reserved mount point assigned by Node.js, so mounts never shadow real
1920-
paths and no target can be chosen. Mounting alone does not change the entry
1921-
point; use \fB--vfs-load\fR for the source to run from.
1922-
\fB--vfs-mount\fR and \fB--vfs-load\fR mount in the order they are written, so
1923-
.Bd -literal
1924-
$ node --experimental-vfs --vfs-mount=a --vfs-load=b --vfs-mount=c
1925-
.Ed
1926-
mounts \fBa\fR, \fBb\fR and \fBc\fR in that order and runs \fBb\fR. Mounts contributed by
1927-
\fBNODE_OPTIONS\fR are mounted before the command line's.
19281901
The provider backing a source is chosen from the source itself rather than from
19291902
its file name:
19301903
.Bl -bullet
@@ -1938,6 +1911,24 @@ Providers registered with \fBvfs.registerProvider()\fR (typically from a module
19381911
preloaded with \fB--require\fR or \fB--import\fR) are consulted first, in
19391912
reverse registration order, and may claim directories as well as files. If no
19401913
provider claims the source, Node.js exits with an error.
1914+
In worker threads \fB--vfs-load\fR mounts but does not load: a worker inherits the
1915+
mount and runs its own entry point, which may itself live in the mount.
1916+
The source is mounted at the same reserved mount point in every thread that
1917+
mounts it, whatever else that thread mounts, so a path into the mount means the
1918+
same thing in all of them.
1919+
A worker created with its own \fBexecArgv\fR inherits none of the parent's options,
1920+
and so does not mount the source at all. To run a script from the mount, such a
1921+
worker must be given the same options again, \fB--experimental-vfs\fR and
1922+
\fB--vfs-load\fR; without them, that thread has no mount for the script to come
1923+
from, and the worker fails to load it. \fB--experimental-vfs\fR is also what makes
1924+
\fBnode:vfs\fR available to the worker's own code. A worker whose script comes
1925+
from anywhere else, such as the real file system, needs nothing added.
1926+
\fB--vfs-load\fR is not permitted in \fBNODE_OPTIONS\fR: which entry point runs is
1927+
the command line's decision, and the environment must not be able to redirect
1928+
it.
1929+
.Bd -literal
1930+
$ node --experimental-vfs --vfs-load=app.zip
1931+
.Ed
19411932
.
19421933
.It Fl -watch
19431934
Starts Node.js in watch mode.
@@ -2430,8 +2421,6 @@ one is included in the list below.
24302421
.It
24312422
\fB--v8-pool-size\fR
24322423
.It
2433-
\fB--vfs-mount\fR
2434-
.It
24352424
\fB--watch-kill-signal\fR
24362425
.It
24372426
\fB--watch-path\fR

‎lib/internal/errors.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1962,7 +1962,7 @@ E('ERR_USE_AFTER_CLOSE', '%s was closed', Error);
19621962
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
19631963
'At least one valid performance entry type is required', Error);
19641964
E('ERR_VFS_INVALID_TARGET',
1965-
'%s is not a valid --vfs-mount source: must be an existing file or directory', Error);
1965+
'%s is not a valid --vfs-load source: must be an existing file or directory', Error);
19661966
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
19671967
'A dynamic import callback was not specified.', TypeError);
19681968
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG',

‎lib/internal/main/worker_thread.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ port.on('message', (message) => {
145145
// initializeAsyncLoaderHooksOnLoaderHookWorker() which needs to run preloads
146146
// after the asynchronous loader hooks are registered.
147147
initializeModuleLoaders({ shouldSpawnLoaderHookWorker: true, shouldPreloadModules: true });
148-
// Re-mount inherited --vfs-mount sources so their reserved paths (which a
149-
// worker filename may point into) resolve in this thread too. With
148+
// Re-mount the inherited --vfs-load source so its reserved path (which a
149+
// worker filename may point into) resolves in this thread too. With
150150
// --import, mounting is deferred to after that loop in run_main, matching
151151
// the main thread; finishVfsMounts() is idempotent so it runs once.
152152
if (getOptionValue('--import').length === 0) {

‎lib/internal/process/pre_execution.js‎

Lines changed: 43 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ const {
1414
ObjectDefineProperty,
1515
ObjectFreeze,
1616
String,
17-
StringPrototypeIndexOf,
18-
StringPrototypeSlice,
1917
globalThis,
2018
} = primordials;
2119

@@ -216,91 +214,58 @@ function setupVmModules() {
216214
let vfsMounted = false;
217215
let vfsLoadRoot;
218216

219-
// --vfs-mount and --vfs-load append to one list, so `mounts` is already in the
220-
// order the command line gave, and the entry point comes from whichever of them
221-
// --vfs-load contributed. The parser stores plain strings and cannot record
222-
// which flag produced an entry, so its position is recovered from execArgv -
223-
// the command line's own node options, in order. NODE_OPTIONS may add mounts
224-
// but not a --vfs-load, so anything it contributed sits ahead of these.
225-
// Returns -1 when no --vfs-load was given.
226-
function getVfsLoadIndex(mountCount) {
227-
if (!getOptionValue('[vfs_load_set]')) return -1;
228-
229-
const execArgv = process.execArgv;
230-
let seen = 0;
231-
let found = -1;
232-
for (let i = 0; i < execArgv.length; i++) {
233-
const arg = execArgv[i];
234-
let name = arg;
235-
const eq = StringPrototypeIndexOf(arg, '=');
236-
let spaced = false;
237-
if (eq !== -1) {
238-
name = StringPrototypeSlice(arg, 0, eq);
239-
} else {
240-
// `--vfs-mount value`: the value is the next argument, so skip it rather
241-
// than counting it as a flag of its own.
242-
spaced = true;
243-
}
244-
if (name !== '--vfs-mount' && name !== '--vfs-load') continue;
245-
if (name === '--vfs-load') found = seen;
246-
seen++;
247-
if (spaced) i++;
248-
}
249-
if (found === -1) return -1;
250-
// Mounts from NODE_OPTIONS are parsed first and so precede the command
251-
// line's; `seen` counts only the latter.
252-
return mountCount - seen + found;
253-
}
254-
255-
// Mounts every --vfs-mount source. Called from prepareExecution() when there is
217+
// Mounts the --vfs-load source. Called from prepareExecution() when there is
256218
// no --import, and otherwise from run_main after the --import loop has run; the
257-
// guard makes the second call a no-op so a provider registered by either a -r or
258-
// an --import preload is available before its source's provider is chosen.
219+
// guard makes the second call a no-op so a provider registered by either a -r
220+
// or an --import preload is available before the source's provider is chosen.
259221
function finishVfsMounts() {
260222
if (vfsMounted) return;
261223
vfsMounted = true;
262224

263-
const entries = getOptionValue('--vfs-mount');
264-
if (entries.length === 0) return;
265-
emitExperimentalWarning('--vfs-mount');
225+
const source = getOptionValue('--vfs-load');
226+
if (source === '') return;
227+
emitExperimentalWarning('--vfs-load');
266228

267229
const fs = require('fs');
268230
const path = require('path');
269231
const { selectProvider } = require('internal/vfs/provider_registry');
270-
const { VirtualFileSystem } = require('internal/vfs/file_system');
271-
272-
// --vfs-load is forced off in workers (see node_worker.cc), so this records a
273-
// load root only on the main thread; a worker re-mounts the same sources in
274-
// the same order (the reserved paths line up) but runs its own entry.
275-
const loadIndex = getVfsLoadIndex(entries.length);
276-
for (let i = 0; i < entries.length; i++) {
277-
const resolvedSource = path.resolve(entries[i]);
278-
let stats;
279-
try {
280-
stats = fs.statSync(resolvedSource);
281-
} catch {
282-
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
283-
}
284-
if (!stats.isDirectory() && !stats.isFile()) {
285-
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
286-
}
287-
const provider = selectProvider(resolvedSource, stats);
288-
if (provider === null) {
289-
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
290-
}
291-
const vfs = new VirtualFileSystem(provider, { emitExperimentalWarning: false });
292-
const mountPoint = vfs.mount();
293-
// The mount --vfs-load contributed is what the entry is require()d from;
294-
// process.argv[1] names the real source instead, since the reserved mount
295-
// point is an opaque implementation detail.
296-
//
297-
// The source is spliced in rather than assigned over argv[1]: the entry
298-
// comes from the mount, so nothing was consumed as an entry point and the
299-
// first positional argument is the program's own. Overwriting would drop it.
300-
if (i === loadIndex) {
301-
vfsLoadRoot = mountPoint;
302-
ArrayPrototypeSplice(process.argv, 1, 0, resolvedSource);
303-
}
232+
const { VirtualFileSystem, kLoadLayer } = require('internal/vfs/file_system');
233+
234+
const resolvedSource = path.resolve(source);
235+
let stats;
236+
try {
237+
stats = fs.statSync(resolvedSource);
238+
} catch {
239+
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
240+
}
241+
if (!stats.isDirectory() && !stats.isFile()) {
242+
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
243+
}
244+
const provider = selectProvider(resolvedSource, stats);
245+
if (provider === null) {
246+
throw new ERR_VFS_INVALID_TARGET(resolvedSource);
247+
}
248+
// The source is mounted at the layer reserved for it, so it is at the same
249+
// reserved path in every thread.
250+
const vfs = new VirtualFileSystem(provider, {
251+
__proto__: null,
252+
emitExperimentalWarning: false,
253+
[kLoadLayer]: true,
254+
});
255+
const mountPoint = vfs.mount();
256+
// A worker inherits the source but not [vfs_load_set] (see node_worker.cc):
257+
// it mounts the same source, and so reaches it at the same reserved path,
258+
// but runs its own entry point.
259+
//
260+
// On the main thread the entry is require()d from the mount point, while
261+
// process.argv[1] reports the source, since the reserved mount point is an
262+
// opaque implementation detail. The source is spliced in rather than
263+
// assigned over argv[1]: the entry comes from the mount, so nothing was
264+
// consumed as an entry point and the first positional argument is the
265+
// program's own. Overwriting would drop it.
266+
if (getOptionValue('[vfs_load_set]')) {
267+
vfsLoadRoot = mountPoint;
268+
ArrayPrototypeSplice(process.argv, 1, 0, resolvedSource);
304269
}
305270
}
306271

0 commit comments

Comments
 (0)