Skip to content

Commit 3391210

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: #64860Fixes: #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 7089bd9 commit 3391210

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
constrawGetFunction=DynamicLibrary.prototype.getFunction;
102103
constrawGetFunctions=DynamicLibrary.prototype.getFunctions;
104+
constrawClose=DynamicLibrary.prototype.close;
105+
106+
functionclose(){
107+
constresult=FunctionPrototypeCall(rawClose,this);
108+
markFastLibraryClosed(this);
109+
returnresult;
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=functiongetFunction(name,signature){
105127
constraw=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
constU64_MAX=0xFFFFFFFFFFFFFFFFn;
2936
constI64_MAX=0x7FFFFFFFFFFFFFFFn;
3037
constI64_MIN=-0x8000000000000000n;
38+
constfastLibraryStates=newSafeWeakMap();
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
returnwrapper;
203211
}
204212

205-
functionwrapWithRawPointerConversions(rawFn,argumentTypes,_owner){
213+
functionmarkFastLibraryClosed(owner){
214+
conststate=fastLibraryStates.get(owner);
215+
if(state!==undefined){
216+
state.closed=true;
217+
}
218+
}
219+
220+
functionthrowIfFastLibraryClosed(state){
221+
if(state.closed){
222+
thrownewERR_FFI_LIBRARY_CLOSED();
223+
}
224+
}
225+
226+
functionwrapWithRawPointerConversions(rawFn,argumentTypes,owner){
206227
if(rawFn===undefined||rawFn===null){
207228
returnrawFn;
208229
}
@@ -213,11 +234,14 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) {
213234
returnrawFn;
214235
}
215236

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

243+
constindexes=getFastArgumentIndexes(argumentTypes)??[];
244+
221245
conststringState={
222246
__proto__: null,
223247
buffers: [],
@@ -233,6 +257,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) {
233257
constfastBufferInvoke=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
constt0=argumentTypes[0];
263288
constt1=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
constt1=argumentTypes[1];
284310
constt2=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
returnkInvalidBuffer;
248251
}
249252

253+
extern"C"voidnode_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+
boolIsFastLibraryGuardSupported() {
283+
#if defined(__aarch64__) || defined(_M_ARM64) || \
284+
(defined(__x86_64__) && !defined(_WIN32))
285+
returntrue;
286+
#else
287+
returnfalse;
288+
#endif
289+
}
290+
291+
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn,
292+
constbool* 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+
constbool 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
returnnullptr;
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+
structFastFFITrampolineConfig {
40+
void* target;
41+
constbool* closed;
42+
v8::Isolate* isolate;
43+
};
44+
3945
structFastFFIMetadata {
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
boolIsFastCallSupported();
66+
boolIsFastLibraryGuardSupported();
5967

6068
boolSignatureNeedsRawPointerConversions(const FFIFunction& fn);
6169
boolSignatureNeedsFastIntegerValidation(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+
constbool* closed,
78+
v8::Isolate* isolate);
6979

7080
} // namespace node::ffi
7181

7282
extern"C" {
7383
uintptr_tnode_ffi_fast_buffer_data(v8::Local<v8::Value> value,
7484
v8::FastApiCallbackOptions* options,
7585
uint32_t index);
76-
boolnode_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+
voidnode_ffi_fast_library_closed(v8::Isolate* isolate);
87+
boolnode_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
voidnode_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
return0xf94003e0 | ((offset / 8) << 10) | reg;
8282
}
8383

84+
uint32_tLdrbW(unsigned dst, unsigned base) {
85+
return0x39400000 | (base << 5) | dst;
86+
}
87+
88+
uint32_tCbzW(unsigned reg, unsigned instruction_offset) {
89+
return0x34000000 | ((instruction_offset & 0x7ffff) << 5) | reg;
90+
}
91+
8492
uint32_tMovzW(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"boolnode_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
returnfalse;
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"boolnode_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"boolnode_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
returnfalse;
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
constsize_t written = reinterpret_cast<uint8_t*>(cursor) -
132132
static_cast<uint8_t*>(code);

0 commit comments

Comments
 (0)