Skip to content

Commit 3b1b100

Browse files
committed
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
1 parent c8e2a82 commit 3b1b100

13 files changed

Lines changed: 209 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
@@ -1234,6 +1234,7 @@ E('ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
12341234
'The feature %s is unavailable on the current platform' +
12351235
', which is being used to run Node.js',
12361236
TypeError);
1237+
E('ERR_FFI_LIBRARY_CLOSED', 'Library is closed', Error);
12371238
E('ERR_FS_CP_DIR_TO_NON_DIR',
12381239
'Cannot overwrite non-directory with directory', SystemError);
12391240
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: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ extern "C" uintptr_t node_ffi_fast_buffer_data(v8::Local<v8::Value> value,
247247
return kInvalidBuffer;
248248
}
249249

250+
extern "C" void node_ffi_fast_library_closed(v8::Isolate* isolate) {
251+
if (isolate != nullptr) {
252+
THROW_ERR_FFI_LIBRARY_CLOSED(isolate);
253+
}
254+
}
255+
250256
FastFFIMetadata::~FastFFIMetadata() {
251257
// Metadata owns executable memory through `trampoline`; releasing it here
252258
// ties code lifetime to the V8 function's weak FFIFunctionInfo cleanup.
@@ -265,7 +271,18 @@ bool IsFastCallSupported() {
265271
#endif
266272
}
267273

268-
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
274+
bool IsFastLibraryGuardSupported() {
275+
#if defined(__aarch64__) || defined(_M_ARM64) || \
276+
(defined(__x86_64__) && !defined(_WIN32))
277+
return true;
278+
#else
279+
return false;
280+
#endif
281+
}
282+
283+
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn,
284+
const bool* closed,
285+
v8::Isolate* isolate) {
269286
// Bail early if executable memory allocation doesn't work on this process
270287
// (missing MAP_JIT entitlement, hardened runtime, SELinux execmem, etc.).
271288
// The self-test runs once and caches the result.
@@ -299,6 +316,7 @@ std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
299316
std::vector<FastFFIType> args;
300317
args.reserve(fn.arg_type_names.size());
301318
bool needs_bigint = NeedsBigIntRepresentation(result);
319+
const bool guards_library = IsFastLibraryGuardSupported();
302320
bool needs_callback_options = false;
303321
// Normalize public argument names into FastFFIType values while collecting
304322
// signature-wide flags required by V8 CFunctionInfo.
@@ -320,8 +338,9 @@ std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
320338
// The platform-specific trampoline is the executable entrypoint V8 calls.
321339
// If the platform rejects the signature, the whole fast metadata object is
322340
// discarded and the caller chooses another invocation path.
341+
FastFFITrampolineConfig config{fn.ptr, closed, isolate};
323342
if (!node_ffi_create_fast_trampoline(
324-
fn.ptr, args.data(), args.size(), result, &metadata->trampoline)) {
343+
config, args.data(), args.size(), result, &metadata->trampoline)) {
325344
return nullptr;
326345
}
327346

@@ -348,6 +367,7 @@ std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
348367
: CFunctionInfo::Int64Representation::kNumber);
349368
metadata->c_function =
350369
v8::CFunction(metadata->trampoline.code, metadata->c_function_info.get());
370+
metadata->guards_library = guards_library;
351371
return metadata;
352372
}
353373

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);

src/ffi/platforms/ppc64.cc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ void FreeCode(void* code, size_t code_size) {
8686
} // namespace
8787

8888
extern "C" bool node_ffi_create_fast_trampoline(
89-
void* target,
89+
const node::ffi::FastFFITrampolineConfig& config,
9090
const node::ffi::FastFFIType* args,
9191
size_t argc,
9292
node::ffi::FastFFIType result,
9393
node::ffi::FastFFITrampoline* out) {
94-
if (target == nullptr || out == nullptr || IsNarrowType(result)) {
94+
if (config.target == nullptr || out == nullptr || IsNarrowType(result)) {
9595
return false;
9696
}
9797

@@ -149,7 +149,7 @@ extern "C" bool node_ffi_create_fast_trampoline(
149149
if (gp_count % 2 == 0) {
150150
Emit32(&cursor, 0x60000000); // nop; align literal to 8 bytes
151151
}
152-
Emit64(&cursor, reinterpret_cast<uintptr_t>(target));
152+
Emit64(&cursor, reinterpret_cast<uintptr_t>(config.target));
153153

154154
const size_t written = reinterpret_cast<uint8_t*>(cursor) -
155155
static_cast<uint8_t*>(code);
@@ -179,7 +179,7 @@ extern "C" void node_ffi_free_fast_trampoline(
179179
#else
180180

181181
extern "C" bool node_ffi_create_fast_trampoline(
182-
void* target,
182+
const node::ffi::FastFFITrampolineConfig& config,
183183
const node::ffi::FastFFIType* args,
184184
size_t argc,
185185
node::ffi::FastFFIType result,

0 commit comments

Comments
 (0)