From d614406fcec10a1009e013b7fa43d65c9612854d Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Fri, 2 Oct 2026 09:51:37 +0200 Subject: [PATCH 1/6] ffi: support function calls from pointers Add getFunctionFromPointer() to create callables from native addresses with explicit signatures. Reuse function preparation without populating named symbol caches. Keep the associated library alive and invalidate pointer callables when it is closed. Add coverage for conversions, vtables, permissions and garbage collection, and document pointer lifetime requirements. Assisted-by: GitHub Copilot Signed-off-by: Damiano Mazzella --- doc/api/ffi.md | 58 +++++++ lib/ffi.js | 6 + src/node_ffi.cc | 144 +++++++++++++--- src/node_ffi.h | 18 +- test/ffi/fixture_library/ffi_test_library.c | 37 +++++ test/ffi/test-ffi-calls.js | 174 ++++++++++++++++++++ test/ffi/test-ffi-permission-drop.js | 14 ++ test/ffi/test-ffi-weakref-calls.js | 78 +++++++++ typings/internalBinding/ffi.d.ts | 1 + 9 files changed, 506 insertions(+), 24 deletions(-) diff --git a/doc/api/ffi.md b/doc/api/ffi.md index d505ca745df8..03e8b7c6e959 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -440,6 +440,64 @@ console.log(add(20, 22)); console.log(add.pointer); ``` +### `library.getFunctionFromPointer(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.getFunctionFromPointer(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 82663a9710dd..109b836fd0fa 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -152,6 +152,7 @@ ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', { }); const rawGetFunction = DynamicLibrary.prototype.getFunction; +const rawGetFunctionFromPointer = DynamicLibrary.prototype.getFunctionFromPointer; 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.getFunctionFromPointer = function getFunctionFromPointer(pointer, signature) { + const raw = FunctionPrototypeCall(rawGetFunctionFromPointer, 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 f963913884c1..740462ed104a 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -106,6 +106,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. @@ -117,6 +125,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 @@ -158,7 +174,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; @@ -167,7 +186,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); @@ -175,14 +195,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); @@ -221,7 +240,7 @@ Maybe DynamicLibrary::PrepareFunction( } #endif - should_cache_function = true; + should_cache_function = !from_pointer; } else { fn = existing->second; @@ -309,7 +328,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 @@ -324,20 +342,41 @@ 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); - if (!CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate) - .MoveTo(&info->fast_metadata)) { - return {}; + if (optimize) { + std::shared_ptr fast_fn = CloneWithRawPointerArgNames(fn); + if (!CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate) + .MoveTo(&info->fast_metadata)) { + return {}; + } } 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. @@ -532,14 +571,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; } @@ -656,6 +687,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; @@ -921,6 +953,68 @@ void DynamicLibrary::GetFunction(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ret); } +void DynamicLibrary::GetFunctionFromPointer( + 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(); @@ -1397,6 +1491,10 @@ Local DynamicLibrary::GetConstructorTemplate( SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close); SetProtoDispose(isolate, tmpl, DynamicLibrary::Close); SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction); + SetProtoMethod(isolate, + tmpl, + "getFunctionFromPointer", + DynamicLibrary::GetFunctionFromPointer); 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 4b5df70124d8..602c9a94f138 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 = {}; @@ -136,6 +143,8 @@ class DynamicLibrary : public BaseObject { static void GetPath(const v8::FunctionCallbackInfo& args); static void GetFunction(const v8::FunctionCallbackInfo& args); + static void GetFunctionFromPointer( + 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); @@ -159,11 +168,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 CleanupHook(void* arg); static void CleanupFunctionInfo( const v8::WeakCallbackInfo& data); @@ -179,6 +194,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 3e24b7cd3d31..25682c3b19e3 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 338a728bd227..998194da6f9e 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.getFunctionFromPointer(address, signature); + const second = lib.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(lib.getSymbol(name), signature); + assert.strictEqual(fn(...values), expected); + } + const identity = lib.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(value, signature), { + code: 'ERR_INVALID_ARG_VALUE', + }); + } + for (const value of [undefined, null, 1, 'signature', []]) { + assert.throws(() => lib.getFunctionFromPointer(address, value), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } + assert.throws(() => lib.getFunctionFromPointer(address, { return: 'unknown' }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.strictEqual(lib.getFunctionFromPointer(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.getFunctionFromPointer(address, signature), + (error) => error === failure); + Object.defineProperty(signature, property, { + get() { + lib.close(); + return property === 'arguments' ? ['i32', 'i32'] : 'i32'; + }, + }); + assert.throws(() => lib.getFunctionFromPointer(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.getFunctionFromPointer(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..2f8c1102e6b3 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.getFunctionFromPointer(address, signature)(20, 22), 42); + assert.throws(() => lib.getFunctionFromPointer(address, { + get arguments() { + process.permission.drop('ffi'); + return ['i32', 'i32']; + }, + return: 'i32', + }), common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'FFI' })); + assert.throws(() => lib.getFunctionFromPointer(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..5f7670900f00 100644 --- a/test/ffi/test-ffi-weakref-calls.js +++ b/test/ffi/test-ffi-weakref-calls.js @@ -8,6 +8,84 @@ 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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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 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 bebf2afa24d0..eda6e9f18aa8 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; + getFunctionFromPointer(pointer: bigint, signature: FunctionSignature): FFIFunction; getFunctions(): Record; getFunctions( definitions: Record, From 39d25c0b83a3551c4207b71ac66bc9bb0a5b86dd Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Fri, 2 Oct 2026 11:07:09 +0200 Subject: [PATCH 2/6] ffi: fix formatting for pointer callables Apply clang-format corrections to pointer callable bindings and the FFI test fixture to satisfy the format-cpp CI check. Signed-off-by: Damiano Mazzella --- src/node_ffi.cc | 9 ++++----- test/ffi/fixture_library/ffi_test_library.c | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 740462ed104a..d8f63907cd6e 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -965,8 +965,7 @@ void DynamicLibrary::GetFunctionFromPointer( bool lossless; uint64_t address = args[0].As()->Uint64Value(&lossless); if (!lossless || address == 0 || - address > static_cast( - std::numeric_limits::max())) { + address > static_cast(std::numeric_limits::max())) { THROW_ERR_INVALID_ARG_VALUE(env, "Invalid function pointer"); return; } @@ -1492,9 +1491,9 @@ Local DynamicLibrary::GetConstructorTemplate( SetProtoDispose(isolate, tmpl, DynamicLibrary::Close); SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction); SetProtoMethod(isolate, - tmpl, - "getFunctionFromPointer", - DynamicLibrary::GetFunctionFromPointer); + tmpl, + "getFunctionFromPointer", + DynamicLibrary::GetFunctionFromPointer); SetProtoMethod(isolate, tmpl, "getFunctions", DynamicLibrary::GetFunctions); SetProtoMethod(isolate, tmpl, "getSymbol", DynamicLibrary::GetSymbol); SetProtoMethod(isolate, tmpl, "getSymbols", DynamicLibrary::GetSymbols); diff --git a/test/ffi/fixture_library/ffi_test_library.c b/test/ffi/fixture_library/ffi_test_library.c index 25682c3b19e3..d04b55a2078f 100644 --- a/test/ffi/fixture_library/ffi_test_library.c +++ b/test/ffi/fixture_library/ffi_test_library.c @@ -44,7 +44,7 @@ FFI_EXPORT PointerTestObject* get_pointer_test_object(void) { } FFI_EXPORT uint64_t pointer_string_lengths(const char* first, - const char* second) { + const char* second) { return strlen(first) + strlen(second); } From 912f1091d18646f800f79df08df352e28721db3e Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Fri, 2 Oct 2026 13:47:55 +0200 Subject: [PATCH 3/6] test: cover library closure during pointer callable creation Verify that closing the library in the pointer setter rejects callable creation, leaves the escaped callable unusable, and allows its collection. Assisted-by: GitHub Copilot Signed-off-by: Damiano Mazzella --- test/ffi/test-ffi-weakref-calls.js | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/ffi/test-ffi-weakref-calls.js b/test/ffi/test-ffi-weakref-calls.js index 5f7670900f00..4777afe8743f 100644 --- a/test/ffi/test-ffi-weakref-calls.js +++ b/test/ffi/test-ffi-weakref-calls.js @@ -86,6 +86,38 @@ test('ffi failed pointer callable construction releases its registration', async } }); +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.getFunctionFromPointer(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()); From f2fd6f60c1ee8b4131f1246c440801b3282b319f Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Fri, 2 Oct 2026 16:22:56 +0200 Subject: [PATCH 4/6] ffi: rename getFunctionFromPointer to toFunction Signed-off-by: Damiano Mazzella --- doc/api/ffi.md | 4 ++-- lib/ffi.js | 6 ++--- src/node_ffi.cc | 8 ++----- src/node_ffi.h | 3 +-- test/ffi/test-ffi-calls.js | 36 ++++++++++++++-------------- test/ffi/test-ffi-permission-drop.js | 6 ++--- test/ffi/test-ffi-weakref-calls.js | 10 ++++---- typings/internalBinding/ffi.d.ts | 2 +- 8 files changed, 35 insertions(+), 40 deletions(-) diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 03e8b7c6e959..3a8adb9378fc 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -440,7 +440,7 @@ console.log(add(20, 22)); console.log(add.pointer); ``` -### `library.getFunctionFromPointer(pointer, signature)` +### `library.toFunction(pointer, signature)` * `pointer` {bigint} * `signature` {Object} @@ -457,7 +457,7 @@ const { DynamicLibrary, suffix } = require('node:ffi'); const lib = new DynamicLibrary(`./mylib.${suffix}`); try { const address = lib.getSymbol('add_i32'); - const add = lib.getFunctionFromPointer(address, { + const add = lib.toFunction(address, { arguments: ['int32', 'int32'], return: 'int32', }); diff --git a/lib/ffi.js b/lib/ffi.js index 109b836fd0fa..f3295ad0ee77 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -152,7 +152,7 @@ ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', { }); const rawGetFunction = DynamicLibrary.prototype.getFunction; -const rawGetFunctionFromPointer = DynamicLibrary.prototype.getFunctionFromPointer; +const rawToFunction = DynamicLibrary.prototype.toFunction; const rawGetFunctions = DynamicLibrary.prototype.getFunctions; const rawClose = DynamicLibrary.prototype.close; @@ -181,8 +181,8 @@ DynamicLibrary.prototype.getFunction = function getFunction(name, signature) { return wrapFFIFunction(raw, this); }; -DynamicLibrary.prototype.getFunctionFromPointer = function getFunctionFromPointer(pointer, signature) { - const raw = FunctionPrototypeCall(rawGetFunctionFromPointer, this, pointer, signature); +DynamicLibrary.prototype.toFunction = function toFunction(pointer, signature) { + const raw = FunctionPrototypeCall(rawToFunction, this, pointer, signature); return wrapFFIFunction(raw, this); }; diff --git a/src/node_ffi.cc b/src/node_ffi.cc index d8f63907cd6e..5d13705e1b10 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -953,8 +953,7 @@ void DynamicLibrary::GetFunction(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ret); } -void DynamicLibrary::GetFunctionFromPointer( - const FunctionCallbackInfo& args) { +void DynamicLibrary::ToFunction(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); @@ -1490,10 +1489,7 @@ Local DynamicLibrary::GetConstructorTemplate( SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close); SetProtoDispose(isolate, tmpl, DynamicLibrary::Close); SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction); - SetProtoMethod(isolate, - tmpl, - "getFunctionFromPointer", - DynamicLibrary::GetFunctionFromPointer); + 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 602c9a94f138..86737273e284 100644 --- a/src/node_ffi.h +++ b/src/node_ffi.h @@ -143,8 +143,7 @@ class DynamicLibrary : public BaseObject { static void GetPath(const v8::FunctionCallbackInfo& args); static void GetFunction(const v8::FunctionCallbackInfo& args); - static void GetFunctionFromPointer( - 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); diff --git a/test/ffi/test-ffi-calls.js b/test/ffi/test-ffi-calls.js index 998194da6f9e..3dbd1a905d13 100644 --- a/test/ffi/test-ffi-calls.js +++ b/test/ffi/test-ffi-calls.js @@ -16,8 +16,8 @@ test('ffi function pointers are independent callables with library guards', () = try { const signature = { arguments: ['i32', 'i32'], return: 'i32' }; const address = lib.getSymbol('add_i32'); - const first = lib.getFunctionFromPointer(address, signature); - const second = lib.getFunctionFromPointer(address, signature); + 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); @@ -26,11 +26,11 @@ test('ffi function pointers are independent callables with library guards', () = assert.deepStrictEqual(Object.keys(lib.getFunctions()), []); const named = lib.getFunction('add_i32', signature); assert.notStrictEqual(named, first); - assert.strictEqual(lib.getFunctionFromPointer(named.pointer, signature)(1, 2), 3); + 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.getFunctionFromPointer(address, { + const different = lib.toFunction(address, { arguments: ['f64'], return: 'f64', }); assert.notStrictEqual(different, first); @@ -38,7 +38,7 @@ test('ffi function pointers are independent callables with library guards', () = for (const fn of [first, second, different]) { assert.throws(() => fn(), { code: 'ERR_FFI_LIBRARY_CLOSED' }); } - assert.throws(() => lib.getFunctionFromPointer(address, signature), { + assert.throws(() => lib.toFunction(address, signature), { code: 'ERR_FFI_LIBRARY_CLOSED', }); } finally { @@ -53,7 +53,7 @@ test('ffi calls function pointers returned by C and stored in vtables', () => { get_pointer_size: { arguments: [], return: 'u32' }, }); try { - const increment = lib.getFunctionFromPointer(functions.get_function_pointer(), { + const increment = lib.toFunction(functions.get_function_pointer(), { arguments: ['i32'], return: 'i32', }); assert.strictEqual(increment(41), 42); @@ -61,7 +61,7 @@ test('ffi calls function pointers returned by C and stored in vtables', () => { (address) => ffi.getUint64(address) : (address) => BigInt(ffi.getUint32(address)); const object = functions.get_pointer_test_object(); const vtable = readPointer(object); - const method = lib.getFunctionFromPointer(readPointer(vtable), { + const method = lib.toFunction(readPointer(vtable), { arguments: ['pointer', 'i32'], return: 'i32', }); assert.strictEqual(method(object, 2), 42); @@ -84,10 +84,10 @@ test('ffi function pointers reuse scalar, buffer and string conversions', () => ['pointer_string_lengths', { arguments: ['string', 'string'], return: 'u64' }, ['first', 'second'], 11n], ]) { - const fn = lib.getFunctionFromPointer(lib.getSymbol(name), signature); + const fn = lib.toFunction(lib.getSymbol(name), signature); assert.strictEqual(fn(...values), expected); } - const identity = lib.getFunctionFromPointer(lib.getSymbol('identity_pointer'), { + const identity = lib.toFunction(lib.getSymbol('identity_pointer'), { arguments: ['pointer'], return: 'pointer', }); const buffer = Buffer.from([1, 2, 3]); @@ -96,7 +96,7 @@ test('ffi function pointers reuse scalar, buffer and string conversions', () => assert.strictEqual(identity(view), ffi.getRawPointer(view)); assert.strictEqual(identity(null), 0n); assert.strictEqual(identity(undefined), 0n); - const lengths = lib.getFunctionFromPointer(lib.getSymbol('pointer_string_lengths'), { + 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' }); @@ -113,7 +113,7 @@ test('ffi function pointers reject invalid addresses and signatures', () => { const address = lib.getSymbol('add_i32'); for (const value of [undefined, null, 1, '1', {}, { valueOf: common.mustNotCall() }]) { - assert.throws(() => lib.getFunctionFromPointer(value, signature), { + assert.throws(() => lib.toFunction(value, signature), { code: 'ERR_INVALID_ARG_TYPE', }); } @@ -121,19 +121,19 @@ test('ffi function pointers reject invalid addresses and signatures', () => { arguments: [], return: 'u32', })(); for (const value of [0n, -1n, 2n ** BigInt(pointerSize * 8), 2n ** 64n]) { - assert.throws(() => lib.getFunctionFromPointer(value, signature), { + assert.throws(() => lib.toFunction(value, signature), { code: 'ERR_INVALID_ARG_VALUE', }); } for (const value of [undefined, null, 1, 'signature', []]) { - assert.throws(() => lib.getFunctionFromPointer(address, value), { + assert.throws(() => lib.toFunction(address, value), { code: 'ERR_INVALID_ARG_TYPE', }); } - assert.throws(() => lib.getFunctionFromPointer(address, { return: 'unknown' }), { + assert.throws(() => lib.toFunction(address, { return: 'unknown' }), { code: 'ERR_INVALID_ARG_VALUE', }); - assert.strictEqual(lib.getFunctionFromPointer(address, signature)(20, 22), 42); + assert.strictEqual(lib.toFunction(address, signature)(20, 22), 42); } finally { lib.close(); } @@ -147,7 +147,7 @@ test('ffi function pointer signature getters preserve errors and closure', () => 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.getFunctionFromPointer(address, signature), + assert.throws(() => lib.toFunction(address, signature), (error) => error === failure); Object.defineProperty(signature, property, { get() { @@ -155,7 +155,7 @@ test('ffi function pointer signature getters preserve errors and closure', () => return property === 'arguments' ? ['i32', 'i32'] : 'i32'; }, }); - assert.throws(() => lib.getFunctionFromPointer(address, signature), { + assert.throws(() => lib.toFunction(address, signature), { code: 'ERR_FFI_LIBRARY_CLOSED', }); } finally { @@ -166,7 +166,7 @@ test('ffi function pointer signature getters preserve errors and closure', () => test('ffi function pointer guards survive optimized JavaScript call sites', () => { const { lib } = ffi.dlopen(libraryPath); - const fn = lib.getFunctionFromPointer(lib.getSymbol('add_i32'), { + const fn = lib.toFunction(lib.getSymbol('add_i32'), { arguments: ['i32', 'i32'], return: 'i32', }); function call(first, second) { diff --git a/test/ffi/test-ffi-permission-drop.js b/test/ffi/test-ffi-permission-drop.js index 2f8c1102e6b3..a20ff8e67053 100644 --- a/test/ffi/test-ffi-permission-drop.js +++ b/test/ffi/test-ffi-permission-drop.js @@ -32,15 +32,15 @@ function openLibrary() { const { lib } = ffi.dlopen(libraryPath); const address = lib.getSymbol('add_i32'); const signature = { arguments: ['i32', 'i32'], return: 'i32' }; - assert.strictEqual(lib.getFunctionFromPointer(address, signature)(20, 22), 42); - assert.throws(() => lib.getFunctionFromPointer(address, { + 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.getFunctionFromPointer(address, signature), + assert.throws(() => lib.toFunction(address, signature), common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'FFI' })); lib.close(); process.permission.drop('ffi'); diff --git a/test/ffi/test-ffi-weakref-calls.js b/test/ffi/test-ffi-weakref-calls.js index 4777afe8743f..b16350849265 100644 --- a/test/ffi/test-ffi-weakref-calls.js +++ b/test/ffi/test-ffi-weakref-calls.js @@ -11,7 +11,7 @@ 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.getFunctionFromPointer(library.lib.getSymbol('add_i32'), { + const fn = library.lib.toFunction(library.lib.getSymbol('add_i32'), { arguments: ['i32', 'i32'], return: 'i32', }); library = null; @@ -33,7 +33,7 @@ test('ffi function pointer registry does not retain callables', async (t) => { const address = lib.getSymbol('add_i32'); const refs = []; for (let index = 0; index < 100; index++) { - let fn = lib.getFunctionFromPointer(address, { + let fn = lib.toFunction(address, { arguments: ['i32', 'i32'], return: 'i32', }); refs.push(new WeakRef(fn)); @@ -41,7 +41,7 @@ test('ffi function pointer registry does not retain callables', async (t) => { } await gcUntil('ffi function pointer callables are collected', () => refs.every((ref) => ref.deref() === undefined)); - const fn = lib.getFunctionFromPointer(address, { + const fn = lib.toFunction(address, { arguments: ['i32', 'i32'], return: 'i32', }); t.assert.strictEqual(fn(20, 22), 42); @@ -66,7 +66,7 @@ test('ffi failed pointer callable construction releases its registration', async throw failure; }, }); - t.assert.throws(() => lib.getFunctionFromPointer(lib.getSymbol('add_i32'), { + t.assert.throws(() => lib.toFunction(lib.getSymbol('add_i32'), { arguments: ['i32', 'i32'], return: 'i32', }), (error) => error === failure); } finally { @@ -101,7 +101,7 @@ test('ffi pointer callable construction detects library closure', async (t) => { lib.close(); }, }); - t.assert.throws(() => lib.getFunctionFromPointer(address, { + t.assert.throws(() => lib.toFunction(address, { arguments: ['i32', 'i32'], return: 'i32', }), { code: 'ERR_FFI_LIBRARY_CLOSED' }); } finally { diff --git a/typings/internalBinding/ffi.d.ts b/typings/internalBinding/ffi.d.ts index eda6e9f18aa8..8ecadbeeb534 100644 --- a/typings/internalBinding/ffi.d.ts +++ b/typings/internalBinding/ffi.d.ts @@ -40,7 +40,7 @@ declare namespace InternalFFIBinding { close(): void; getFunction(name: string, signature: FunctionSignature): FFIFunction; - getFunctionFromPointer(pointer: bigint, signature: FunctionSignature): FFIFunction; + toFunction(pointer: bigint, signature: FunctionSignature): FFIFunction; getFunctions(): Record; getFunctions( definitions: Record, From 767bcddd3debf3e7083b3ce4855851aca48632e9 Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Tue, 3 Nov 2026 09:05:09 +0100 Subject: [PATCH 5/6] test: align ffi pointer tests with own property creation Verify that pointer callable creation ignores inherited pointer setters while preserving coverage for garbage collection and library closure. Assisted-by: GitHub Copilot --- test/ffi/test-ffi-weakref-calls.js | 45 +++++++++++++++++++----------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/test/ffi/test-ffi-weakref-calls.js b/test/ffi/test-ffi-weakref-calls.js index b16350849265..37758ea61af5 100644 --- a/test/ffi/test-ffi-weakref-calls.js +++ b/test/ffi/test-ffi-weakref-calls.js @@ -52,23 +52,29 @@ test('ffi function pointer registry does not retain callables', async (t) => { } }); -test('ffi failed pointer callable construction releases its registration', async (t) => { +test('ffi pointer callable construction ignores throwing inherited setters', 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; + let fn; + let setterCalls = 0; const failure = new Error('pointer setter failed'); try { Object.defineProperty(Function.prototype, 'pointer', { configurable: true, get() { return undefined; }, set() { - escaped = this; + setterCalls++; throw failure; }, }); - t.assert.throws(() => lib.toFunction(lib.getSymbol('add_i32'), { + fn = lib.toFunction(address, { arguments: ['i32', 'i32'], return: 'i32', - }), (error) => error === failure); + }); + t.assert.strictEqual(setterCalls, 0); + t.assert.strictEqual(Object.getOwnPropertyDescriptor(fn, 'pointer').value, address); + t.assert.strictEqual(fn(20, 22), 42); } finally { if (original === undefined) { delete Function.prototype.pointer; @@ -77,33 +83,37 @@ test('ffi failed pointer callable construction releases its registration', async } } 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); + const ref = new WeakRef(fn); + fn = null; + await gcUntil('ffi pointer callable with inherited setter is collected', + () => ref.deref() === undefined); } finally { lib.close(); } }); -test('ffi pointer callable construction detects library closure', async (t) => { +test('ffi pointer callable construction ignores inherited setters that close the library', 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; + let fn; + let setterCalls = 0; try { Object.defineProperty(Function.prototype, 'pointer', { configurable: true, get() { return undefined; }, set() { - escaped = this; + setterCalls++; lib.close(); }, }); - t.assert.throws(() => lib.toFunction(address, { + fn = lib.toFunction(address, { arguments: ['i32', 'i32'], return: 'i32', - }), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + }); + t.assert.strictEqual(setterCalls, 0); + t.assert.strictEqual(Object.getOwnPropertyDescriptor(fn, 'pointer').value, address); + t.assert.strictEqual(fn(20, 22), 42); } finally { if (original === undefined) { delete Function.prototype.pointer; @@ -111,9 +121,10 @@ test('ffi pointer callable construction detects library closure', async (t) => { Object.defineProperty(Function.prototype, 'pointer', original); } } - t.assert.throws(() => escaped(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' }); - const ref = new WeakRef(escaped); - escaped = null; + lib.close(); + t.assert.throws(() => fn(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + const ref = new WeakRef(fn); + fn = null; await gcUntil('ffi pointer callable from closed library is collected', () => ref.deref() === undefined); }); From 59bffd155ef60bc0d059292e35c3d84416003b1a Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Tue, 3 Nov 2026 18:23:42 +0100 Subject: [PATCH 6/6] ffi: support variadic function calls Add automatic inference and explicitly typed variadic tails. Apply C default argument promotions and prepare a per-call variadic CIF while preserving optimized paths for fixed signatures. Add tests for snprintf, mixed argument layouts, signature validation, callback reentrancy and garbage-collection lifetimes. Update FFI documentation and typings. Signed-off-by: Damiano Mazzella --- src/node_ffi.cc | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 5d13705e1b10..317af5512942 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -687,7 +687,6 @@ 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; @@ -996,7 +995,6 @@ void DynamicLibrary::ToFunction(const FunctionCallbackInfo& args) { 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)) { @@ -1004,12 +1002,6 @@ void DynamicLibrary::ToFunction(const FunctionCallbackInfo& args) { 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); }