Skip to content

Commit 9b5642c

Browse files
trivikraduh95
authored andcommitted
ffi: reject fast calls after library close
Optimized Fast API calls bypass InvokeFunction and can jump directly to a symbol after DynamicLibrary::close() unloads its library. Check the function's closed state in the AArch64 and SysV x64 trampolines before entering the target. If the library is closed, schedule ERR_FFI_LIBRARY_CLOSED and return without calling the symbol. Keep the JavaScript guard on platforms without a native trampoline guard and for signatures that already require argument conversion or validation. This keeps raw scalar fast calls close to their original performance on supported platforms. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: #64860 Fixes: #64854 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent d926ddc commit 9b5642c

13 files changed

Lines changed: 217 additions & 41 deletions

File tree

lib/ffi.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const {
6868
} = require('internal/ffi-shared-buffer');
6969

7070
const {
71+
markFastLibraryClosed,
7172
wrapWithRawPointerConversions,
7273
} = require('internal/ffi/fast-api');
7374

@@ -100,6 +101,27 @@ function wrapFFIFunction(rawFn, owner) {
100101

101102
const rawGetFunction = DynamicLibrary.prototype.getFunction;
102103
const rawGetFunctions = DynamicLibrary.prototype.getFunctions;
104+
const rawClose = DynamicLibrary.prototype.close;
105+
106+
function close() {
107+
const result = FunctionPrototypeCall(rawClose, this);
108+
markFastLibraryClosed(this);
109+
return result;
110+
}
111+
112+
ObjectDefineProperty(DynamicLibrary.prototype, 'close', {
113+
__proto__: null,
114+
configurable: true,
115+
value: close,
116+
writable: true,
117+
});
118+
119+
ObjectDefineProperty(DynamicLibrary.prototype, SymbolDispose, {
120+
__proto__: null,
121+
configurable: true,
122+
value: close,
123+
writable: true,
124+
});
103125

104126
DynamicLibrary.prototype.getFunction = function getFunction(name, signature) {
105127
const raw = FunctionPrototypeCall(rawGetFunction, this, name, signature);

lib/internal/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,7 @@ E('ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
12321232
'The feature %s is unavailable on the current platform' +
12331233
', which is being used to run Node.js',
12341234
TypeError);
1235+
E('ERR_FFI_LIBRARY_CLOSED', 'Library is closed', Error);
12351236
E('ERR_FS_CP_DIR_TO_NON_DIR',
12361237
'Cannot overwrite non-directory with directory', SystemError);
12371238
E('ERR_FS_CP_EEXIST', 'Target already exists', SystemError);

lib/internal/ffi/fast-api.js

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const {
55
NumberIsInteger,
66
ObjectDefineProperty,
77
ReflectApply,
8+
SafeWeakMap,
89
StringPrototypeIncludes,
910
TypeError,
1011
} = primordials;
@@ -25,9 +26,16 @@ const {
2526
kFastBufferInvoke,
2627
} = internalBinding('ffi');
2728

29+
const {
30+
codes: {
31+
ERR_FFI_LIBRARY_CLOSED,
32+
},
33+
} = require('internal/errors');
34+
2835
const U64_MAX = 0xFFFFFFFFFFFFFFFFn;
2936
const I64_MAX = 0x7FFFFFFFFFFFFFFFn;
3037
const I64_MIN = -0x8000000000000000n;
38+
const fastLibraryStates = new SafeWeakMap();
3139

3240
// These ranges mirror ToFFIArgument in src/ffi/types.cc. V8's Fast API
3341
// exposes narrow integers as 32-bit values and uses truncating BigInt
@@ -202,7 +210,20 @@ function inheritMetadata(wrapper, rawFn, nargs) {
202210
return wrapper;
203211
}
204212

205-
function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) {
213+
function markFastLibraryClosed(owner) {
214+
const state = fastLibraryStates.get(owner);
215+
if (state !== undefined) {
216+
state.closed = true;
217+
}
218+
}
219+
220+
function throwIfFastLibraryClosed(state) {
221+
if (state.closed) {
222+
throw new ERR_FFI_LIBRARY_CLOSED();
223+
}
224+
}
225+
226+
function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) {
206227
if (rawFn === undefined || rawFn === null) {
207228
return rawFn;
208229
}
@@ -213,11 +234,14 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) {
213234
return rawFn;
214235
}
215236

216-
const indexes = getFastArgumentIndexes(argumentTypes);
217-
if (indexes === null) {
218-
return rawFn;
237+
let state = fastLibraryStates.get(owner);
238+
if (state === undefined) {
239+
state = { __proto__: null, closed: false };
240+
fastLibraryStates.set(owner, state);
219241
}
220242

243+
const indexes = getFastArgumentIndexes(argumentTypes) ?? [];
244+
221245
const stringState = {
222246
__proto__: null,
223247
buffers: [],
@@ -233,6 +257,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) {
233257
const fastBufferInvoke = needsPointerLikeConversion(t0) ?
234258
rawFn[kFastBufferInvoke] : undefined;
235259
wrapper = function(a0) {
260+
throwIfFastLibraryClosed(state);
236261
if (arguments.length !== 1) {
237262
throwFFIArgCountError(1, arguments.length);
238263
}
@@ -262,6 +287,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) {
262287
const t0 = argumentTypes[0];
263288
const t1 = argumentTypes[1];
264289
wrapper = function(a0, a1) {
290+
throwIfFastLibraryClosed(state);
265291
if (arguments.length !== 2) {
266292
throwFFIArgCountError(2, arguments.length);
267293
}
@@ -283,6 +309,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) {
283309
const t1 = argumentTypes[1];
284310
const t2 = argumentTypes[2];
285311
wrapper = function(a0, a1, a2) {
312+
throwIfFastLibraryClosed(state);
286313
if (arguments.length !== 3) {
287314
throwFFIArgCountError(3, arguments.length);
288315
}
@@ -300,6 +327,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) {
300327
};
301328
} else {
302329
wrapper = function(...args) {
330+
throwIfFastLibraryClosed(state);
303331
if (args.length !== nargs) {
304332
throwFFIArgCountError(nargs, args.length);
305333
}
@@ -332,5 +360,6 @@ module.exports = {
332360
convertPointerArg,
333361
hasPointerMemoryArg,
334362
hasStringPointerArg,
363+
markFastLibraryClosed,
335364
wrapWithRawPointerConversions,
336365
};

src/ffi/fast.cc

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,26 @@ extern "C" uintptr_t node_ffi_fast_buffer_data(v8::Local<v8::Value> value,
241241

242242
v8::Isolate* isolate = options != nullptr ? options->isolate : nullptr;
243243
if (isolate != nullptr) {
244+
// No HandleScope is active during a Fast API call, so open one before
245+
// creating the error object.
246+
v8::HandleScope scope(isolate);
244247
THROW_ERR_INVALID_ARG_VALUE(
245248
isolate, "Argument %u must be a buffer or an ArrayBuffer", index);
246249
}
247250
return kInvalidBuffer;
248251
}
249252

253+
extern "C" void node_ffi_fast_library_closed(v8::Isolate* isolate) {
254+
if (isolate != nullptr) {
255+
// Fast API calls do not enter a HandleScope, and the generated trampolines
256+
// call this helper directly. Building the error object allocates handles,
257+
// so open a scope here. The scheduled exception lives on the isolate and
258+
// outlives the scope.
259+
v8::HandleScope scope(isolate);
260+
THROW_ERR_FFI_LIBRARY_CLOSED(isolate);
261+
}
262+
}
263+
250264
FastFFIMetadata::~FastFFIMetadata() {
251265
// Metadata owns executable memory through `trampoline`; releasing it here
252266
// ties code lifetime to the V8 function's weak FFIFunctionInfo cleanup.
@@ -265,7 +279,18 @@ bool IsFastCallSupported() {
265279
#endif
266280
}
267281

268-
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
282+
bool IsFastLibraryGuardSupported() {
283+
#if defined(__aarch64__) || defined(_M_ARM64) || \
284+
(defined(__x86_64__) && !defined(_WIN32))
285+
return true;
286+
#else
287+
return false;
288+
#endif
289+
}
290+
291+
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn,
292+
const bool* closed,
293+
v8::Isolate* isolate) {
269294
// Bail early if executable memory allocation doesn't work on this process
270295
// (missing MAP_JIT entitlement, hardened runtime, SELinux execmem, etc.).
271296
// The self-test runs once and caches the result.
@@ -299,6 +324,7 @@ std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
299324
std::vector<FastFFIType> args;
300325
args.reserve(fn.arg_type_names.size());
301326
bool needs_bigint = NeedsBigIntRepresentation(result);
327+
const bool guards_library = IsFastLibraryGuardSupported();
302328
bool needs_callback_options = false;
303329
// Normalize public argument names into FastFFIType values while collecting
304330
// signature-wide flags required by V8 CFunctionInfo.
@@ -320,8 +346,9 @@ std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
320346
// The platform-specific trampoline is the executable entrypoint V8 calls.
321347
// If the platform rejects the signature, the whole fast metadata object is
322348
// discarded and the caller chooses another invocation path.
349+
FastFFITrampolineConfig config{fn.ptr, closed, isolate};
323350
if (!node_ffi_create_fast_trampoline(
324-
fn.ptr, args.data(), args.size(), result, &metadata->trampoline)) {
351+
config, args.data(), args.size(), result, &metadata->trampoline)) {
325352
return nullptr;
326353
}
327354

@@ -348,6 +375,7 @@ std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
348375
: CFunctionInfo::Int64Representation::kNumber);
349376
metadata->c_function =
350377
v8::CFunction(metadata->trampoline.code, metadata->c_function_info.get());
378+
metadata->guards_library = guards_library;
351379
return metadata;
352380
}
353381

src/ffi/fast.h

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ struct FastFFITrampoline {
3636
size_t size = 0;
3737
};
3838

39+
struct FastFFITrampolineConfig {
40+
void* target;
41+
const bool* closed;
42+
v8::Isolate* isolate;
43+
};
44+
3945
struct FastFFIMetadata {
4046
FastFFIMetadata() = default;
4147
~FastFFIMetadata();
@@ -47,6 +53,7 @@ struct FastFFIMetadata {
4753
std::vector<v8::CTypeInfo> arg_info;
4854
std::unique_ptr<v8::CFunctionInfo> c_function_info;
4955
v8::CFunction c_function;
56+
bool guards_library = false;
5057
};
5158

5259
// Public detection queries.
@@ -56,6 +63,7 @@ struct FastFFIMetadata {
5663
// of any particular signature — if this returns false, no signature can
5764
// use the fast-call path.
5865
bool IsFastCallSupported();
66+
bool IsFastLibraryGuardSupported();
5967

6068
bool SignatureNeedsRawPointerConversions(const FFIFunction& fn);
6169
bool SignatureNeedsFastIntegerValidation(const FFIFunction& fn);
@@ -65,19 +73,23 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
6573
const std::shared_ptr<FFIFunction>& fn);
6674
std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
6775
const std::shared_ptr<FFIFunction>& fn);
68-
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn);
76+
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn,
77+
const bool* closed,
78+
v8::Isolate* isolate);
6979

7080
} // namespace node::ffi
7181

7282
extern "C" {
7383
uintptr_t node_ffi_fast_buffer_data(v8::Local<v8::Value> value,
7484
v8::FastApiCallbackOptions* options,
7585
uint32_t index);
76-
bool node_ffi_create_fast_trampoline(void* target,
77-
const node::ffi::FastFFIType* args,
78-
size_t argc,
79-
node::ffi::FastFFIType result,
80-
node::ffi::FastFFITrampoline* out);
86+
void node_ffi_fast_library_closed(v8::Isolate* isolate);
87+
bool node_ffi_create_fast_trampoline(
88+
const node::ffi::FastFFITrampolineConfig& config,
89+
const node::ffi::FastFFIType* args,
90+
size_t argc,
91+
node::ffi::FastFFIType result,
92+
node::ffi::FastFFITrampoline* out);
8193
void node_ffi_free_fast_trampoline(node::ffi::FastFFITrampoline* trampoline);
8294
}
8395

src/ffi/platforms/arm64.cc

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ uint32_t LdrXSp(unsigned reg, unsigned offset) {
8181
return 0xf94003e0 | ((offset / 8) << 10) | reg;
8282
}
8383

84+
uint32_t LdrbW(unsigned dst, unsigned base) {
85+
return 0x39400000 | (base << 5) | dst;
86+
}
87+
88+
uint32_t CbzW(unsigned reg, unsigned instruction_offset) {
89+
return 0x34000000 | ((instruction_offset & 0x7ffff) << 5) | reg;
90+
}
91+
8492
uint32_t MovzW(unsigned dst, uint16_t value) {
8593
// Load a small immediate into a W register. The buffer helper's argument
8694
// 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) {
213221
} // namespace
214222

215223
extern "C" bool node_ffi_create_fast_trampoline(
216-
void* target,
224+
const node::ffi::FastFFITrampolineConfig& config,
217225
const node::ffi::FastFFIType* args,
218226
size_t argc,
219227
node::ffi::FastFFIType result,
220228
node::ffi::FastFFITrampoline* out) {
221229
// Null inputs mean the caller cannot safely create executable code for this
222230
// signature. Report rejection so the generic FFI path can be used instead.
223-
if (target == nullptr || out == nullptr) {
231+
if (config.target == nullptr || config.closed == nullptr ||
232+
config.isolate == nullptr || out == nullptr) {
224233
return false;
225234
}
226235

@@ -275,6 +284,24 @@ extern "C" bool node_ffi_create_fast_trampoline(
275284
// call can return through this generated trampoline safely.
276285
*cursor++ = kStpFpLrPreIndex;
277286

287+
// Fast calls bypass DynamicLibrary::InvokeFunction, so check the stable
288+
// FFIFunction::closed flag before touching the target address. The open
289+
// branch is the hot path. On close, schedule the standard JS exception and
290+
// return; V8 checks for pending exceptions after Fast API calls.
291+
EmitLoadX16(&cursor, reinterpret_cast<uintptr_t>(config.closed));
292+
*cursor++ = LdrbW(17, 16);
293+
uint32_t* open_branch = cursor++;
294+
EmitLoadX16(&cursor, reinterpret_cast<uintptr_t>(config.isolate));
295+
*cursor++ = MovX(0, 16);
296+
EmitLoadX16(
297+
&cursor, reinterpret_cast<uintptr_t>(node_ffi_fast_library_closed));
298+
*cursor++ = kBlrX16;
299+
*cursor++ = MovX(0, 31);
300+
*cursor++ = kLdpFpLrPostIndex;
301+
*cursor++ = kRet;
302+
*open_branch =
303+
CbzW(17, static_cast<unsigned>(cursor - open_branch));
304+
278305
if (has_buffer_args) {
279306
// Buffer conversion calls a C++ helper before the target call, so spill all
280307
// incoming GP registers that may be clobbered by that helper.
@@ -361,7 +388,7 @@ extern "C" bool node_ffi_create_fast_trampoline(
361388

362389
// Tail of the trampoline: load the actual library symbol address and call it
363390
// with arguments now arranged according to the native ABI.
364-
EmitLoadX16(&cursor, reinterpret_cast<uintptr_t>(target));
391+
EmitLoadX16(&cursor, reinterpret_cast<uintptr_t>(config.target));
365392
*cursor++ = kBlrX16;
366393

367394
if (has_buffer_args) {
@@ -414,7 +441,7 @@ extern "C" void node_ffi_free_fast_trampoline(
414441
!(defined(__riscv) && __riscv_xlen == 64) && !defined(__s390x__)
415442

416443
extern "C" bool node_ffi_create_fast_trampoline(
417-
void* target,
444+
const node::ffi::FastFFITrampolineConfig& config,
418445
const node::ffi::FastFFIType* args,
419446
size_t argc,
420447
node::ffi::FastFFIType result,

src/ffi/platforms/loong64.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ void FreeCode(void* code, size_t code_size) {
7474
} // namespace
7575

7676
extern "C" bool node_ffi_create_fast_trampoline(
77-
void* target,
77+
const node::ffi::FastFFITrampolineConfig& config,
7878
const node::ffi::FastFFIType* args,
7979
size_t argc,
8080
node::ffi::FastFFIType result,
8181
node::ffi::FastFFITrampoline* out) {
82-
if (target == nullptr || out == nullptr || IsNarrowType(result)) {
82+
if (config.target == nullptr || out == nullptr || IsNarrowType(result)) {
8383
return false;
8484
}
8585

@@ -126,7 +126,7 @@ extern "C" bool node_ffi_create_fast_trampoline(
126126
Emit32(&cursor, LdD(12, 12, 16)); // ld.d t0, t0, literal
127127
Emit32(&cursor, Jirl(0, 12, 0)); // jr t0
128128
Emit32(&cursor, Or(0, 0, 0)); // nop; align literal to 8 bytes
129-
Emit64(&cursor, reinterpret_cast<uintptr_t>(target));
129+
Emit64(&cursor, reinterpret_cast<uintptr_t>(config.target));
130130

131131
const size_t written = reinterpret_cast<uint8_t*>(cursor) -
132132
static_cast<uint8_t*>(code);

0 commit comments

Comments
 (0)