diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 342acfbec503..cf24422347e1 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -433,6 +433,64 @@ console.log(add(20, 22)); console.log(add.pointer); ``` +### `library.toFunction(pointer, signature)` + +* `pointer` {bigint} +* `signature` {Object} +* Returns: {Function} + +Creates a callable JavaScript wrapper for a native function address. The pointer +must be a nonzero, non-negative `bigint` that fits the platform's pointer width. +It can come from a resolved symbol, a native function's return value, or a +function pointer stored in native memory, such as a vtable slot. + +```cjs +const { DynamicLibrary, suffix } = require('node:ffi'); + +const lib = new DynamicLibrary(`./mylib.${suffix}`); +try { + const address = lib.getSymbol('add_i32'); + const add = lib.toFunction(address, { + arguments: ['int32', 'int32'], + return: 'int32', + }); + console.log(add(20, 22)); + console.log(add.pointer === address); +} finally { + lib.close(); +} +``` + +Argument and return conversions follow the same rules as `getFunction()`. +This method supports fixed signatures using the platform's default calling +convention. Explicit calling convention selection and structures passed or +returned by value are not supported. + +Each call creates a distinct wrapper. Multiple signatures can be associated +with the same address, but the caller is responsible for their correctness. +These wrappers do not appear in `library.functions`, `library.getFunctions()`, +`library.symbols`, or `library.getSymbols()` unless a symbol was separately +resolved by name. They use the generic libffi call path rather than Fast API +or SharedBuffer invokers. + +The wrapper keeps the associated library alive. Calling it after +`library.close()` throws `ERR_FFI_LIBRARY_CLOSED`. FFI permission is required +when creating the wrapper, including after permission has been revoked on an +already-open library. + +**The associated library does not establish ownership or validity of the +address.** Node.js cannot determine whether the pointer refers to executable +code, matches the signature, or is still valid. Passing a data pointer, using +an incorrect signature, or calling code that has been unloaded can crash the +process or corrupt memory. + +The caller must keep the actual code and any native object used by the call +alive. For example, associating a COM vtable method with `ole32.dll` does not +retain the COM object or the module implementing that method. The caller must +manage its native references and lifetime. A wrapper for a callback pointer +also becomes unsafe if that callback is unregistered, even if the associated +library remains open. + ### `library.getFunctions([definitions])` * `definitions` {Object} diff --git a/lib/ffi.js b/lib/ffi.js index 5cd7c4b354ab..246b714de0ae 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -152,6 +152,7 @@ ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', { }); const rawGetFunction = DynamicLibrary.prototype.getFunction; +const rawToFunction = DynamicLibrary.prototype.toFunction; const rawGetFunctions = DynamicLibrary.prototype.getFunctions; const rawClose = DynamicLibrary.prototype.close; @@ -180,6 +181,11 @@ DynamicLibrary.prototype.getFunction = function getFunction(name, signature) { return wrapFFIFunction(raw, this); }; +DynamicLibrary.prototype.toFunction = function toFunction(pointer, signature) { + const raw = FunctionPrototypeCall(rawToFunction, this, pointer, signature); + return wrapFFIFunction(raw, this); +}; + DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) { const raw = definitions === undefined ? FunctionPrototypeCall(rawGetFunctions, this) : diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 4ef22a80a428..768d9bc46db4 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -90,6 +90,14 @@ void DynamicLibrary::MemoryInfo(MemoryTracker* tracker) const { sizeof(decltype(function_wrappers_)::value_type), "std::unordered_map>"); + tracker->TrackFieldWithSize( + "pointer_functions", + pointer_functions_ == nullptr + ? 0 + : sizeof(std::unordered_set) + + pointer_functions_->size() * sizeof(FFIFunction*), + "std::unordered_set"); + // FFIFunctionInfo instances and their sb_backing ArrayBuffers are // owned by V8 function wrappers and reachable only via weak references, // so they are deliberately not counted here. @@ -101,6 +109,14 @@ void DynamicLibrary::Close() { fn->ptr = nullptr; } + if (pointer_functions_ != nullptr) { + for (FFIFunction* fn : *pointer_functions_) { + fn->closed = true; + fn->ptr = nullptr; + } + pointer_functions_->clear(); + } + // Closing the library invalidates all registered callbacks. Node.js does not // track or revoke callback pointers that have already been handed to native // code. If native code calls a callback pointer after `close()` or @@ -142,7 +158,10 @@ Maybe DynamicLibrary::ResolveSymbol(Environment* env, } Maybe DynamicLibrary::PrepareFunction( - Environment* env, const std::string& name, Local signature) { + Environment* env, + const std::string& name, + Local signature, + void* ptr) { std::shared_ptr fn; FunctionSignature parsed; @@ -151,7 +170,8 @@ Maybe DynamicLibrary::PrepareFunction( } // Look up the cache only after parsing: the signature's getters run user // code that may close the library, which clears `functions_`. - auto existing = functions_.find(name); + const bool from_pointer = ptr != nullptr; + auto existing = from_pointer ? functions_.end() : functions_.find(name); auto [return_type, args, return_type_name, arg_type_names] = std::move(parsed); @@ -159,14 +179,13 @@ Maybe DynamicLibrary::PrepareFunction( bool should_cache_function = false; if (existing == functions_.end()) { - void* ptr; - - if (!ResolveSymbol(env, name).To(&ptr)) { - return {}; + if (!from_pointer) { + if (!ResolveSymbol(env, name).To(&ptr)) { + return {}; + } + should_cache_symbol = symbols_.find(name) == symbols_.end(); } - should_cache_symbol = symbols_.find(name) == symbols_.end(); - fn = std::make_shared(); fn->ptr = ptr; fn->args = std::move(args); @@ -205,7 +224,7 @@ Maybe DynamicLibrary::PrepareFunction( } #endif - should_cache_function = true; + should_cache_function = !from_pointer; } else { fn = existing->second; @@ -267,7 +286,6 @@ MaybeLocal DynamicLibrary::CreateFunction( const std::string& name, const std::shared_ptr& fn) { Isolate* isolate = env->isolate(); - Local context = env->context(); // Creating a callable emits a trampoline, allocates an FFIFunctionInfo, and // on the SharedBuffer path allocates an ArrayBuffer, so reuse the one already @@ -282,17 +300,38 @@ MaybeLocal DynamicLibrary::CreateFunction( function_wrappers_.erase(cached); } + Local ret; + if (!BuildFunction(env, name, fn, true).ToLocal(&ret)) { + return {}; + } + function_wrappers_.emplace(name, Global(isolate, ret)) + .first->second.SetWeak(); + return ret; +} + +MaybeLocal DynamicLibrary::BuildFunction( + Environment* env, + const std::string& name, + const std::shared_ptr& fn, + bool optimize) { + Isolate* isolate = env->isolate(); + Local context = env->context(); auto info = FFIFunctionInfo::Create(env, fn, this); + if (!info) { + return {}; + } DCHECK_EQ(fn->args.size(), fn->arg_type_names.size()); // Try the generated Fast API path first. If metadata creation rejects the // signature, fall back to SharedBuffer for supported scalar shapes, then to // the generic libffi invoker. - std::shared_ptr fast_fn = CloneWithRawPointerArgNames(fn); - info->fast_metadata = CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate); + if (optimize) { + std::shared_ptr fast_fn = CloneWithRawPointerArgNames(fn); + info->fast_metadata = CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate); + } bool use_fast_api = info->fast_metadata != nullptr; - bool use_sb = !use_fast_api && IsSBEligibleSignature(*fn); + bool use_sb = optimize && !use_fast_api && IsSBEligibleSignature(*fn); bool has_ptr_args = use_sb && SignatureHasPointerArgs(*fn); // Signatures that need JS-side conversion or validation use a wrapper, as // do all fast signatures on platforms without a native library guard. @@ -485,14 +524,6 @@ MaybeLocal DynamicLibrary::CreateFunction( } } - // A strong handle would root the callable, which holds the library object - // through FFIFunctionInfo, so neither could ever be collected. Weaken the - // stored handle instead, so the cache lasts exactly as long as user code - // keeps a reference. SetWeak() runs after the move into the map because - // moving a handle relocates the underlying slot. - function_wrappers_.emplace(name, Global(isolate, ret)) - .first->second.SetWeak(); - return ret; } @@ -607,6 +638,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo& args) { std::vector values(expected_args, 0); std::vector ffi_args(expected_args, nullptr); std::vector strings; + strings.reserve(expected_args); for (unsigned int i = 0; i < expected_args; i++) { FFIArgumentCategory res; @@ -861,6 +893,66 @@ void DynamicLibrary::GetFunction(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ret); } +void DynamicLibrary::ToFunction(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); + + if (args.Length() < 1 || !args[0]->IsBigInt()) { + THROW_ERR_INVALID_ARG_TYPE(env, "Function pointer must be a bigint"); + return; + } + bool lossless; + uint64_t address = args[0].As()->Uint64Value(&lossless); + if (!lossless || address == 0 || + address > static_cast(std::numeric_limits::max())) { + THROW_ERR_INVALID_ARG_VALUE(env, "Invalid function pointer"); + return; + } + if (args.Length() < 2 || !args[1]->IsObject() || args[1]->IsArray()) { + THROW_ERR_INVALID_ARG_TYPE(env, "Function signature must be an object"); + return; + } + + DynamicLibrary* lib = Unwrap(args.This()); + if (lib->is_closed()) { + THROW_ERR_FFI_LIBRARY_CLOSED(env); + return; + } + Local signature = args[1].As(); + PreparedFunction prepared; + void* ptr = reinterpret_cast(static_cast(address)); + if (!lib->PrepareFunction(env, "", signature, ptr).To(&prepared)) { + return; + } + THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); + if (lib->is_closed()) { + THROW_ERR_FFI_LIBRARY_CLOSED(env); + return; + } + + auto fn = std::move(prepared.fn); + if (lib->pointer_functions_ == nullptr) { + lib->pointer_functions_ = + std::make_shared>(); + } + fn->pointer_registry = lib->pointer_functions_; + fn->closed = true; + lib->pointer_functions_->insert(fn.get()); + Local ret; + if (!lib->BuildFunction(env, "", fn, false).ToLocal(&ret)) { + lib->pointer_functions_->erase(fn.get()); + fn->ptr = nullptr; + return; + } + THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); + if (lib->is_closed()) { + THROW_ERR_FFI_LIBRARY_CLOSED(env); + return; + } + fn->closed = false; + args.GetReturnValue().Set(ret); +} + void DynamicLibrary::GetFunctions(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); @@ -1324,6 +1416,7 @@ Local DynamicLibrary::GetConstructorTemplate( SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close); SetProtoDispose(isolate, tmpl, DynamicLibrary::Close); SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction); + SetProtoMethod(isolate, tmpl, "toFunction", DynamicLibrary::ToFunction); SetProtoMethod(isolate, tmpl, "getFunctions", DynamicLibrary::GetFunctions); SetProtoMethod(isolate, tmpl, "getSymbol", DynamicLibrary::GetSymbol); SetProtoMethod(isolate, tmpl, "getSymbols", DynamicLibrary::GetSymbols); diff --git a/src/node_ffi.h b/src/node_ffi.h index 7758380138fd..d46abba5516a 100644 --- a/src/node_ffi.h +++ b/src/node_ffi.h @@ -12,6 +12,7 @@ #include #include #include +#include #include // libffi only accelerates reusable call plans on x86-64 System V. Other @@ -29,12 +30,18 @@ struct FFIFunction; struct FFIFunction { FFIFunction() = default; + ~FFIFunction() { + if (auto registry = pointer_registry.lock()) { + registry->erase(this); + } + } FFIFunction(const FFIFunction&) = delete; FFIFunction& operator=(const FFIFunction&) = delete; FFIFunction(FFIFunction&&) = delete; FFIFunction& operator=(FFIFunction&&) = delete; bool closed = false; + std::weak_ptr> pointer_registry; void* ptr = nullptr; ffi_cif cif = {}; @@ -133,6 +140,7 @@ class DynamicLibrary : public BaseObject { static void GetPath(const v8::FunctionCallbackInfo& args); static void GetFunction(const v8::FunctionCallbackInfo& args); + static void ToFunction(const v8::FunctionCallbackInfo& args); static void GetFunctions(const v8::FunctionCallbackInfo& args); static void GetSymbol(const v8::FunctionCallbackInfo& args); static void GetSymbols(const v8::FunctionCallbackInfo& args); @@ -156,11 +164,17 @@ class DynamicLibrary : public BaseObject { }; v8::Maybe PrepareFunction(Environment* env, const std::string& name, - v8::Local signature); + v8::Local signature, + void* ptr = nullptr); v8::MaybeLocal CreateFunction( Environment* env, const std::string& name, const std::shared_ptr& fn); + v8::MaybeLocal BuildFunction( + Environment* env, + const std::string& name, + const std::shared_ptr& fn, + bool optimize); static void CleanupFunctionInfo( const v8::WeakCallbackInfo& data); bool is_closed() const; @@ -175,6 +189,7 @@ class DynamicLibrary : public BaseObject { // which keeps the map from rooting the library through the wrapper's // FFIFunctionInfo. std::unordered_map> function_wrappers_; + std::shared_ptr> pointer_functions_; std::unordered_map> callbacks_; }; diff --git a/test/ffi/fixture_library/ffi_test_library.c b/test/ffi/fixture_library/ffi_test_library.c index 71c54a49fefe..34d6406a1f4b 100644 --- a/test/ffi/fixture_library/ffi_test_library.c +++ b/test/ffi/fixture_library/ffi_test_library.c @@ -11,6 +11,43 @@ // Integer operations. +typedef int32_t (*PointerUnaryFunction)(int32_t); + +FFI_EXPORT uint32_t get_pointer_size(void) { + return sizeof(uintptr_t); +} + +static int32_t pointer_increment(int32_t value) { + return value + 1; +} + +FFI_EXPORT PointerUnaryFunction get_function_pointer(void) { + return pointer_increment; +} + +typedef struct PointerTestObject PointerTestObject; +typedef int32_t (*PointerTestMethod)(PointerTestObject*, int32_t); + +struct PointerTestObject { + const PointerTestMethod* vtable; + int32_t base; +}; + +static int32_t pointer_object_add(PointerTestObject* self, int32_t value) { + return self->base + value; +} + +FFI_EXPORT PointerTestObject* get_pointer_test_object(void) { + static const PointerTestMethod vtable[] = {pointer_object_add}; + static PointerTestObject object = {vtable, 40}; + return &object; +} + +FFI_EXPORT uint64_t pointer_string_lengths(const char* first, + const char* second) { + return strlen(first) + strlen(second); +} + FFI_EXPORT void noop_void(void) {} FFI_EXPORT int8_t add_i8(int8_t a, int8_t b) { diff --git a/test/ffi/test-ffi-calls.js b/test/ffi/test-ffi-calls.js index bc3be0072db1..4a7ef2feb11f 100644 --- a/test/ffi/test-ffi-calls.js +++ b/test/ffi/test-ffi-calls.js @@ -11,6 +11,180 @@ function getLibrary() { return ffi.dlopen(libraryPath, fixtureSymbols); } +test('ffi function pointers are independent callables with library guards', () => { + const { lib } = ffi.dlopen(libraryPath); + try { + const signature = { arguments: ['i32', 'i32'], return: 'i32' }; + const address = lib.getSymbol('add_i32'); + const first = lib.toFunction(address, signature); + const second = lib.toFunction(address, signature); + assert.notStrictEqual(first, second); + assert.strictEqual(first.pointer, address); + assert.strictEqual(first(20, 22), 42); + assert.strictEqual(second(-10, 52), 42); + assert.deepStrictEqual(Object.keys(lib.functions), []); + assert.deepStrictEqual(Object.keys(lib.getFunctions()), []); + const named = lib.getFunction('add_i32', signature); + assert.notStrictEqual(named, first); + assert.strictEqual(lib.toFunction(named.pointer, signature)(1, 2), 3); + assert.throws(() => first(1), { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => first(1, 2, 3), { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => first(1, 2n), { code: 'ERR_INVALID_ARG_VALUE' }); + const different = lib.toFunction(address, { + arguments: ['f64'], return: 'f64', + }); + assert.notStrictEqual(different, first); + lib.close(); + for (const fn of [first, second, different]) { + assert.throws(() => fn(), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + } + assert.throws(() => lib.toFunction(address, signature), { + code: 'ERR_FFI_LIBRARY_CLOSED', + }); + } finally { + lib.close(); + } +}); + +test('ffi calls function pointers returned by C and stored in vtables', () => { + const { lib, functions } = ffi.dlopen(libraryPath, { + get_function_pointer: { arguments: [], return: 'pointer' }, + get_pointer_test_object: { arguments: [], return: 'pointer' }, + get_pointer_size: { arguments: [], return: 'u32' }, + }); + try { + const increment = lib.toFunction(functions.get_function_pointer(), { + arguments: ['i32'], return: 'i32', + }); + assert.strictEqual(increment(41), 42); + const readPointer = functions.get_pointer_size() === 8 ? + (address) => ffi.getUint64(address) : (address) => BigInt(ffi.getUint32(address)); + const object = functions.get_pointer_test_object(); + const vtable = readPointer(object); + const method = lib.toFunction(readPointer(vtable), { + arguments: ['pointer', 'i32'], return: 'i32', + }); + assert.strictEqual(method(object, 2), 42); + lib.close(); + assert.throws(() => method(object, 2), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + } finally { + lib.close(); + } +}); + +test('ffi function pointers reuse scalar, buffer and string conversions', () => { + const { lib } = ffi.dlopen(libraryPath); + try { + for (const [name, signature, values, expected] of [ + ['add_i8', { arguments: ['i8', 'i8'], return: 'i8' }, [120, 10], -126], + ['add_u64', { arguments: ['u64', 'u64'], return: 'u64' }, [20n, 22n], 42n], + ['add_f32', { arguments: ['f32', 'f32'], return: 'f32' }, [1.25, 2.75], 4], + ['multiply_f64', { arguments: ['f64', 'f64'], return: 'f64' }, [6, 7], 42], + ['noop_void', { arguments: [], return: 'void' }, [], undefined], + ['pointer_string_lengths', { arguments: ['string', 'string'], return: 'u64' }, + ['first', 'second'], 11n], + ]) { + const fn = lib.toFunction(lib.getSymbol(name), signature); + assert.strictEqual(fn(...values), expected); + } + const identity = lib.toFunction(lib.getSymbol('identity_pointer'), { + arguments: ['pointer'], return: 'pointer', + }); + const buffer = Buffer.from([1, 2, 3]); + assert.strictEqual(identity(buffer), ffi.getRawPointer(buffer)); + const view = new Uint8Array(buffer.buffer, buffer.byteOffset + 1, 1); + assert.strictEqual(identity(view), ffi.getRawPointer(view)); + assert.strictEqual(identity(null), 0n); + assert.strictEqual(identity(undefined), 0n); + const lengths = lib.toFunction(lib.getSymbol('pointer_string_lengths'), { + arguments: ['string', 'string'], return: 'u64', + }); + assert.throws(() => lengths('first\0', 'second'), { code: 'ERR_INVALID_ARG_VALUE' }); + assert.strictEqual(lengths('first', 'second'), 11n); + } finally { + lib.close(); + } +}); + +test('ffi function pointers reject invalid addresses and signatures', () => { + const { lib } = ffi.dlopen(libraryPath); + try { + const signature = { arguments: ['i32', 'i32'], return: 'i32' }; + const address = lib.getSymbol('add_i32'); + for (const value of [undefined, null, 1, '1', {}, + { valueOf: common.mustNotCall() }]) { + assert.throws(() => lib.toFunction(value, signature), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } + const pointerSize = lib.getFunction('get_pointer_size', { + arguments: [], return: 'u32', + })(); + for (const value of [0n, -1n, 2n ** BigInt(pointerSize * 8), 2n ** 64n]) { + assert.throws(() => lib.toFunction(value, signature), { + code: 'ERR_INVALID_ARG_VALUE', + }); + } + for (const value of [undefined, null, 1, 'signature', []]) { + assert.throws(() => lib.toFunction(address, value), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } + assert.throws(() => lib.toFunction(address, { return: 'unknown' }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.strictEqual(lib.toFunction(address, signature)(20, 22), 42); + } finally { + lib.close(); + } +}); + +test('ffi function pointer signature getters preserve errors and closure', () => { + for (const property of ['return', 'arguments']) { + const { lib } = ffi.dlopen(libraryPath); + try { + const address = lib.getSymbol('add_i32'); + const failure = new Error('pointer signature getter failed'); + const signature = { arguments: ['i32', 'i32'], return: 'i32' }; + Object.defineProperty(signature, property, { get() { throw failure; } }); + assert.throws(() => lib.toFunction(address, signature), + (error) => error === failure); + Object.defineProperty(signature, property, { + get() { + lib.close(); + return property === 'arguments' ? ['i32', 'i32'] : 'i32'; + }, + }); + assert.throws(() => lib.toFunction(address, signature), { + code: 'ERR_FFI_LIBRARY_CLOSED', + }); + } finally { + lib.close(); + } + } +}); + +test('ffi function pointer guards survive optimized JavaScript call sites', () => { + const { lib } = ffi.dlopen(libraryPath); + const fn = lib.toFunction(lib.getSymbol('add_i32'), { + arguments: ['i32', 'i32'], return: 'i32', + }); + function call(first, second) { + return fn(first, second); + } + try { + eval('%PrepareFunctionForOptimization(call)'); + assert.strictEqual(call(20, 22), 42); + eval('%OptimizeFunctionOnNextCall(call)'); + assert.strictEqual(call(20, 22), 42); + assert.throws(() => call(1, {}), { code: 'ERR_INVALID_ARG_VALUE' }); + lib.close(); + assert.throws(() => call(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + } finally { + lib.close(); + } +}); + test('ffi calls support integer arithmetic and char semantics', () => { const { lib, functions: symbols } = getLibrary(); try { diff --git a/test/ffi/test-ffi-permission-drop.js b/test/ffi/test-ffi-permission-drop.js index 275499503e6b..a20ff8e67053 100644 --- a/test/ffi/test-ffi-permission-drop.js +++ b/test/ffi/test-ffi-permission-drop.js @@ -29,6 +29,20 @@ function openLibrary() { } { + const { lib } = ffi.dlopen(libraryPath); + const address = lib.getSymbol('add_i32'); + const signature = { arguments: ['i32', 'i32'], return: 'i32' }; + assert.strictEqual(lib.toFunction(address, signature)(20, 22), 42); + assert.throws(() => lib.toFunction(address, { + get arguments() { + process.permission.drop('ffi'); + return ['i32', 'i32']; + }, + return: 'i32', + }), common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'FFI' })); + assert.throws(() => lib.toFunction(address, signature), + common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'FFI' })); + lib.close(); process.permission.drop('ffi'); assert.ok(!process.permission.has('ffi')); assert.throws(() => { diff --git a/test/ffi/test-ffi-weakref-calls.js b/test/ffi/test-ffi-weakref-calls.js index 4daa26a9e355..b16350849265 100644 --- a/test/ffi/test-ffi-weakref-calls.js +++ b/test/ffi/test-ffi-weakref-calls.js @@ -8,6 +8,116 @@ const test = require('node:test'); const ffi = require('node:ffi'); const { fixtureSymbols, libraryPath } = require('./ffi-test-common'); +test('ffi function pointer callables retain their library through GC', async (t) => { + let library = ffi.dlopen(libraryPath); + const ref = new WeakRef(library.lib); + const fn = library.lib.toFunction(library.lib.getSymbol('add_i32'), { + arguments: ['i32', 'i32'], return: 'i32', + }); + library = null; + try { + for (let index = 0; index < 5; index++) { + await gcUntil('ffi function pointer retains library', () => true, 1); + t.assert.notStrictEqual(ref.deref(), undefined); + t.assert.strictEqual(fn(20, 22), 42); + } + } finally { + ref.deref()?.close(); + } + t.assert.throws(() => fn(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' }); +}); + +test('ffi function pointer registry does not retain callables', async (t) => { + const { lib } = ffi.dlopen(libraryPath); + try { + const address = lib.getSymbol('add_i32'); + const refs = []; + for (let index = 0; index < 100; index++) { + let fn = lib.toFunction(address, { + arguments: ['i32', 'i32'], return: 'i32', + }); + refs.push(new WeakRef(fn)); + fn = null; + } + await gcUntil('ffi function pointer callables are collected', + () => refs.every((ref) => ref.deref() === undefined)); + const fn = lib.toFunction(address, { + arguments: ['i32', 'i32'], return: 'i32', + }); + t.assert.strictEqual(fn(20, 22), 42); + lib.close(); + t.assert.throws(() => fn(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + } finally { + lib.close(); + } +}); + +test('ffi failed pointer callable construction releases its registration', async (t) => { + const { lib } = ffi.dlopen(libraryPath); + const original = Object.getOwnPropertyDescriptor(Function.prototype, 'pointer'); + let escaped; + const failure = new Error('pointer setter failed'); + try { + Object.defineProperty(Function.prototype, 'pointer', { + configurable: true, + get() { return undefined; }, + set() { + escaped = this; + throw failure; + }, + }); + t.assert.throws(() => lib.toFunction(lib.getSymbol('add_i32'), { + arguments: ['i32', 'i32'], return: 'i32', + }), (error) => error === failure); + } finally { + if (original === undefined) { + delete Function.prototype.pointer; + } else { + Object.defineProperty(Function.prototype, 'pointer', original); + } + } + try { + t.assert.throws(() => escaped(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + const ref = new WeakRef(escaped); + escaped = null; + await gcUntil('ffi failed pointer callable is collected', () => ref.deref() === undefined); + } finally { + lib.close(); + } +}); + +test('ffi pointer callable construction detects library closure', async (t) => { + const { lib } = ffi.dlopen(libraryPath); + t.after(() => lib.close()); + const address = lib.getSymbol('add_i32'); + const original = Object.getOwnPropertyDescriptor(Function.prototype, 'pointer'); + let escaped; + try { + Object.defineProperty(Function.prototype, 'pointer', { + configurable: true, + get() { return undefined; }, + set() { + escaped = this; + lib.close(); + }, + }); + t.assert.throws(() => lib.toFunction(address, { + arguments: ['i32', 'i32'], return: 'i32', + }), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + } finally { + if (original === undefined) { + delete Function.prototype.pointer; + } else { + Object.defineProperty(Function.prototype, 'pointer', original); + } + } + t.assert.throws(() => escaped(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + const ref = new WeakRef(escaped); + escaped = null; + await gcUntil('ffi pointer callable from closed library is collected', + () => ref.deref() === undefined); +}); + test('ffi unrefCallback releases callback function', async (t) => { const { lib, functions: symbols } = ffi.dlopen(libraryPath, fixtureSymbols); t.after(() => lib.close()); diff --git a/typings/internalBinding/ffi.d.ts b/typings/internalBinding/ffi.d.ts index 0294d1987b03..b0f51b8f2c9c 100644 --- a/typings/internalBinding/ffi.d.ts +++ b/typings/internalBinding/ffi.d.ts @@ -40,6 +40,7 @@ declare namespace InternalFFIBinding { close(): void; getFunction(name: string, signature: FunctionSignature): FFIFunction; + toFunction(pointer: bigint, signature: FunctionSignature): FFIFunction; getFunctions(): Record; getFunctions( definitions: Record,