Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/ffi.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const {
} = require('internal/ffi-shared-buffer');

const {
markFastLibraryClosed,
wrapWithRawPointerConversions,
} = require('internal/ffi/fast-api');

Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
37 changes: 33 additions & 4 deletions lib/internal/ffi/fast-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
NumberIsInteger,
ObjectDefineProperty,
ReflectApply,
SafeWeakMap,
StringPrototypeIncludes,
TypeError,
} = primordials;
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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: [],
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -300,6 +327,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) {
};
} else {
wrapper = function(...args) {
throwIfFastLibraryClosed(state);
if (args.length !== nargs) {
throwFFIArgCountError(nargs, args.length);
}
Expand Down Expand Up @@ -332,5 +360,6 @@ module.exports = {
convertPointerArg,
hasPointerMemoryArg,
hasStringPointerArg,
markFastLibraryClosed,
wrapWithRawPointerConversions,
};
32 changes: 30 additions & 2 deletions src/ffi/fast.cc
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,26 @@ extern "C" uintptr_t node_ffi_fast_buffer_data(v8::Local<v8::Value> 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.
Expand All @@ -265,7 +279,18 @@ bool IsFastCallSupported() {
#endif
}

std::unique_ptr<FastFFIMetadata> 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<FastFFIMetadata> 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.
Expand Down Expand Up @@ -299,6 +324,7 @@ std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
std::vector<FastFFIType> 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.
Expand All @@ -320,8 +346,9 @@ std::unique_ptr<FastFFIMetadata> 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;
}

Expand All @@ -348,6 +375,7 @@ std::unique_ptr<FastFFIMetadata> 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;
}

Expand Down
24 changes: 18 additions & 6 deletions src/ffi/fast.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -47,6 +53,7 @@ struct FastFFIMetadata {
std::vector<v8::CTypeInfo> arg_info;
std::unique_ptr<v8::CFunctionInfo> c_function_info;
v8::CFunction c_function;
bool guards_library = false;
};

// Public detection queries.
Expand All @@ -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);
Expand All @@ -65,19 +73,23 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
const std::shared_ptr<FFIFunction>& fn);
std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
const std::shared_ptr<FFIFunction>& fn);
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn);
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn,
const bool* closed,
v8::Isolate* isolate);

} // namespace node::ffi

extern "C" {
uintptr_t node_ffi_fast_buffer_data(v8::Local<v8::Value> 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);
}

Expand Down
35 changes: 31 additions & 4 deletions src/ffi/platforms/arm64.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<uintptr_t>(config.closed));
*cursor++ = LdrbW(17, 16);
uint32_t* open_branch = cursor++;
EmitLoadX16(&cursor, reinterpret_cast<uintptr_t>(config.isolate));
*cursor++ = MovX(0, 16);
EmitLoadX16(
&cursor, reinterpret_cast<uintptr_t>(node_ffi_fast_library_closed));
*cursor++ = kBlrX16;
*cursor++ = MovX(0, 31);
*cursor++ = kLdpFpLrPostIndex;
*cursor++ = kRet;
*open_branch =
CbzW(17, static_cast<unsigned>(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.
Expand Down Expand Up @@ -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<uintptr_t>(target));
EmitLoadX16(&cursor, reinterpret_cast<uintptr_t>(config.target));
*cursor++ = kBlrX16;

if (has_buffer_args) {
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/ffi/platforms/loong64.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<uintptr_t>(target));
Emit64(&cursor, reinterpret_cast<uintptr_t>(config.target));

const size_t written = reinterpret_cast<uint8_t*>(cursor) -
static_cast<uint8_t*>(code);
Expand Down
Loading
Loading