From 4c9023c168b7b2933e2fe56e0cbdc3c4dd8e1286 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:15:16 -0700 Subject: [PATCH] ffi: keep FFI functions non-constructible Use concise method functions for Fast API and shared-buffer wrappers, and create native fallback functions with ConstructorBehavior::kThrow, so FFI functions remain non-constructible on all invocation paths. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol --- lib/internal/ffi-shared-buffer.js | 70 ++++++++++++++-------------- lib/internal/ffi/fast-api.js | 18 +++---- src/node_ffi.cc | 11 +++-- test/ffi/test-ffi-dynamic-library.js | 21 +++++++++ 4 files changed, 75 insertions(+), 45 deletions(-) diff --git a/lib/internal/ffi-shared-buffer.js b/lib/internal/ffi-shared-buffer.js index c8b48d2aad4c..94764bdf0db7 100644 --- a/lib/internal/ffi-shared-buffer.js +++ b/lib/internal/ffi-shared-buffer.js @@ -198,6 +198,8 @@ function inheritMetadata(wrapper, rawFn, nargs) { // arguments out of it into invocation-local storage before `ffi_call` and // reads the return value back only after, so nested/reentrant calls into // the same function are safe. +// Concise methods do not have [[Construct]], unlike function expressions, so +// use them below to match the native FFI functions' non-constructible behavior. function wrapWithSharedBuffer(rawFn, signature) { if (rawFn == null) return rawFn; const buffer = rawFn[kSbSharedBuffer]; @@ -254,7 +256,7 @@ function wrapWithSharedBuffer(rawFn, signature) { // so arity specialization wouldn't buy much here. assert(slowInvoke !== undefined, 'FFI: shared-buffer raw function with pointer arguments is missing kSbInvokeSlow'); - wrapper = function(...args) { + wrapper = { invoke(...args) { if (args.length !== nargs) { throwFFIArgCountError(nargs, args.length); } @@ -271,7 +273,7 @@ function wrapWithSharedBuffer(rawFn, signature) { } rawFn(); return retGetter === null ? undefined : retGetter(view, 0, true); - }; + } }.invoke; } else { // Arity specialization avoids the per-call `Array` allocation of // `...args`; the void/non-void split removes a per-call branch on @@ -295,42 +297,42 @@ function buildNumericWrapper( /* c8 ignore start */ if (nargs === 0) { if (retGetter === null) { - return function() { + return { invoke() { if (arguments.length !== 0) { throwFFIArgCountError(0, arguments.length); } rawFn(); - }; + } }.invoke; } - return function() { + return { invoke() { if (arguments.length !== 0) { throwFFIArgCountError(0, arguments.length); } rawFn(); return retGetter(view, 0, true); - }; + } }.invoke; } /* c8 ignore stop */ if (nargs === 1) { const i0 = argInfos[0]; const o0 = argOffsets[0]; if (retGetter === null) { - return function(a0) { + return { invoke(a0) { if (arguments.length !== 1) { throwFFIArgCountError(1, arguments.length); } writeNumericArg(view, i0, o0, a0, 0); rawFn(); - }; + } }.invoke; } - return function(a0) { + return { invoke(a0) { if (arguments.length !== 1) { throwFFIArgCountError(1, arguments.length); } writeNumericArg(view, i0, o0, a0, 0); rawFn(); return retGetter(view, 0, true); - }; + } }.invoke; } if (nargs === 2) { const i0 = argInfos[0]; @@ -338,16 +340,16 @@ function buildNumericWrapper( const o0 = argOffsets[0]; const o1 = argOffsets[1]; if (retGetter === null) { - return function(a0, a1) { + return { invoke(a0, a1) { if (arguments.length !== 2) { throwFFIArgCountError(2, arguments.length); } writeNumericArg(view, i0, o0, a0, 0); writeNumericArg(view, i1, o1, a1, 1); rawFn(); - }; + } }.invoke; } - return function(a0, a1) { + return { invoke(a0, a1) { if (arguments.length !== 2) { throwFFIArgCountError(2, arguments.length); } @@ -355,7 +357,7 @@ function buildNumericWrapper( writeNumericArg(view, i1, o1, a1, 1); rawFn(); return retGetter(view, 0, true); - }; + } }.invoke; } if (nargs === 3) { const i0 = argInfos[0]; @@ -365,7 +367,7 @@ function buildNumericWrapper( const o1 = argOffsets[1]; const o2 = argOffsets[2]; if (retGetter === null) { - return function(a0, a1, a2) { + return { invoke(a0, a1, a2) { if (arguments.length !== 3) { throwFFIArgCountError(3, arguments.length); } @@ -373,9 +375,9 @@ function buildNumericWrapper( writeNumericArg(view, i1, o1, a1, 1); writeNumericArg(view, i2, o2, a2, 2); rawFn(); - }; + } }.invoke; } - return function(a0, a1, a2) { + return { invoke(a0, a1, a2) { if (arguments.length !== 3) { throwFFIArgCountError(3, arguments.length); } @@ -384,7 +386,7 @@ function buildNumericWrapper( writeNumericArg(view, i2, o2, a2, 2); rawFn(); return retGetter(view, 0, true); - }; + } }.invoke; } if (nargs === 4) { const i0 = argInfos[0]; @@ -396,7 +398,7 @@ function buildNumericWrapper( const o2 = argOffsets[2]; const o3 = argOffsets[3]; if (retGetter === null) { - return function(a0, a1, a2, a3) { + return { invoke(a0, a1, a2, a3) { if (arguments.length !== 4) { throwFFIArgCountError(4, arguments.length); } @@ -405,9 +407,9 @@ function buildNumericWrapper( writeNumericArg(view, i2, o2, a2, 2); writeNumericArg(view, i3, o3, a3, 3); rawFn(); - }; + } }.invoke; } - return function(a0, a1, a2, a3) { + return { invoke(a0, a1, a2, a3) { if (arguments.length !== 4) { throwFFIArgCountError(4, arguments.length); } @@ -417,7 +419,7 @@ function buildNumericWrapper( writeNumericArg(view, i3, o3, a3, 3); rawFn(); return retGetter(view, 0, true); - }; + } }.invoke; } if (nargs === 5) { const i0 = argInfos[0]; @@ -431,7 +433,7 @@ function buildNumericWrapper( const o3 = argOffsets[3]; const o4 = argOffsets[4]; if (retGetter === null) { - return function(a0, a1, a2, a3, a4) { + return { invoke(a0, a1, a2, a3, a4) { if (arguments.length !== 5) { throwFFIArgCountError(5, arguments.length); } @@ -441,9 +443,9 @@ function buildNumericWrapper( writeNumericArg(view, i3, o3, a3, 3); writeNumericArg(view, i4, o4, a4, 4); rawFn(); - }; + } }.invoke; } - return function(a0, a1, a2, a3, a4) { + return { invoke(a0, a1, a2, a3, a4) { if (arguments.length !== 5) { throwFFIArgCountError(5, arguments.length); } @@ -454,7 +456,7 @@ function buildNumericWrapper( writeNumericArg(view, i4, o4, a4, 4); rawFn(); return retGetter(view, 0, true); - }; + } }.invoke; } if (nargs === 6) { const i0 = argInfos[0]; @@ -470,7 +472,7 @@ function buildNumericWrapper( const o4 = argOffsets[4]; const o5 = argOffsets[5]; if (retGetter === null) { - return function(a0, a1, a2, a3, a4, a5) { + return { invoke(a0, a1, a2, a3, a4, a5) { if (arguments.length !== 6) { throwFFIArgCountError(6, arguments.length); } @@ -481,9 +483,9 @@ function buildNumericWrapper( writeNumericArg(view, i4, o4, a4, 4); writeNumericArg(view, i5, o5, a5, 5); rawFn(); - }; + } }.invoke; } - return function(a0, a1, a2, a3, a4, a5) { + return { invoke(a0, a1, a2, a3, a4, a5) { if (arguments.length !== 6) { throwFFIArgCountError(6, arguments.length); } @@ -495,12 +497,12 @@ function buildNumericWrapper( writeNumericArg(view, i5, o5, a5, 5); rawFn(); return retGetter(view, 0, true); - }; + } }.invoke; } // 7+ args: further specialization is diminishing returns and bloats // this builder. if (retGetter === null) { - return function(...args) { + return { invoke(...args) { if (args.length !== nargs) { throwFFIArgCountError(nargs, args.length); } @@ -508,9 +510,9 @@ function buildNumericWrapper( writeNumericArg(view, argInfos[i], argOffsets[i], args[i], i); } rawFn(); - }; + } }.invoke; } - return function(...args) { + return { invoke(...args) { if (args.length !== nargs) { throwFFIArgCountError(nargs, args.length); } @@ -519,7 +521,7 @@ function buildNumericWrapper( } rawFn(); return retGetter(view, 0, true); - }; + } }.invoke; } module.exports = { diff --git a/lib/internal/ffi/fast-api.js b/lib/internal/ffi/fast-api.js index 486a119a2e07..b099220fac92 100644 --- a/lib/internal/ffi/fast-api.js +++ b/lib/internal/ffi/fast-api.js @@ -222,6 +222,8 @@ function throwIfFastLibraryClosed(state) { } } +// Concise methods do not have [[Construct]], unlike function expressions. +// Keep wrappers non-constructible to match the native FFI functions. function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { if (rawFn === undefined || rawFn === null) { return rawFn; @@ -255,7 +257,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { const memory0 = needsRawPointerConversion(t0) || string0; const fastBufferInvoke = needsPointerLikeConversion(t0) ? rawFn[kFastBufferInvoke] : undefined; - wrapper = function(a0) { + wrapper = { invoke(a0) { throwIfFastLibraryClosed(state); if (arguments.length !== 1) { throwFFIArgCountError(1, arguments.length); @@ -279,13 +281,13 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { arg = getRawPointer(arg); } return rawFn(arg); - }; + } }.invoke; } else if (nargs === 2) { const c0 = ArrayPrototypeIncludes(indexes, 0); const c1 = ArrayPrototypeIncludes(indexes, 1); const t0 = argumentTypes[0]; const t1 = argumentTypes[1]; - wrapper = function(a0, a1) { + wrapper = { invoke(a0, a1) { throwIfFastLibraryClosed(state); if (arguments.length !== 2) { throwFFIArgCountError(2, arguments.length); @@ -299,7 +301,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { } finally { if (stringCall) exitStringConversion(stringState); } - }; + } }.invoke; } else if (nargs === 3) { const c0 = ArrayPrototypeIncludes(indexes, 0); const c1 = ArrayPrototypeIncludes(indexes, 1); @@ -307,7 +309,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { const t0 = argumentTypes[0]; const t1 = argumentTypes[1]; const t2 = argumentTypes[2]; - wrapper = function(a0, a1, a2) { + wrapper = { invoke(a0, a1, a2) { throwIfFastLibraryClosed(state); if (arguments.length !== 3) { throwFFIArgCountError(3, arguments.length); @@ -323,9 +325,9 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { } finally { if (stringCall) exitStringConversion(stringState); } - }; + } }.invoke; } else { - wrapper = function(...args) { + wrapper = { invoke(...args) { throwIfFastLibraryClosed(state); if (args.length !== nargs) { throwFFIArgCountError(nargs, args.length); @@ -349,7 +351,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { } finally { if (stringCall) exitStringConversion(stringState); } - }; + } }.invoke; } return inheritMetadata(wrapper, rawFn, nargs); diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 1e1fc5654591..2a27b5803052 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -324,7 +324,9 @@ MaybeLocal DynamicLibrary::CreateFunction( maybe_ret = Function::New(context, use_sb ? DynamicLibrary::InvokeFunctionSB : DynamicLibrary::InvokeFunction, - info->object()); + info->object(), + 0, + v8::ConstructorBehavior::kThrow); } Local ret; @@ -377,8 +379,11 @@ MaybeLocal DynamicLibrary::CreateFunction( // (strings, Buffers, ArrayBuffers, and ArrayBufferViews). if (has_ptr_args) { Local slow_fn; - if (!Function::New( - context, DynamicLibrary::InvokeFunction, info->object()) + if (!Function::New(context, + DynamicLibrary::InvokeFunction, + info->object(), + 0, + v8::ConstructorBehavior::kThrow) .ToLocal(&slow_fn)) { return MaybeLocal(); } diff --git a/test/ffi/test-ffi-dynamic-library.js b/test/ffi/test-ffi-dynamic-library.js index 82400335a12f..d22cf48d77b1 100644 --- a/test/ffi/test-ffi-dynamic-library.js +++ b/test/ffi/test-ffi-dynamic-library.js @@ -67,6 +67,27 @@ test('dlopen resolves functions from definitions', () => { } }); +test('FFI functions are not constructible', () => { + const { lib, functions } = ffi.dlopen(libraryPath, { + add_i32: fixtureSymbols.add_i32, + multiply_f64: fixtureSymbols.multiply_f64, + }); + + try { + assert.strictEqual(Object.hasOwn(functions.add_i32, 'prototype'), false); + assert.strictEqual( + Object.hasOwn(functions.multiply_f64, 'prototype'), false); + assert.throws( + () => Reflect.construct(functions.add_i32, [20, 22]), + TypeError); + assert.throws( + () => Reflect.construct(functions.multiply_f64, [6, 7]), + TypeError); + } finally { + lib.close(); + } +}); + test('DynamicLibrary exposes functions and symbols', () => { const lib = new ffi.DynamicLibrary(libraryPath);