mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
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>
415 lines
10 KiB
JavaScript
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,
|
|
};
|