diff --git a/lib/ffi.js b/lib/ffi.js index 01c330953ac8..cde6cca7a86e 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -68,6 +68,7 @@ const { } = require('internal/ffi-shared-buffer'); const { + markFastLibraryClosed, wrapWithRawPointerConversions, } = require('internal/ffi/fast-api'); @@ -100,6 +101,27 @@ function wrapFFIFunction(rawFn, owner) { 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); diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 1b5487849169..0975760f940e 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1234,6 +1234,7 @@ E('ERR_FEATURE_UNAVAILABLE_ON_PLATFORM', 'The feature %s is unavailable on the current platform' + ', which is being used to run Node.js', TypeError); +E('ERR_FFI_LIBRARY_CLOSED', 'Library is closed', Error); E('ERR_FS_CP_DIR_TO_NON_DIR', 'Cannot overwrite non-directory with directory', SystemError); E('ERR_FS_CP_EEXIST', 'Target already exists', SystemError); diff --git a/lib/internal/ffi/fast-api.js b/lib/internal/ffi/fast-api.js index ebfaed92b27d..44e4c3a04e0f 100644 --- a/lib/internal/ffi/fast-api.js +++ b/lib/internal/ffi/fast-api.js @@ -5,6 +5,7 @@ const { NumberIsInteger, ObjectDefineProperty, ReflectApply, + SafeWeakMap, StringPrototypeIncludes, TypeError, } = primordials; @@ -25,9 +26,16 @@ const { kFastBufferInvoke, } = internalBinding('ffi'); +const { + codes: { + ERR_FFI_LIBRARY_CLOSED, + }, +} = require('internal/errors'); + const U64_MAX = 0xFFFFFFFFFFFFFFFFn; const I64_MAX = 0x7FFFFFFFFFFFFFFFn; const I64_MIN = -0x8000000000000000n; +const fastLibraryStates = new SafeWeakMap(); // These ranges mirror ToFFIArgument in src/ffi/types.cc. V8's Fast API // exposes narrow integers as 32-bit values and uses truncating BigInt @@ -202,7 +210,20 @@ function inheritMetadata(wrapper, rawFn, nargs) { return wrapper; } -function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) { +function markFastLibraryClosed(owner) { + const state = fastLibraryStates.get(owner); + if (state !== undefined) { + state.closed = true; + } +} + +function throwIfFastLibraryClosed(state) { + if (state.closed) { + throw new ERR_FFI_LIBRARY_CLOSED(); + } +} + +function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { if (rawFn === undefined || rawFn === null) { return rawFn; } @@ -213,11 +234,14 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) { return rawFn; } - const indexes = getFastArgumentIndexes(argumentTypes); - if (indexes === null) { - return rawFn; + let state = fastLibraryStates.get(owner); + if (state === undefined) { + state = { __proto__: null, closed: false }; + fastLibraryStates.set(owner, state); } + const indexes = getFastArgumentIndexes(argumentTypes) ?? []; + const stringState = { __proto__: null, buffers: [], @@ -233,6 +257,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) { const fastBufferInvoke = needsPointerLikeConversion(t0) ? rawFn[kFastBufferInvoke] : undefined; wrapper = function(a0) { + throwIfFastLibraryClosed(state); if (arguments.length !== 1) { throwFFIArgCountError(1, arguments.length); } @@ -262,6 +287,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) { const t0 = argumentTypes[0]; const t1 = argumentTypes[1]; wrapper = function(a0, a1) { + throwIfFastLibraryClosed(state); if (arguments.length !== 2) { throwFFIArgCountError(2, arguments.length); } @@ -283,6 +309,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) { const t1 = argumentTypes[1]; const t2 = argumentTypes[2]; wrapper = function(a0, a1, a2) { + throwIfFastLibraryClosed(state); if (arguments.length !== 3) { throwFFIArgCountError(3, arguments.length); } @@ -300,6 +327,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) { }; } else { wrapper = function(...args) { + throwIfFastLibraryClosed(state); if (args.length !== nargs) { throwFFIArgCountError(nargs, args.length); } @@ -332,5 +360,6 @@ module.exports = { convertPointerArg, hasPointerMemoryArg, hasStringPointerArg, + markFastLibraryClosed, wrapWithRawPointerConversions, }; diff --git a/src/ffi/fast.cc b/src/ffi/fast.cc index ea98bf8aa4f1..ca9a5715724b 100644 --- a/src/ffi/fast.cc +++ b/src/ffi/fast.cc @@ -241,12 +241,26 @@ extern "C" uintptr_t node_ffi_fast_buffer_data(v8::Local value, v8::Isolate* isolate = options != nullptr ? options->isolate : nullptr; if (isolate != nullptr) { + // No HandleScope is active during a Fast API call, so open one before + // creating the error object. + v8::HandleScope scope(isolate); THROW_ERR_INVALID_ARG_VALUE( isolate, "Argument %u must be a buffer or an ArrayBuffer", index); } return kInvalidBuffer; } +extern "C" void node_ffi_fast_library_closed(v8::Isolate* isolate) { + if (isolate != nullptr) { + // Fast API calls do not enter a HandleScope, and the generated trampolines + // call this helper directly. Building the error object allocates handles, + // so open a scope here. The scheduled exception lives on the isolate and + // outlives the scope. + v8::HandleScope scope(isolate); + THROW_ERR_FFI_LIBRARY_CLOSED(isolate); + } +} + FastFFIMetadata::~FastFFIMetadata() { // Metadata owns executable memory through `trampoline`; releasing it here // ties code lifetime to the V8 function's weak FFIFunctionInfo cleanup. @@ -265,7 +279,18 @@ bool IsFastCallSupported() { #endif } -std::unique_ptr CreateFastFFIMetadata(const FFIFunction& fn) { +bool IsFastLibraryGuardSupported() { +#if defined(__aarch64__) || defined(_M_ARM64) || \ + (defined(__x86_64__) && !defined(_WIN32)) + return true; +#else + return false; +#endif +} + +std::unique_ptr CreateFastFFIMetadata(const FFIFunction& fn, + const bool* closed, + v8::Isolate* isolate) { // Bail early if executable memory allocation doesn't work on this process // (missing MAP_JIT entitlement, hardened runtime, SELinux execmem, etc.). // The self-test runs once and caches the result. @@ -299,6 +324,7 @@ std::unique_ptr CreateFastFFIMetadata(const FFIFunction& fn) { std::vector args; args.reserve(fn.arg_type_names.size()); bool needs_bigint = NeedsBigIntRepresentation(result); + const bool guards_library = IsFastLibraryGuardSupported(); bool needs_callback_options = false; // Normalize public argument names into FastFFIType values while collecting // signature-wide flags required by V8 CFunctionInfo. @@ -320,8 +346,9 @@ std::unique_ptr CreateFastFFIMetadata(const FFIFunction& fn) { // The platform-specific trampoline is the executable entrypoint V8 calls. // If the platform rejects the signature, the whole fast metadata object is // discarded and the caller chooses another invocation path. + FastFFITrampolineConfig config{fn.ptr, closed, isolate}; if (!node_ffi_create_fast_trampoline( - fn.ptr, args.data(), args.size(), result, &metadata->trampoline)) { + config, args.data(), args.size(), result, &metadata->trampoline)) { return nullptr; } @@ -348,6 +375,7 @@ std::unique_ptr CreateFastFFIMetadata(const FFIFunction& fn) { : CFunctionInfo::Int64Representation::kNumber); metadata->c_function = v8::CFunction(metadata->trampoline.code, metadata->c_function_info.get()); + metadata->guards_library = guards_library; return metadata; } diff --git a/src/ffi/fast.h b/src/ffi/fast.h index b85191aade13..7aced6a7cc54 100644 --- a/src/ffi/fast.h +++ b/src/ffi/fast.h @@ -36,6 +36,12 @@ struct FastFFITrampoline { size_t size = 0; }; +struct FastFFITrampolineConfig { + void* target; + const bool* closed; + v8::Isolate* isolate; +}; + struct FastFFIMetadata { FastFFIMetadata() = default; ~FastFFIMetadata(); @@ -47,6 +53,7 @@ struct FastFFIMetadata { std::vector arg_info; std::unique_ptr c_function_info; v8::CFunction c_function; + bool guards_library = false; }; // Public detection queries. @@ -56,6 +63,7 @@ struct FastFFIMetadata { // of any particular signature — if this returns false, no signature can // use the fast-call path. bool IsFastCallSupported(); +bool IsFastLibraryGuardSupported(); bool SignatureNeedsRawPointerConversions(const FFIFunction& fn); bool SignatureNeedsFastIntegerValidation(const FFIFunction& fn); @@ -65,7 +73,9 @@ std::shared_ptr CloneWithRawPointerArgNames( const std::shared_ptr& fn); std::shared_ptr CloneWithFastBufferArgNames( const std::shared_ptr& fn); -std::unique_ptr CreateFastFFIMetadata(const FFIFunction& fn); +std::unique_ptr CreateFastFFIMetadata(const FFIFunction& fn, + const bool* closed, + v8::Isolate* isolate); } // namespace node::ffi @@ -73,11 +83,13 @@ extern "C" { uintptr_t node_ffi_fast_buffer_data(v8::Local value, v8::FastApiCallbackOptions* options, uint32_t index); -bool node_ffi_create_fast_trampoline(void* target, - const node::ffi::FastFFIType* args, - size_t argc, - node::ffi::FastFFIType result, - node::ffi::FastFFITrampoline* out); +void node_ffi_fast_library_closed(v8::Isolate* isolate); +bool node_ffi_create_fast_trampoline( + const node::ffi::FastFFITrampolineConfig& config, + const node::ffi::FastFFIType* args, + size_t argc, + node::ffi::FastFFIType result, + node::ffi::FastFFITrampoline* out); void node_ffi_free_fast_trampoline(node::ffi::FastFFITrampoline* trampoline); } diff --git a/src/ffi/platforms/arm64.cc b/src/ffi/platforms/arm64.cc index a3132966b560..df215ecafed3 100644 --- a/src/ffi/platforms/arm64.cc +++ b/src/ffi/platforms/arm64.cc @@ -81,6 +81,14 @@ uint32_t LdrXSp(unsigned reg, unsigned offset) { return 0xf94003e0 | ((offset / 8) << 10) | reg; } +uint32_t LdrbW(unsigned dst, unsigned base) { + return 0x39400000 | (base << 5) | dst; +} + +uint32_t CbzW(unsigned reg, unsigned instruction_offset) { + return 0x34000000 | ((instruction_offset & 0x7ffff) << 5) | reg; +} + uint32_t MovzW(unsigned dst, uint16_t value) { // Load a small immediate into a W register. The buffer helper's argument // index is uint32_t, but current Fast API signatures are capped well below @@ -213,14 +221,15 @@ bool ProtectCode(void* code, size_t code_size) { } // namespace extern "C" bool node_ffi_create_fast_trampoline( - void* target, + const node::ffi::FastFFITrampolineConfig& config, const node::ffi::FastFFIType* args, size_t argc, node::ffi::FastFFIType result, node::ffi::FastFFITrampoline* out) { // Null inputs mean the caller cannot safely create executable code for this // signature. Report rejection so the generic FFI path can be used instead. - if (target == nullptr || out == nullptr) { + if (config.target == nullptr || config.closed == nullptr || + config.isolate == nullptr || out == nullptr) { return false; } @@ -275,6 +284,24 @@ extern "C" bool node_ffi_create_fast_trampoline( // call can return through this generated trampoline safely. *cursor++ = kStpFpLrPreIndex; + // Fast calls bypass DynamicLibrary::InvokeFunction, so check the stable + // FFIFunction::closed flag before touching the target address. The open + // branch is the hot path. On close, schedule the standard JS exception and + // return; V8 checks for pending exceptions after Fast API calls. + EmitLoadX16(&cursor, reinterpret_cast(config.closed)); + *cursor++ = LdrbW(17, 16); + uint32_t* open_branch = cursor++; + EmitLoadX16(&cursor, reinterpret_cast(config.isolate)); + *cursor++ = MovX(0, 16); + EmitLoadX16( + &cursor, reinterpret_cast(node_ffi_fast_library_closed)); + *cursor++ = kBlrX16; + *cursor++ = MovX(0, 31); + *cursor++ = kLdpFpLrPostIndex; + *cursor++ = kRet; + *open_branch = + CbzW(17, static_cast(cursor - open_branch)); + if (has_buffer_args) { // Buffer conversion calls a C++ helper before the target call, so spill all // incoming GP registers that may be clobbered by that helper. @@ -361,7 +388,7 @@ extern "C" bool node_ffi_create_fast_trampoline( // Tail of the trampoline: load the actual library symbol address and call it // with arguments now arranged according to the native ABI. - EmitLoadX16(&cursor, reinterpret_cast(target)); + EmitLoadX16(&cursor, reinterpret_cast(config.target)); *cursor++ = kBlrX16; if (has_buffer_args) { @@ -414,7 +441,7 @@ extern "C" void node_ffi_free_fast_trampoline( !(defined(__riscv) && __riscv_xlen == 64) && !defined(__s390x__) extern "C" bool node_ffi_create_fast_trampoline( - void* target, + const node::ffi::FastFFITrampolineConfig& config, const node::ffi::FastFFIType* args, size_t argc, node::ffi::FastFFIType result, diff --git a/src/ffi/platforms/loong64.cc b/src/ffi/platforms/loong64.cc index 6bc90358cfca..dda579b6c63f 100644 --- a/src/ffi/platforms/loong64.cc +++ b/src/ffi/platforms/loong64.cc @@ -74,12 +74,12 @@ void FreeCode(void* code, size_t code_size) { } // namespace extern "C" bool node_ffi_create_fast_trampoline( - void* target, + const node::ffi::FastFFITrampolineConfig& config, const node::ffi::FastFFIType* args, size_t argc, node::ffi::FastFFIType result, node::ffi::FastFFITrampoline* out) { - if (target == nullptr || out == nullptr || IsNarrowType(result)) { + if (config.target == nullptr || out == nullptr || IsNarrowType(result)) { return false; } @@ -126,7 +126,7 @@ extern "C" bool node_ffi_create_fast_trampoline( Emit32(&cursor, LdD(12, 12, 16)); // ld.d t0, t0, literal Emit32(&cursor, Jirl(0, 12, 0)); // jr t0 Emit32(&cursor, Or(0, 0, 0)); // nop; align literal to 8 bytes - Emit64(&cursor, reinterpret_cast(target)); + Emit64(&cursor, reinterpret_cast(config.target)); const size_t written = reinterpret_cast(cursor) - static_cast(code); diff --git a/src/ffi/platforms/ppc64.cc b/src/ffi/platforms/ppc64.cc index 78cd91440561..52808abac894 100644 --- a/src/ffi/platforms/ppc64.cc +++ b/src/ffi/platforms/ppc64.cc @@ -86,12 +86,12 @@ void FreeCode(void* code, size_t code_size) { } // namespace extern "C" bool node_ffi_create_fast_trampoline( - void* target, + const node::ffi::FastFFITrampolineConfig& config, const node::ffi::FastFFIType* args, size_t argc, node::ffi::FastFFIType result, node::ffi::FastFFITrampoline* out) { - if (target == nullptr || out == nullptr || IsNarrowType(result)) { + if (config.target == nullptr || out == nullptr || IsNarrowType(result)) { return false; } @@ -149,7 +149,7 @@ extern "C" bool node_ffi_create_fast_trampoline( if (gp_count % 2 == 0) { Emit32(&cursor, 0x60000000); // nop; align literal to 8 bytes } - Emit64(&cursor, reinterpret_cast(target)); + Emit64(&cursor, reinterpret_cast(config.target)); const size_t written = reinterpret_cast(cursor) - static_cast(code); @@ -179,7 +179,7 @@ extern "C" void node_ffi_free_fast_trampoline( #else extern "C" bool node_ffi_create_fast_trampoline( - void* target, + const node::ffi::FastFFITrampolineConfig& config, const node::ffi::FastFFIType* args, size_t argc, node::ffi::FastFFIType result, diff --git a/src/ffi/platforms/riscv64.cc b/src/ffi/platforms/riscv64.cc index 0497f38619a8..841b0db85f68 100644 --- a/src/ffi/platforms/riscv64.cc +++ b/src/ffi/platforms/riscv64.cc @@ -75,12 +75,12 @@ void FreeCode(void* code, size_t code_size) { } // namespace extern "C" bool node_ffi_create_fast_trampoline( - void* target, + const node::ffi::FastFFITrampolineConfig& config, const node::ffi::FastFFIType* args, size_t argc, node::ffi::FastFFIType result, node::ffi::FastFFITrampoline* out) { - if (target == nullptr || out == nullptr || IsNarrowType(result)) { + if (config.target == nullptr || out == nullptr || IsNarrowType(result)) { return false; } @@ -127,7 +127,7 @@ extern "C" bool node_ffi_create_fast_trampoline( Emit32(&cursor, Ld(5, 5, 16)); // ld t0, literal(t0) Emit32(&cursor, Jalr(0, 5, 0)); // jr t0 Emit32(&cursor, Addi(0, 0, 0)); // nop; align literal to 8 bytes - Emit64(&cursor, reinterpret_cast(target)); + Emit64(&cursor, reinterpret_cast(config.target)); const size_t written = reinterpret_cast(cursor) - static_cast(code); diff --git a/src/ffi/platforms/s390x.cc b/src/ffi/platforms/s390x.cc index dd8606de82bc..5ec44d9e486d 100644 --- a/src/ffi/platforms/s390x.cc +++ b/src/ffi/platforms/s390x.cc @@ -86,12 +86,12 @@ void FreeCode(void* code, size_t code_size) { } // namespace extern "C" bool node_ffi_create_fast_trampoline( - void* target, + const node::ffi::FastFFITrampolineConfig& config, const node::ffi::FastFFIType* args, size_t argc, node::ffi::FastFFIType result, node::ffi::FastFFITrampoline* out) { - if (target == nullptr || out == nullptr || IsNarrowType(result)) { + if (config.target == nullptr || out == nullptr || IsNarrowType(result)) { return false; } @@ -136,7 +136,7 @@ extern "C" bool node_ffi_create_fast_trampoline( // Load the target address from the literal pool into r1 and tail-branch. Emit48(&cursor, Lgrl(1, 4)); // lgrl r1, literal Emit16(&cursor, Br(1)); // br r1 - Emit64(&cursor, reinterpret_cast(target)); + Emit64(&cursor, reinterpret_cast(config.target)); const size_t written = cursor - static_cast(code); __builtin___clear_cache(static_cast(code), diff --git a/src/ffi/platforms/x64.cc b/src/ffi/platforms/x64.cc index 51de3c5452f3..e105eabc13db 100644 --- a/src/ffi/platforms/x64.cc +++ b/src/ffi/platforms/x64.cc @@ -211,6 +211,15 @@ void EmitCmp(uint8_t** cursor, unsigned lhs, unsigned rhs) { EmitModRM(cursor, rhs, lhs); } +void EmitCmpBytePtrZero(uint8_t** cursor, unsigned base) { + // cmp byte ptr [base], 0. The closed flag is a stable bool owned by the + // FFIFunction kept alive by this trampoline's FunctionTemplate data. + EmitRex(cursor, false, 7, base); + Emit8(cursor, 0x80); + Emit8(cursor, (7 << 3) | (base & 7)); + Emit8(cursor, 0); +} + void EmitXorEax(uint8_t** cursor) { // Return nullptr/zero after the helper has already scheduled the JS exception. Emit8(cursor, 0x31); @@ -327,14 +336,15 @@ void* AllocateCodeNear(uintptr_t target_address, size_t code_size) { } // namespace extern "C" bool node_ffi_create_fast_trampoline( - void* target, + const node::ffi::FastFFITrampolineConfig& config, const node::ffi::FastFFIType* args, size_t argc, node::ffi::FastFFIType result, node::ffi::FastFFITrampoline* out) { // Null inputs mean the caller cannot safely create executable code for this // signature. Report rejection so the generic FFI path can be used instead. - if (target == nullptr || out == nullptr) { + if (config.target == nullptr || config.closed == nullptr || + config.isolate == nullptr || out == nullptr) { return false; } @@ -377,13 +387,36 @@ extern "C" bool node_ffi_create_fast_trampoline( // Generate into writable anonymous memory first; the page is made executable // only after the instruction stream is complete and the instruction cache is // synchronized. - const uintptr_t target_address = reinterpret_cast(target); + const uintptr_t target_address = + reinterpret_cast(config.target); void* code = AllocateCodeNear(target_address, kCodeSize); if (code == MAP_FAILED) { return false; } uint8_t* cursor = static_cast(code); + + EmitMovImm64(&cursor, kR11, reinterpret_cast(config.closed)); + EmitCmpBytePtrZero(&cursor, kR11); + uint8_t* open_branch = cursor; + Emit8(&cursor, 0x74); // je open + Emit8(&cursor, 0); + + // The closed path is cold. Align the stack, pass the isolate to the helper, + // and return after it schedules ERR_FFI_LIBRARY_CLOSED. V8 checks for + // pending exceptions immediately after the Fast API call. + EmitPushRbp(&cursor); + EmitMovImm64(&cursor, kRdi, reinterpret_cast(config.isolate)); + EmitMovImm64( + &cursor, + kR11, + reinterpret_cast(node_ffi_fast_library_closed)); + EmitCall(&cursor, kR11); + EmitXorEax(&cursor); + EmitPopRbp(&cursor); + EmitRet(&cursor); + open_branch[1] = static_cast(cursor - (open_branch + 2)); + const bool tail_call = !has_buffer_args && !NeedsNarrow(result); if (!tail_call) { EmitPushRbp(&cursor); @@ -689,12 +722,12 @@ void FreeCode(void* code, size_t code_size) { } // namespace extern "C" bool node_ffi_create_fast_trampoline( - void* target, + const node::ffi::FastFFITrampolineConfig& config, const node::ffi::FastFFIType* args, size_t argc, node::ffi::FastFFIType result, node::ffi::FastFFITrampoline* out) { - if (target == nullptr || out == nullptr || argc > 3) { + if (config.target == nullptr || out == nullptr || argc > 3) { return false; } @@ -724,7 +757,8 @@ extern "C" bool node_ffi_create_fast_trampoline( } } - EmitMovImm64(&cursor, kR11, reinterpret_cast(target)); + EmitMovImm64( + &cursor, kR11, reinterpret_cast(config.target)); if (tail_call) { // The caller already provided Win64 shadow space for the trampoline; after // the receiver-slot shuffle, the target can reuse the same stack shape. diff --git a/src/node_ffi.cc b/src/node_ffi.cc index b05d09270126..17b4c13c4552 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -250,15 +250,16 @@ MaybeLocal DynamicLibrary::CreateFunction( // 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); + 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 has_ptr_args = use_sb && SignatureHasPointerArgs(*fn); - // Fast API signatures that need JS-side argument conversion or range checks - // use a wrapper with the native type names attached as hidden metadata. + // Signatures that need JS-side conversion or validation use a wrapper, as + // do all fast signatures on platforms without a native library guard. bool needs_fast_argument_wrapper = use_fast_api && (SignatureNeedsRawPointerConversions(*fn) || - SignatureNeedsFastIntegerValidation(*fn)); + SignatureNeedsFastIntegerValidation(*fn) || + !info->fast_metadata->guards_library); // A single pointer-like parameter can get a separate Buffer-aware Fast API // entrypoint so Buffer calls avoid JS pointer extraction. bool needs_fast_buffer_invoke = @@ -407,7 +408,8 @@ MaybeLocal DynamicLibrary::CreateFunction( // argument is Buffer/ArrayBuffer-backed memory. std::shared_ptr fast_buffer_fn = CloneWithFastBufferArgNames(fn); - info->fast_buffer_metadata = CreateFastFFIMetadata(*fast_buffer_fn); + info->fast_buffer_metadata = + CreateFastFFIMetadata(*fast_buffer_fn, &fn->closed, isolate); if (info->fast_buffer_metadata != nullptr) { // Store the secondary invoker on the primary raw function under a hidden // Symbol. Keeping it separate avoids overloading SharedBuffer slow-path diff --git a/test/ffi/test-ffi-dynamic-library.js b/test/ffi/test-ffi-dynamic-library.js index 88897be6eaec..2a6aa4129e95 100644 --- a/test/ffi/test-ffi-dynamic-library.js +++ b/test/ffi/test-ffi-dynamic-library.js @@ -1,4 +1,4 @@ -// Flags: --experimental-ffi --expose-gc +// Flags: --experimental-ffi --expose-gc --allow-natives-syntax 'use strict'; const common = require('../common'); common.skipIfFFIMissing(); @@ -209,6 +209,27 @@ test('closed libraries reject subsequent operations', () => { assert.throws(() => lib.getSymbol('add_i32'), /Library is closed/); }); +test('optimized fast calls reject calls after the library is closed', () => { + const { lib, functions } = ffi.dlopen(libraryPath, { + multiply_f64: fixtureSymbols.multiply_f64, + }); + + function hot(a, b) { + return functions.multiply_f64(a, b); + } + + eval('%PrepareFunctionForOptimization(hot)'); + assert.strictEqual(hot(2, 3), 6); + eval('%OptimizeFunctionOnNextCall(hot)'); + assert.strictEqual(hot(2, 3), 6); + + lib.close(); + assert.throws(() => hot(2, 3), { + code: 'ERR_FFI_LIBRARY_CLOSED', + message: 'Library is closed', + }); +}); + test('DynamicLibrary supports Symbol.dispose', () => { const lib = new ffi.DynamicLibrary(libraryPath); const addI32 = lib.getFunction('add_i32', fixtureSymbols.add_i32);