Skip to content

Commit ee36353

Browse files
authored
vfs: load native addons from a mounted file system
A native addon inside a virtual file system could not be require()d: dlopen() and LoadLibrary() open a shared object by path, and a VFS path has no inode for them to open. Read the addon's bytes from the VFS and load them from a private, self-cleaning image instead, using the smallest on-disk footprint each platform allows: on Linux an anonymous memfd loaded through /proc/self/fd, so the bytes never reach the file system; on other POSIX platforms a file in a 0700 mkdtemp() directory, unlinked right after loading, with the mapping keeping it alive; on Windows a temp file opened FILE_FLAG_DELETE_ON_CLOSE whose handle is held until exit. This is internal. process.dlopen() keeps its documented (module, filename[, flags]) signature and ignores anything further; the bytes are passed to a dlopenBinary() reachable only through the process_methods binding, which the VFS hook calls for VFS paths. Addons on the real file system are untouched and load directly. Signed-off-by: Philipp Dunkel <pip@pipobscure.com> PR-URL: #65680 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent dd0f203 commit ee36353

12 files changed

Lines changed: 627 additions & 6 deletions

File tree

doc/api/cli.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,26 @@ process.permission.has('fs.read', 'custom-require.js'); // true
268268
process.permission.has('fs.read', 'custom-require-2.js'); // true
269269
```
270270

271+
### `--allow-fs-vfs`
272+
273+
<!-- YAML
274+
added: REPLACEME
275+
-->
276+
277+
> Stability: 1.1 - Active development
278+
279+
When using the [Permission Model][], a [virtual file system][] cannot be
280+
mounted by default: [`vfs.mount()`][] throws `ERR_INVALID_STATE` unless the
281+
user explicitly passes the `--allow-fs-vfs` flag when starting Node.js.
282+
283+
A mounted VFS serves paths that the file system permissions do not describe,
284+
so mounting one is gated on its own flag rather than on `--allow-fs-read` or
285+
`--allow-fs-write`.
286+
287+
```console
288+
$ node --experimental-vfs --permission --allow-fs-vfs app.js
289+
```
290+
271291
### `--allow-fs-write`
272292

273293
<!-- YAML
@@ -4021,6 +4041,7 @@ one is included in the list below.
40214041
* `--allow-child-process`
40224042
* `--allow-ffi`
40234043
* `--allow-fs-read`
4044+
* `--allow-fs-vfs`
40244045
* `--allow-fs-write`
40254046
* `--allow-inspector`
40264047
* `--allow-net`
@@ -4741,6 +4762,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
47414762
[`v8.startupSnapshot.addDeserializeCallback()`]: v8.md#v8startupsnapshotadddeserializecallbackcallback-data
47424763
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
47434764
[`v8.startupSnapshot` API]: v8.md#startup-snapshot-api
4765+
[`vfs.mount()`]: vfs.md#vfsmount
47444766
[asynchronous module customization hooks]: module.md#asynchronous-customization-hooks
47454767
[benchmark runner]: bench.md#command-line-runner
47464768
[captured by the built-in snapshot of Node.js]: https://github.com/nodejs/node/blob/b19525a33cc84033af4addd0f80acd4dc33ce0cf/test/parallel/test-bootstrap-modules.js#L24
@@ -4773,4 +4795,5 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
47734795
[test runner execution model]: test.md#test-runner-execution-model
47744796
[timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
47754797
[tracking issue for user-land snapshots]: https://github.com/nodejs/node/issues/44014
4798+
[virtual file system]: vfs.md
47764799
[ways that `TZ` is handled in other environments]: https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html

doc/api/vfs.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,12 @@ system, the callers are responsible for avoiding removal or
419419
invalidation of modules in the virtual file system while they are
420420
being loaded.
421421

422+
Native addons (`.node` files) stored in a mounted VFS can be `require()`d as
423+
well. The operating system's dynamic loader cannot open a virtual path, so the
424+
addon's bytes are read from the VFS and loaded from a private, self-cleaning
425+
temporary image instead. Addons on the real file system are unaffected and
426+
load directly.
427+
422428
## Use with Single Executable Applications
423429

424430
When running as a [Single Executable Application][] built with

doc/node.1

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,17 @@ process.permission.has('fs.read', 'custom-require.js'); // true
166166
process.permission.has('fs.read', 'custom-require-2.js'); // true
167167
.Ed
168168
.
169+
.It Fl -allow-fs-vfs
170+
When using the Permission Model, a virtual file system cannot be
171+
mounted by default: \fBvfs.mount()\fR throws \fBERR_INVALID_STATE\fR unless the
172+
user explicitly passes the \fB--allow-fs-vfs\fR flag when starting Node.js.
173+
A mounted VFS serves paths that the file system permissions do not describe,
174+
so mounting one is gated on its own flag rather than on \fB--allow-fs-read\fR or
175+
\fB--allow-fs-write\fR.
176+
.Bd -literal
177+
$ node --experimental-vfs --permission --allow-fs-vfs app.js
178+
.Ed
179+
.
169180
.It Fl -allow-fs-write
170181
This flag configures file system write permissions using
171182
the Permission Model.
@@ -2018,6 +2029,8 @@ one is included in the list below.
20182029
.It
20192030
\fB--allow-fs-read\fR
20202031
.It
2032+
\fB--allow-fs-vfs\fR
2033+
.It
20212034
\fB--allow-fs-write\fR
20222035
.It
20232036
\fB--allow-inspector\fR

lib/internal/vfs/setup.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,26 @@ function installModuleLoaderOverrides() {
914914
});
915915
}
916916

917+
let originalDlopen;
918+
919+
function installAddonLoader() {
920+
originalDlopen = process.dlopen;
921+
process.dlopen = function(module, filename, flags) {
922+
// dlopen(2) cannot open a native addon that lives in a VFS by path (it has
923+
// no real inode). Read its bytes and hand them to the internal
924+
// dlopenBinary(), which writes them to a private, self-cleaning temporary
925+
// image - an in-memory memfd on Linux - and loads that. Only VFS paths take
926+
// this route; everything else loads straight from disk through the
927+
// unchanged process.dlopen().
928+
if (StringPrototypeStartsWith(filename, normalizedVfsRootPrefix)) {
929+
const { readFileSync } = require('fs');
930+
const { dlopenBinary } = internalBinding('process_methods');
931+
return dlopenBinary(module, filename, flags, readFileSync(filename));
932+
}
933+
return originalDlopen(module, filename, flags);
934+
};
935+
}
936+
917937
/**
918938
* Install all VFS hooks: module loader overrides and fs handlers.
919939
*/
@@ -922,6 +942,7 @@ function installHooks() {
922942
debug('install hooks');
923943
normalizedVfsRootPrefix = getNormalizedVfsRoot() + sep;
924944
installModuleLoaderOverrides();
945+
installAddonLoader();
925946
vfsHandlerObj = createVfsHandlers();
926947
setVfsHandlers(vfsHandlerObj);
927948
hooksInstalled = true;
@@ -939,6 +960,7 @@ function uninstallHooks() {
939960
setLoaderOverrides();
940961
setVfsHandlers(null);
941962
vfsHandlerObj = undefined;
963+
process.dlopen = originalDlopen;
942964
hooksInstalled = false;
943965
}
944966

0 commit comments

Comments
 (0)