Files
node/lib/ffi.js
T
Matteo CollinaandPhilipp Dunkel a4bb025f28 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: https://github.com/nodejs/node/pull/65909
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: James M Snell <jasnell@gmail.com>
2026-09-15 08:33:52 +00:00

415 lines
10 KiB
JavaScript

'use strict';
const {
ArrayBufferPrototypeGetDetached,
DataViewPrototypeGetBuffer,
FunctionPrototypeCall,
ObjectDefineProperty,
ObjectFreeze,
ObjectGetOwnPropertyDescriptor,
ObjectKeys,
ObjectPrototypeToString,
ReflectConstruct,
SafeWeakMap,
SafeWeakRef,
SymbolDispose,
TypedArrayPrototypeGetBuffer,
} = primordials;
const { Buffer } = require('buffer');
const { emitExperimentalWarning } = require('internal/util');
const {
isDataView,
isArrayBufferView,
isSharedArrayBuffer,
} = require('internal/util/types');
const {
codes: {
ERR_ACCESS_DENIED,
ERR_INTERNAL_ASSERTION,
ERR_INVALID_ARG_TYPE,
ERR_OUT_OF_RANGE,
},
} = require('internal/errors');
const permission = require('internal/process/permission');
const {
validateInteger,
validateString,
} = require('internal/validators');
emitExperimentalWarning('FFI');
const {
DynamicLibrary: NativeDynamicLibrary,
getInt8,
getUint8,
getInt16,
getUint16,
getInt32,
getUint32,
getInt64,
getUint64,
getFloat32,
getFloat64,
getCurrentEventLoop,
exportBytes,
getRawPointer,
kFastArguments,
kSbArguments,
kSbReturn,
setInt8,
setUint8,
setInt16,
setUint16,
setInt32,
setUint32,
setInt64,
setUint64,
setFloat32,
setFloat64,
toString,
toBuffer,
toArrayBuffer,
} = internalBinding('ffi');
const {
wrapWithSharedBuffer,
} = require('internal/ffi-shared-buffer');
const {
markFastLibraryClosed,
wrapWithRawPointerConversions,
} = require('internal/ffi/fast-api');
function makeSignature(argumentTypes, returnType) {
return {
__proto__: null,
arguments: argumentTypes,
return: returnType,
};
}
// The native layer hands out one raw function per resolved symbol, so the
// wrapper composed around it is reused too, otherwise every read of
// `library.functions` would return callables that are not identical to the
// previous read's. The entry holds a WeakRef because V8 can keep a raw function
// alive after user code drops the wrapper, and a strong value would then pin
// every wrapper for the lifetime of the library.
const wrappedFunctions = new SafeWeakMap();
function wrapFFIFunction(rawFn, owner) {
if (rawFn === undefined || rawFn === null) {
return rawFn;
}
const cached = wrappedFunctions.get(rawFn)?.deref();
if (cached !== undefined) {
return cached;
}
let returnType;
const sbArguments = rawFn[kSbArguments];
const argumentTypes = sbArguments ?? rawFn[kFastArguments];
if (sbArguments !== undefined) {
returnType = rawFn[kSbReturn];
}
let wrapped = wrapWithSharedBuffer(
rawFn,
argumentTypes === undefined ? undefined : makeSignature(argumentTypes, returnType));
if (wrapped === rawFn) {
wrapped = wrapWithRawPointerConversions(rawFn, argumentTypes, owner);
}
wrappedFunctions.set(rawFn, new SafeWeakRef(wrapped));
return wrapped;
}
const { getVfsLibraryReader } = require('internal/ffi/vfs');
// A thin constructor in front of the native class so that a library inside
// a mounted virtual file system loads transparently: its bytes are read
// from the VFS and handed to the native constructor, which loads them from
// a private, self-cleaning image - the same way require() handles a native
// addon in a VFS. The reader is installed by the VFS while it is mounted
// (see internal/ffi/vfs), so no VFS code is ever loaded from here. The
// wrapper shares the native prototype, so instances and instanceof behave
// as if the native class were exposed directly.
function DynamicLibrary(path) {
if (new.target === undefined) {
// Let the native constructor produce its usual error.
return FunctionPrototypeCall(NativeDynamicLibrary, this, path);
}
const readVirtualLibrary = getVfsLibraryReader();
const binary =
readVirtualLibrary === null || typeof path !== 'string' ?
undefined : readVirtualLibrary(path);
return ReflectConstruct(NativeDynamicLibrary,
binary === undefined ? [path] : [path, binary],
new.target);
}
DynamicLibrary.prototype = NativeDynamicLibrary.prototype;
ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', {
__proto__: null,
configurable: true,
value: DynamicLibrary,
writable: true,
});
const rawGetFunction = DynamicLibrary.prototype.getFunction;
const rawGetFunctions = DynamicLibrary.prototype.getFunctions;
const rawClose = DynamicLibrary.prototype.close;
function close() {
const result = FunctionPrototypeCall(rawClose, this);
markFastLibraryClosed(this);
return result;
}
ObjectDefineProperty(DynamicLibrary.prototype, 'close', {
__proto__: null,
configurable: true,
value: close,
writable: true,
});
ObjectDefineProperty(DynamicLibrary.prototype, SymbolDispose, {
__proto__: null,
configurable: true,
value: close,
writable: true,
});
DynamicLibrary.prototype.getFunction = function getFunction(name, signature) {
const raw = FunctionPrototypeCall(rawGetFunction, this, name, signature);
return wrapFFIFunction(raw, this);
};
DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) {
const raw = definitions === undefined ?
FunctionPrototypeCall(rawGetFunctions, this) :
FunctionPrototypeCall(rawGetFunctions, this, definitions);
if (raw === undefined || raw === null) return raw;
const keys = ObjectKeys(raw);
const out = { __proto__: null };
for (let i = 0; i < keys.length; i++) {
const name = keys[i];
out[name] = wrapFFIFunction(raw[name], this);
}
return out;
};
{
const functionsDescriptor =
ObjectGetOwnPropertyDescriptor(DynamicLibrary.prototype, 'functions');
if (functionsDescriptor === undefined || functionsDescriptor.get === undefined) {
throw new ERR_INTERNAL_ASSERTION(
'FFI: DynamicLibrary.prototype.functions accessor not found or has no getter');
}
const origGetter = functionsDescriptor.get;
ObjectDefineProperty(DynamicLibrary.prototype, 'functions', {
__proto__: null,
configurable: true,
enumerable: functionsDescriptor.enumerable,
get() {
const raw = FunctionPrototypeCall(origGetter, this);
if (raw === undefined || raw === null) return raw;
const wrapped = { __proto__: null };
const keys = ObjectKeys(raw);
for (let i = 0; i < keys.length; i++) {
const name = keys[i];
wrapped[name] = wrapFFIFunction(raw[name], this);
}
return wrapped;
},
});
}
function checkFFIPermission() {
if (!permission.isEnabled()) {
return;
}
if (permission.has('ffi') || permission.isAuditMode()) {
return;
}
throw new ERR_ACCESS_DENIED(
'Access to this API has been restricted. Use --allow-ffi to manage permissions.',
'FFI');
}
function dlopen(path, definitions) {
checkFFIPermission();
const lib = new DynamicLibrary(path);
try {
const functions = definitions === undefined ? ObjectFreeze({ __proto__: null }) : lib.getFunctions(definitions);
return {
lib,
functions,
[SymbolDispose]() { lib.close(); },
};
} catch (error) {
lib.close();
throw error;
}
}
function dlclose(handle) {
checkFFIPermission();
handle.close();
}
function dlsym(handle, symbol) {
checkFFIPermission();
return handle.getSymbol(symbol);
}
function exportString(str, data, len, encoding = 'utf8') {
checkFFIPermission();
validateString(str, 'string');
validateString(encoding, 'encoding');
validateInteger(len, 'len', 0);
let terminatorSize;
switch (encoding.toLowerCase()) {
case 'ucs2':
case 'ucs-2':
case 'utf16le':
case 'utf-16le':
terminatorSize = 2;
break;
default:
terminatorSize = 1;
break;
}
const sourceBuffer = Buffer.from(str, encoding);
const requiredLength = sourceBuffer.length + terminatorSize;
if (len < requiredLength) {
throw new ERR_OUT_OF_RANGE('len', `>= ${requiredLength}`, len);
}
const terminated = Buffer.allocUnsafe(requiredLength);
sourceBuffer.copy(terminated);
terminated.fill(0, sourceBuffer.length);
exportBytes(terminated, data, len);
}
function exportBuffer(source, data, len) {
checkFFIPermission();
if (!Buffer.isBuffer(source)) {
throw new ERR_INVALID_ARG_TYPE('buffer', 'Buffer', source);
}
validateInteger(len, 'len', 0);
if (len < source.length) {
throw new ERR_OUT_OF_RANGE('len', `>= ${source.length}`, len);
}
exportBytes(source, data, len);
}
function exportArrayBuffer(source, data, len) {
checkFFIPermission();
if (ObjectPrototypeToString(source) !== '[object ArrayBuffer]') {
throw new ERR_INVALID_ARG_TYPE('arrayBuffer', 'ArrayBuffer', source);
}
validateInteger(len, 'len', 0);
if (len < source.byteLength) {
throw new ERR_OUT_OF_RANGE('len', `>= ${source.byteLength}`, len);
}
exportBytes(source, data, len);
}
function exportArrayBufferView(source, data, len) {
checkFFIPermission();
if (!isArrayBufferView(source)) {
throw new ERR_INVALID_ARG_TYPE('arrayBufferView', 'ArrayBufferView', source);
}
validateInteger(len, 'len', 0);
// Reading byteLength throws for a detached DataView. Let the native binding
// reject detached views consistently with detached ArrayBuffers.
const buffer = isDataView(source) ?
DataViewPrototypeGetBuffer(source) : TypedArrayPrototypeGetBuffer(source);
if ((isSharedArrayBuffer(buffer) ||
!ArrayBufferPrototypeGetDetached(buffer)) &&
len < source.byteLength) {
throw new ERR_OUT_OF_RANGE('len', `>= ${source.byteLength}`, len);
}
exportBytes(source, data, len);
}
const suffix = process.platform === 'win32' ? 'dll' : process.platform === 'darwin' ? 'dylib' : 'so';
const types = ObjectFreeze({
__proto__: null,
VOID: 'void',
POINTER: 'pointer',
BUFFER: 'buffer',
ARRAY_BUFFER: 'arraybuffer',
FUNCTION: 'function',
BOOL: 'bool',
CHAR: 'char',
STRING: 'string',
FLOAT: 'float',
DOUBLE: 'double',
INT_8: 'int8',
UINT_8: 'uint8',
INT_16: 'int16',
UINT_16: 'uint16',
INT_32: 'int32',
UINT_32: 'uint32',
INT_64: 'int64',
UINT_64: 'uint64',
FLOAT_32: 'float32',
FLOAT_64: 'float64',
});
module.exports = {
DynamicLibrary,
dlopen,
dlclose,
dlsym,
exportArrayBuffer,
exportArrayBufferView,
exportString,
exportBuffer,
getInt8,
getUint8,
getInt16,
getUint16,
getInt32,
getUint32,
getInt64,
getUint64,
getFloat32,
getFloat64,
getCurrentEventLoop,
getRawPointer,
setInt8,
setUint8,
setInt16,
setUint16,
setInt32,
setUint32,
setInt64,
setUint64,
setFloat32,
setFloat64,
suffix,
toString,
toArrayBuffer,
toBuffer,
types,
};