Skip to content

Commit 64fb33d

Browse files
mcollinapipobscure
authored andcommitted
ffi: load libraries from a mounted VFS
The operating system's dynamic loader cannot open a library that lives in a mounted virtual file system: the reserved mount path has no real inode. Native addons already handle this in require(): the loader hands their bytes to process.dlopen(), which loads them from a private, self-cleaning image - an anonymous in-memory memfd on Linux. Make ffi.dlopen() and new DynamicLibrary() do the same transparently. Mirroring the fs handler integration, the VFS hook installer sets a library reader into node:ffi while at least one VFS is mounted and clears it when the last one unmounts; DynamicLibrary consults it before every load, so the dependency points from the VFS into ffi and ffi never loads any VFS code. The reader hands the library's bytes to the native constructor, which loads them from the same kind of image, released right after the load, while library.path keeps reporting the virtual path. Libraries on the real file system are unaffected and load directly, and pay only a null check while no VFS is mounted. The AddonImage materializer moves from an anonymous namespace in node_binding.cc to node_binding.h so that node_ffi.cc can reuse it. On Windows the image is now written and closed before the load, because the loader shares read alone and a retained writable delete-on-close handle failed the load with ERROR_SHARING_VIOLATION; since a mapped image cannot be unlinked there, it is kept with the module it loaded as and both are released at process exit. On POSIX the image still never outlives the constructor call, so nothing is left for dlclose() to clean up. Also fix the VFS dlopen hook forwarding a missing flags argument as undefined, which process.dlopen() coerces to 0 - not a valid dlopen(2) mode - so loading any addon from the real file system failed with EINVAL while a VFS was mounted. Co-authored-by: Philipp Dunkel <pipobscure@users.noreply.github.com> Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65909 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 287ece3 commit 64fb33d

11 files changed

Lines changed: 501 additions & 88 deletions

File tree

doc/api/ffi.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ const path = `libsqlite3.${suffix}`;
210210

211211
<!-- YAML
212212
added: v26.1.0
213+
changes:
214+
- version: REPLACEME
215+
pr-url: https://github.com/nodejs/node/pull/65909
216+
description: Library paths inside a mounted virtual file system are now
217+
supported.
213218
-->
214219

215220
* `path` {string|null} Path to a dynamic library, or `null` to resolve symbols
@@ -221,6 +226,13 @@ Loads a dynamic library and resolves the requested function definitions.
221226

222227
On Windows passing `null` is not supported.
223228

229+
A `path` inside a mounted [virtual file system][] is supported: the
230+
operating system's dynamic loader cannot open a virtual path, so the
231+
library's bytes are read from the VFS and loaded from a private,
232+
self-cleaning temporary image instead, while `lib.path` keeps reporting
233+
the virtual path. Libraries on the real file system are unaffected and
234+
load directly.
235+
224236
When `definitions` is omitted, `functions` is returned as an empty object until
225237
symbols are resolved explicitly.
226238

@@ -302,13 +314,24 @@ Represents a loaded dynamic library.
302314

303315
### `new DynamicLibrary(path)`
304316

317+
<!-- YAML
318+
changes:
319+
- version: REPLACEME
320+
pr-url: https://github.com/nodejs/node/pull/65909
321+
description: Library paths inside a mounted virtual file system are now
322+
supported.
323+
-->
324+
305325
* `path` {string|null} Path to a dynamic library, or `null` to resolve symbols
306326
from the current process image.
307327

308328
Loads the dynamic library without resolving any functions eagerly.
309329

310330
On Windows passing `null` is not supported.
311331

