Files
node/lib/ffi.js
Trivikram Kamat 2bcc5dd2ee ffi: remove permission checks from dlclose and dlsym
The docs describe `ffi.dlclose(handle)` and `ffi.dlsym(handle, symbol)`
as equivalent to `handle.close()` and `handle.getSymbol(symbol)`, but
only the functions called `checkFFIPermission()`. After
`process.permission.drop('ffi')`, `ffi.dlclose(lib)` threw
`ERR_ACCESS_DENIED` while `lib.close()` succeeded.

Remove the checks so the functions defer to the handle. Permission is
already checked when the `DynamicLibrary` is constructed, and dropping
a permission does not revoke resources that are already open.

Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com>
Assisted-by: claude:opus-5.5
PR-URL: https://github.com/nodejs/node/pull/66426
Fixes: https://github.com/nodejs/node/issues/66425
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
2026-10-03 05:29:33 +00:00

413 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) {
handle.close();
}
function dlsym(handle, symbol) {
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,
};