332+
A `path` inside a mounted [virtual file system][] loads the same way as
333+
with [`ffi.dlopen()`][].
334+
312335
```cjs
313336
const { DynamicLibrary, suffix } = require('node:ffi');
314337

@@ -798,7 +821,9 @@ and keep callback and pointer lifetimes explicit on the native side.
798821

799822
[Permission Model]: permissions.md#permission-model
800823
[`--allow-ffi`]: cli.md#--allow-ffi
824+
[`ffi.dlopen()`]: #ffidlopenpath-definitions
801825
[`ffi.toBuffer(pointer, length, copy)`]: #ffitobufferpointer-length-copy
802826
[`library.functions`]: #libraryfunctions
803827
[`using`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using
804828
[type names]: #type-names
829+
[virtual file system]: vfs.md

doc/api/vfs.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,12 @@ addon's bytes are read from the VFS and loaded from a private, self-cleaning
425425
temporary image instead. Addons on the real file system are unaffected and
426426
load directly.
427427

428+
Shared libraries opened through [`ffi.dlopen()`][] (or
429+
[`new ffi.DynamicLibrary()`][]) work the same way: a library path inside a
430+
mounted VFS is detected, its bytes are read from the VFS, and the library is
431+
loaded from a private, self-cleaning image while `library.path` keeps
432+
reporting the virtual path. Libraries on the real file system load directly.
433+
428434
## Use with Single Executable Applications
429435

430436
When running as a [Single Executable Application][] built with
@@ -634,9 +640,11 @@ fields use synthetic but stable values:
634640
[`VirtualFileSystem`]: #class-virtualfilesystem
635641
[`VirtualProvider`]: #class-virtualprovider
636642
[`ZipProvider`]: #class-zipprovider
643+
[`ffi.dlopen()`]: ffi.md#ffidlopenpath-definitions
637644
[`fs.BigIntStats`]: fs.md#class-fsstats
638645
[`fs.Stats`]: fs.md#class-fsstats
639646
[`import.meta.resolve()`]: esm.md#importmetaresolvespecifier
647+
[`new ffi.DynamicLibrary()`]: ffi.md#new-dynamiclibrarypath
640648
[`node:fs`]: fs.md
641649
[`require()`]: modules.md#requireid
642650
[`require.resolve()`]: modules.md#requireresolverequest-options

lib/ffi.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
ObjectGetOwnPropertyDescriptor,
1010
ObjectKeys,
1111
ObjectPrototypeToString,
12+
ReflectConstruct,
1213
SafeWeakMap,
1314
SafeWeakRef,
1415
SymbolDispose,
@@ -38,7 +39,7 @@ const {
3839
emitExperimentalWarning('FFI');
3940

4041
const {
41-
DynamicLibrary,
42+
DynamicLibrary: NativeDynamicLibrary,
4243
getInt8,
4344
getUint8,
4445
getInt16,
@@ -119,6 +120,37 @@ function wrapFFIFunction(rawFn, owner) {
119120
return wrapped;
120121
}
121122

123+
const { getVfsLibraryReader } = require('internal/ffi/vfs');
124+
125+
// A thin constructor in front of the native class so that a library inside
126+
// a mounted virtual file system loads transparently: its bytes are read
127+
// from the VFS and handed to the native constructor, which loads them from
128+
// a private, self-cleaning image - the same way require() handles a native
129+
// addon in a VFS. The reader is installed by the VFS while it is mounted
130+
// (see internal/ffi/vfs), so no VFS code is ever loaded from here. The
131+
// wrapper shares the native prototype, so instances and instanceof behave
132+
// as if the native class were exposed directly.
133+
function DynamicLibrary(path) {
134+
if (new.target === undefined) {
135+
// Let the native constructor produce its usual error.
136+
return FunctionPrototypeCall(NativeDynamicLibrary, this, path);
137+
}
138+
const readVirtualLibrary = getVfsLibraryReader();
139+
const binary =
140+
readVirtualLibrary === null || typeof path !== 'string' ?
141+
undefined : readVirtualLibrary(path);
142+
return ReflectConstruct(NativeDynamicLibrary,
143+
binary === undefined ? [path] : [path, binary],
144+
new.target);
145+
}
146+
DynamicLibrary.prototype = NativeDynamicLibrary.prototype;
147+
ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', {
148+
__proto__: null,
149+
configurable: true,
150+
value: DynamicLibrary,
151+
writable: true,
152+
});
153+
122154
const rawGetFunction = DynamicLibrary.prototype.getFunction;
123155
const rawGetFunctions = DynamicLibrary.prototype.getFunctions;
124156
const rawClose = DynamicLibrary.prototype.close;

lib/internal/ffi/vfs.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict';
2+
3+
// Seam between node:ffi and the virtual file system, mirroring the fs
4+
// handler integration in internal/fs/utils: the VFS hook installer sets a
5+
// library reader while at least one VFS is mounted and clears it when the
6+
// last one unmounts, and DynamicLibrary consults it before every load. The
7+
// dependency points from the VFS into ffi: ffi never loads any VFS code,
8+
// and pays only a null check while no VFS is mounted.
9+
10+
// When reader is null, no VFS is active (zero overhead). Otherwise it is
11+
// (path) => Buffer|undefined: the library's bytes for a path inside a
12+
// mounted VFS, or undefined for a path the dynamic loader should open
13+
// itself.
14+
let vfsLibraryReader = null;
15+
16+
function setVfsLibraryReader(reader) {
17+
vfsLibraryReader = reader;
18+
}
19+
20+
function getVfsLibraryReader() {
21+
return vfsLibraryReader;
22+
}
23+
24+
module.exports = {
25+
getVfsLibraryReader,
26+
setVfsLibraryReader,
27+
};

lib/internal/vfs/setup.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,10 +968,38 @@ function installAddonLoader() {
968968
const { dlopenBinary } = internalBinding('process_methods');
969969
return dlopenBinary(module, filename, flags, readFileSync(filename));
970970
}
971+
// Do not forward a missing flags argument as `undefined`:
972+
// process.dlopen() coerces it to 0, which is not a valid dlopen(2)
973+
// mode, instead of applying the default flags.
974+
if (flags === undefined) return originalDlopen(module, filename);
971975
return originalDlopen(module, filename, flags);
972976
};
973977
}
974978

979+
/**
980+
* Reads the bytes of a file that lives in a mounted VFS. Returns undefined
981+
* for a path outside the reserved VFS root - the caller should open the
982+
* path itself - and throws ENOENT for a path under the root that no
983+
* mounted VFS serves, since no real file can exist there. Installed into
984+
* node:ffi while hooks are installed, so DynamicLibrary can load a
985+
* VFS-resident library from a private image, the same way the module
986+
* loader handles a native addon in a VFS.
987+
* @param {string} pathStr The path of the library
988+
* @returns {Buffer|undefined} The library's bytes, or undefined
989+
*/
990+
function readVirtualBinary(pathStr) {
991+
const normalized = normalizeMountedPath(pathStr);
992+
if (!StringPrototypeStartsWith(normalized, normalizedVfsRootPrefix)) {
993+
return undefined;
994+
}
995+
const layerId = getLayerIdFromPath(normalized);
996+
const vfs = layerId === -1 ? undefined : activeVFSLayers.get(layerId);
997+
if (vfs === undefined || !vfs.shouldHandleNormalized(normalized)) {
998+
throw createENOENT('open', pathStr);
999+
}
1000+
return vfs.readFileSync(normalized);
1001+
}
1002+
9751003
/**
9761004
* Install all VFS hooks: module loader overrides and fs handlers.
9771005
*/
@@ -981,6 +1009,8 @@ function installHooks() {
9811009
normalizedVfsRootPrefix = getNormalizedVfsRoot() + sep;
9821010
installModuleLoaderOverrides();
9831011
installAddonLoader();
1012+
const { setVfsLibraryReader } = require('internal/ffi/vfs');
1013+
setVfsLibraryReader(readVirtualBinary);
9841014
vfsHandlerObj = createVfsHandlers();
9851015
setVfsHandlers(vfsHandlerObj);
9861016
hooksInstalled = true;
@@ -998,6 +1028,8 @@ function uninstallHooks() {
9981028
setLoaderOverrides();
9991029
setVfsHandlers(null);
10001030
vfsHandlerObj = undefined;
1031+
const { setVfsLibraryReader } = require('internal/ffi/vfs');
1032+
setVfsLibraryReader(null);
10011033
process.dlopen = originalDlopen;
10021034
hooksInstalled = false;
10031035
}

0 commit comments

Comments
 (0)