From 31ba820e27ee3219538ad8ea92fb12a5b372c850 Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Wed, 15 Apr 2026 15:10:07 +0100 Subject: [PATCH 1/2] cli: add --experimental build flag Signed-off-by: Paolo Insogna --- common.gypi | 4 ++++ configure.py | 7 +++++++ src/node_options.h | 42 ++++++++++++++++++++++++------------------ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/common.gypi b/common.gypi index 183d8707682e8e..8bdc0a938e3256 100644 --- a/common.gypi +++ b/common.gypi @@ -15,6 +15,7 @@ 'python%': 'python', 'node_shared%': 'false', + 'node_enable_experimentals%': 0, 'force_dynamic_crt%': 0, 'node_use_v8_platform%': 'true', 'node_use_bundled_v8%': 'true', @@ -437,6 +438,9 @@ }], # The defines bellow must include all things from the external_v8_defines # list in v8/BUILD.gn. + ['node_enable_experimentals==1', { + 'defines': ['NODE_ENABLE_EXPERIMENTALS'], + }], ['v8_enable_v8_checks == 1', { 'defines': ['V8_ENABLE_CHECKS'], }], diff --git a/configure.py b/configure.py index 995d800bf69461..c0bee68d2c7c89 100755 --- a/configure.py +++ b/configure.py @@ -797,6 +797,12 @@ default=None, help='Enable the --trace-maps flag in V8 (use at your own risk)') +parser.add_argument('--experimental', + action='store_true', + dest='experimental', + default=None, + help='Enable all experimental features by default') + parser.add_argument('--experimental-enable-pointer-compression', action='store_true', dest='enable_pointer_compression', @@ -1803,6 +1809,7 @@ def configure_node_cctest_sources(o): def configure_node(o): if options.dest_os == 'android': o['variables']['OS'] = 'android' + o['variables']['node_enable_experimentals'] = B(options.experimental) o['variables']['node_prefix'] = options.prefix o['variables']['node_install_npm'] = b(not options.without_npm) o['variables']['node_install_corepack'] = b(options.with_corepack) diff --git a/src/node_options.h b/src/node_options.h index 4d48b4e5752112..1bb973a5ae1489 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -112,6 +112,12 @@ class DebugOptions : public Options { std::vector* argv) override; }; +#ifdef NODE_ENABLE_EXPERIMENTALS +#define EXPERIMENTAL_OPTION(name, default_value) bool name = true; +#else +#define EXPERIMENTAL_OPTION(name, default_value) bool name = default_value; +#endif + class EnvironmentOptions : public Options { public: bool abort_on_uncaught_exception = false; @@ -122,19 +128,19 @@ class EnvironmentOptions : public Options { bool require_module = true; std::string dns_result_order; bool enable_source_maps = false; - bool experimental_addon_modules = false; - bool experimental_eventsource = false; - bool experimental_fetch = true; - bool experimental_ffi = false; - bool experimental_websocket = true; - bool experimental_sqlite = true; - bool experimental_stream_iter = false; + EXPERIMENTAL_OPTION(experimental_addon_modules, false) + EXPERIMENTAL_OPTION(experimental_eventsource, false) + EXPERIMENTAL_OPTION(experimental_fetch, true) + EXPERIMENTAL_OPTION(experimental_ffi, false) + EXPERIMENTAL_OPTION(experimental_websocket, true) + EXPERIMENTAL_OPTION(experimental_sqlite, true) + EXPERIMENTAL_OPTION(experimental_stream_iter, false) bool webstorage = HAVE_SQLITE; - bool experimental_quic = false; + EXPERIMENTAL_OPTION(experimental_quic, false) std::string localstorage_file; - bool experimental_global_navigator = true; - bool experimental_global_web_crypto = true; - bool experimental_import_meta_resolve = false; + EXPERIMENTAL_OPTION(experimental_global_navigator, true) + EXPERIMENTAL_OPTION(experimental_global_web_crypto, true) + EXPERIMENTAL_OPTION(experimental_import_meta_resolve, false) std::string input_type; // Value of --input-type bool entry_is_url = false; bool permission = false; @@ -148,8 +154,8 @@ class EnvironmentOptions : public Options { bool allow_wasi = false; bool allow_ffi = false; bool allow_worker_threads = false; - bool experimental_repl_await = true; - bool experimental_vm_modules = false; + EXPERIMENTAL_OPTION(experimental_repl_await, true) + EXPERIMENTAL_OPTION(experimental_vm_modules, false) bool async_context_frame = true; bool expose_internals = false; bool force_node_api_uncaught_exceptions_policy = false; @@ -176,10 +182,10 @@ class EnvironmentOptions : public Options { uint64_t cpu_prof_interval = kDefaultCpuProfInterval; std::string cpu_prof_name; bool cpu_prof = false; - bool experimental_network_inspection = false; - bool experimental_worker_inspection = false; - bool experimental_storage_inspection = false; - bool experimental_inspector_network_resource = false; + EXPERIMENTAL_OPTION(experimental_network_inspection, false) + EXPERIMENTAL_OPTION(experimental_worker_inspection, false) + EXPERIMENTAL_OPTION(experimental_storage_inspection, false) + EXPERIMENTAL_OPTION(experimental_inspector_network_resource, false) std::string heap_prof_dir; std::string heap_prof_name; static const uint64_t kDefaultHeapProfInterval = 512 * 1024; @@ -273,7 +279,7 @@ class EnvironmentOptions : public Options { bool report_exclude_env = false; bool report_exclude_network = false; std::string experimental_config_file_path; - bool experimental_default_config_file = false; + EXPERIMENTAL_OPTION(experimental_default_config_file, false) inline DebugOptions* get_debug_options() { return &debug_options_; } inline const DebugOptions& debug_options() const { return debug_options_; } From 927da3bf134692528038b615cca1182ea534343b Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Wed, 15 Apr 2026 19:23:31 +0100 Subject: [PATCH 2/2] ffi: added new methods and error codes Signed-off-by: Paolo Insogna --- benchmark/ffi/getpid.js | 25 +++++ doc/api/errors.md | 18 ++++ doc/api/ffi.md | 59 ++++++++++- lib/ffi.js | 54 ++++++++-- src/ffi/data.cc | 146 +++++++++++++++++++++++++-- src/ffi/types.cc | 78 ++++++++------ src/node_errors.h | 4 + src/node_ffi.cc | 101 +++++++++++------- src/node_ffi.h | 2 + test/ffi/test-ffi-dynamic-library.js | 15 +++ test/ffi/test-ffi-memory.js | 43 ++++++++ test/ffi/test-ffi-module.js | 6 ++ test/ffi/test-ffi-permissions.js | 12 +++ 13 files changed, 477 insertions(+), 86 deletions(-) create mode 100644 benchmark/ffi/getpid.js diff --git a/benchmark/ffi/getpid.js b/benchmark/ffi/getpid.js new file mode 100644 index 00000000000000..c2e7d4ed89c34e --- /dev/null +++ b/benchmark/ffi/getpid.js @@ -0,0 +1,25 @@ +'use strict'; + +const common = require('../common.js'); +const ffi = require('node:ffi'); + +const bench = common.createBenchmark(main, { + n: [1e7], +}, { + flags: ['--experimental-ffi'], +}); + +const { lib, functions } = ffi.dlopen(null, { + uv_os_getpid: { result: 'i32', parameters: [] }, +}); + +const getpid = functions.uv_os_getpid; + +function main({ n }) { + bench.start(); + for (let i = 0; i < n; ++i) + getpid(); + bench.end(n); + + lib.close(); +} diff --git a/doc/api/errors.md b/doc/api/errors.md index 98073d49d62098..c7ef683b082402 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1332,6 +1332,24 @@ added: v14.0.0 Used when a feature that is not available to the current platform which is running Node.js is used. + + +### `ERR_FFI_INVALID_POINTER` + +An invalid pointer was passed to an FFI operation. + + + +### `ERR_FFI_LIBRARY_CLOSED` + +An operation was attempted on an FFI dynamic library after it was closed. + + + +### `ERR_FFI_SYSCALL_FAILED` + +A low-level FFI call failed. + ### `ERR_FS_CP_DIR_TO_NON_DIR` diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 33eef15106124f..6f458e8a7b2f38 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -165,12 +165,15 @@ const path = `libsqlite3.${suffix}`; added: REPLACEME --> -* `path` {string} Path to a dynamic library. +* `path` {string|null} Path to a dynamic library, or `null` to resolve symbols + from the current process image. * `definitions` {Object} Symbol definitions to resolve immediately. * Returns: {Object} Loads a dynamic library and resolves the requested function definitions. +On Windows passing `null` is not supported. + When `definitions` is omitted, `functions` is returned as an empty object until symbols are resolved explicitly. @@ -237,10 +240,13 @@ Represents a loaded dynamic library. ### `new DynamicLibrary(path)` -* `path` {string} Path to a dynamic library. +* `path` {string|null} Path to a dynamic library, or `null` to resolve symbols + from the current process image. Loads the dynamic library without resolving any functions eagerly. +On Windows passing `null` is not supported. + ```cjs const { DynamicLibrary } = require('node:ffi'); @@ -603,6 +609,55 @@ available storage. This function does not allocate memory on its own. `buffer` must be a Node.js `Buffer`. +## `ffi.exportArrayBuffer(arrayBuffer, pointer, length)` + + + +* `arrayBuffer` {ArrayBuffer} +* `pointer` {bigint} +* `length` {number} + +Copies bytes from an `ArrayBuffer` into native memory. + +`length` must be at least `arrayBuffer.byteLength`. + +`pointer` must refer to writable native memory with at least `length` bytes of +available storage. This function does not allocate memory on its own. + +## `ffi.exportArrayBufferView(arrayBufferView, pointer, length)` + + + +* `arrayBufferView` {ArrayBufferView} +* `pointer` {bigint} +* `length` {number} + +Copies bytes from an `ArrayBufferView` into native memory. + +`length` must be at least `arrayBufferView.byteLength`. + +`pointer` must refer to writable native memory with at least `length` bytes of +available storage. This function does not allocate memory on its own. + +## `ffi.getRawPointer(source)` + + + +* `source` {Buffer|ArrayBuffer|ArrayBufferView} +* Returns: {bigint} + +Returns the raw memory address of JavaScript-managed byte storage. + +This is unsafe and dangerous. The returned pointer can become invalid if the +underlying memory is detached, resized, transferred, or otherwise invalidated. +Using stale pointers can cause memory corruption or process crashes. + ## Safety notes The `node:ffi` module does not track pointer validity, memory ownership, or diff --git a/lib/ffi.js b/lib/ffi.js index 944a01330d6ba0..b276f4b29dfcdc 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -2,9 +2,13 @@ const { ObjectFreeze, + ObjectPrototypeToString, } = primordials; const { Buffer } = require('buffer'); const { emitExperimentalWarning } = require('internal/util'); +const { + isArrayBufferView, +} = require('internal/util/types'); const { codes: { ERR_ACCESS_DENIED, @@ -32,6 +36,8 @@ const { getUint64, getFloat32, getFloat64, + exportBytes, + getRawPointer, setInt8, setUint8, setInt16, @@ -114,21 +120,52 @@ function exportString(str, data, len, encoding = 'utf8') { targetBuffer.fill(0, dataLength, dataLength + terminatorSize); } -function exportBuffer(buffer, data, len) { +function exportBuffer(source, data, len) { checkFFIPermission(); - if (!Buffer.isBuffer(buffer)) { - throw new ERR_INVALID_ARG_TYPE('buffer', 'Buffer', buffer); + if (!Buffer.isBuffer(source)) { + throw new ERR_INVALID_ARG_TYPE('buffer', 'Buffer', source); } validateInteger(len, 'len', 0); - if (len < buffer.length) { - throw new ERR_OUT_OF_RANGE('len', `>= ${buffer.length}`, len); + if (len < source.length) { + throw new ERR_OUT_OF_RANGE('len', `>= ${source.length}`, len); } - const targetBuffer = toBuffer(data, len, false); - buffer.copy(targetBuffer, 0, 0, buffer.length); + exportBytes(source, data, len); +} + +function exportArrayBuffer(source, data, len) { + checkFFIPermission(); + + if (ObjectPrototypeToString(source) !== '[object ArrayBuffer]') { + throw new ERR_INVALID_ARG_TYPE('arrayBuffer', 'ArrayBuffer', source); + } + + validateInteger(len, 'len', 0); + + if (len < source.byteLength) { + throw new ERR_OUT_OF_RANGE('len', `>= ${source.byteLength}`, len); + } + + exportBytes(source, data, len); +} + +function exportArrayBufferView(source, data, len) { + checkFFIPermission(); + + if (!isArrayBufferView(source)) { + throw new ERR_INVALID_ARG_TYPE('arrayBufferView', 'ArrayBufferView', source); + } + + validateInteger(len, 'len', 0); + + if (len < source.byteLength) { + throw new ERR_OUT_OF_RANGE('len', `>= ${source.byteLength}`, len); + } + + exportBytes(source, data, len); } const suffix = process.platform === 'win32' ? 'dll' : process.platform === 'darwin' ? 'dylib' : 'so'; @@ -163,6 +200,8 @@ module.exports = { dlopen, dlclose, dlsym, + exportArrayBuffer, + exportArrayBufferView, exportString, exportBuffer, getInt8, @@ -175,6 +214,7 @@ module.exports = { getUint64, getFloat32, getFloat64, + getRawPointer, setInt8, setUint8, setInt16, diff --git a/src/ffi/data.cc b/src/ffi/data.cc index 5ffedf0ffb5650..2135bc23786d64 100644 --- a/src/ffi/data.cc +++ b/src/ffi/data.cc @@ -13,6 +13,7 @@ #include using v8::ArrayBuffer; +using v8::ArrayBufferView; using v8::BackingStore; using v8::BigInt; using v8::Context; @@ -51,8 +52,8 @@ bool GetValidatedSize(Environment* env, } if (length > static_cast(std::numeric_limits::max())) { - env->ThrowRangeError( - (std::string("The ") + label + " is too large").c_str()); + THROW_ERR_OUT_OF_RANGE( + env, (std::string("The ") + label + " is too large").c_str()); return false; } @@ -81,7 +82,8 @@ bool GetValidatedPointerAddress(Environment* env, } if (address > static_cast(std::numeric_limits::max())) { - env->ThrowRangeError( + THROW_ERR_INVALID_ARG_VALUE( + env, (std::string("The ") + label + " exceeds the platform pointer range") .c_str()); return false; @@ -145,14 +147,14 @@ bool ValidatePointerSpan(Environment* env, size_t length, const char* error_message) { if (offset > std::numeric_limits::max() - raw_ptr) { - env->ThrowRangeError(error_message); + THROW_ERR_INVALID_ARG_VALUE(env, error_message); return false; } uintptr_t start = raw_ptr + offset; if (length > 0 && length - 1 > std::numeric_limits::max() - start) { - env->ThrowRangeError(error_message); + THROW_ERR_INVALID_ARG_VALUE(env, error_message); return false; } @@ -188,7 +190,7 @@ bool GetValidatedPointerAndOffset(Environment* env, } if (raw_ptr == 0) { - env->ThrowError("Cannot dereference a null pointer"); + THROW_ERR_FFI_INVALID_POINTER(env, "Cannot dereference a null pointer"); return false; } @@ -224,7 +226,7 @@ bool GetValidatedPointerValueAndOffset(Environment* env, } if (raw_ptr == 0) { - env->ThrowError("Cannot dereference a null pointer"); + THROW_ERR_FFI_INVALID_POINTER(env, "Cannot dereference a null pointer"); return false; } @@ -570,7 +572,8 @@ void ToBuffer(const FunctionCallbackInfo& args) { } if (ptr == 0 && len > 0) { - env->ThrowError("Cannot create a buffer from a null pointer"); + THROW_ERR_FFI_INVALID_POINTER(env, + "Cannot create a buffer from a null pointer"); return; } @@ -630,7 +633,8 @@ void ToArrayBuffer(const FunctionCallbackInfo& args) { } if (ptr == 0 && len > 0) { - env->ThrowError("Cannot create an ArrayBuffer from a null pointer"); + THROW_ERR_FFI_INVALID_POINTER( + env, "Cannot create an ArrayBuffer from a null pointer"); return; } @@ -668,6 +672,130 @@ void ToArrayBuffer(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ab); } +void ExportBytes(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); + + if (args.Length() < 1) { + THROW_ERR_INVALID_ARG_TYPE( + env, + "The first argument must be a Buffer, ArrayBuffer, or ArrayBufferView"); + return; + } + + uint8_t* source_data = nullptr; + size_t source_len = 0; + + if (Buffer::HasInstance(args[0])) { + source_data = reinterpret_cast(Buffer::Data(args[0])); + source_len = Buffer::Length(args[0]); + } else if (args[0]->IsArrayBuffer()) { + Local array_buffer = args[0].As(); + std::shared_ptr store = array_buffer->GetBackingStore(); + if (!store) { + THROW_ERR_INVALID_ARG_VALUE(env, "Invalid ArrayBuffer backing store"); + return; + } + source_data = static_cast(store->Data()); + source_len = array_buffer->ByteLength(); + } else if (args[0]->IsArrayBufferView()) { + Local view = args[0].As(); + std::shared_ptr store = view->Buffer()->GetBackingStore(); + if (!store) { + THROW_ERR_INVALID_ARG_VALUE(env, "Invalid ArrayBufferView backing store"); + return; + } + source_data = static_cast(store->Data()) + view->ByteOffset(); + source_len = view->ByteLength(); + } else { + THROW_ERR_INVALID_ARG_TYPE( + env, + "The first argument must be a Buffer, ArrayBuffer, or ArrayBufferView"); + return; + } + + uintptr_t ptr; + if (args.Length() < 2 || + !GetValidatedPointerAddress(env, args[1], "pointer", &ptr)) { + return; + } + + size_t len; + if (args.Length() < 3 || !GetValidatedSize(env, args[2], "length", &len)) { + return; + } + + if (len < source_len) { + THROW_ERR_OUT_OF_RANGE(env, "The length must be >= source byte length"); + return; + } + + if (ptr == 0 && source_len > 0) { + THROW_ERR_FFI_INVALID_POINTER(env, + "Cannot create a buffer from a null pointer"); + return; + } + + if (!ValidatePointerSpan( + env, + ptr, + 0, + len, + "The pointer and length exceed the platform address range")) { + return; + } + + if (source_len > 0) { + std::memcpy(reinterpret_cast(ptr), source_data, source_len); + } +} + +void GetRawPointer(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); + + if (args.Length() < 1) { + THROW_ERR_INVALID_ARG_TYPE( + env, + "The first argument must be a Buffer, ArrayBuffer, or ArrayBufferView"); + return; + } + + uintptr_t ptr = 0; + + if (Buffer::HasInstance(args[0])) { + ptr = reinterpret_cast(Buffer::Data(args[0])); + } else if (args[0]->IsArrayBuffer()) { + Local array_buffer = args[0].As(); + std::shared_ptr store = array_buffer->GetBackingStore(); + if (!store) { + THROW_ERR_INVALID_ARG_VALUE(env, "Invalid ArrayBuffer backing store"); + return; + } + ptr = reinterpret_cast(store->Data()); + } else if (args[0]->IsArrayBufferView()) { + Local view = args[0].As(); + std::shared_ptr store = view->Buffer()->GetBackingStore(); + if (!store) { + THROW_ERR_INVALID_ARG_VALUE(env, "Invalid ArrayBufferView backing store"); + return; + } + ptr = reinterpret_cast(static_cast(store->Data()) + + view->ByteOffset()); + } else { + THROW_ERR_INVALID_ARG_TYPE( + env, + "The first argument must be a Buffer, ArrayBuffer, or ArrayBufferView"); + return; + } + + args.GetReturnValue().Set( + BigInt::NewFromUnsigned(isolate, static_cast(ptr))); +} + } // namespace ffi } // namespace node diff --git a/src/ffi/types.cc b/src/ffi/types.cc index f70fa8d09a05dd..54f4c83f127b17 100644 --- a/src/ffi/types.cc +++ b/src/ffi/types.cc @@ -4,6 +4,7 @@ #include "base_object-inl.h" #include "data.h" #include "ffi.h" +#include "node_errors.h" #include "node_ffi.h" #include "v8.h" @@ -37,7 +38,8 @@ bool ThrowIfContainsNullBytes(Environment* env, const std::string& label) { if (value.length() != 0 && std::memchr(*value, '\0', value.length()) != nullptr) { - env->ThrowTypeError((label + " must not contain null bytes").c_str()); + THROW_ERR_INVALID_ARG_VALUE( + env, (label + " must not contain null bytes").c_str()); return true; } @@ -120,7 +122,7 @@ bool ParseFunctionSignature(Environment* env, std::string msg = "Function signature of " + name + " must have either 'returns', 'return' or 'result' " "property"; - env->ThrowTypeError(msg.c_str()); + THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str()); return false; } @@ -128,7 +130,7 @@ bool ParseFunctionSignature(Environment* env, std::string msg = "Function signature of " + name + " must have either 'parameters' or 'arguments' " "property"; - env->ThrowTypeError(msg.c_str()); + THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str()); return false; } @@ -154,7 +156,7 @@ bool ParseFunctionSignature(Environment* env, if (!return_type_val->IsString()) { std::string msg = "Return value type of function " + name + " must be a string"; - env->ThrowTypeError(msg.c_str()); + THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str()); return false; } @@ -178,7 +180,7 @@ bool ParseFunctionSignature(Environment* env, if (!arguments_val->IsArray()) { std::string msg = "Arguments list of function " + name + " must be an array"; - env->ThrowTypeError(msg.c_str()); + THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str()); return false; } @@ -195,7 +197,7 @@ bool ParseFunctionSignature(Environment* env, if (!arg->IsString()) { std::string msg = "Argument " + std::to_string(i) + " of function " + name + " must be a string"; - env->ThrowTypeError(msg.c_str()); + THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str()); return false; } @@ -236,7 +238,7 @@ bool SignaturesMatch(const FFIFunction& fn, bool ToFFIType(Environment* env, const std::string& type_str, ffi_type** ret) { if (ret == nullptr) { - env->ThrowTypeError("ret must not be null"); + THROW_ERR_INVALID_ARG_VALUE(env, "ret must not be null"); return false; } @@ -271,7 +273,7 @@ bool ToFFIType(Environment* env, const std::string& type_str, ffi_type** ret) { *ret = &ffi_type_pointer; } else { std::string msg = std::string("Unsupported FFI type: ") + type_str; - env->ThrowTypeError(msg.c_str()); + THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str()); return false; } @@ -291,7 +293,8 @@ uint8_t ToFFIArgument(Environment* env, int64_t value; if (!GetValidatedSignedInt(env, arg, INT8_MIN, INT8_MAX, "int8", &value)) { if (env->isolate()->IsExecutionTerminating()) return 0; - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be an int8").c_str()); return 0; } @@ -301,7 +304,8 @@ uint8_t ToFFIArgument(Environment* env, uint64_t value; if (!GetValidatedUnsignedInt(env, arg, UINT8_MAX, "uint8", &value)) { if (env->isolate()->IsExecutionTerminating()) return 0; - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be a uint8").c_str()); return 0; } @@ -312,7 +316,8 @@ uint8_t ToFFIArgument(Environment* env, if (!GetValidatedSignedInt( env, arg, INT16_MIN, INT16_MAX, "int16", &value)) { if (env->isolate()->IsExecutionTerminating()) return 0; - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be an int16").c_str()); return 0; } @@ -322,7 +327,8 @@ uint8_t ToFFIArgument(Environment* env, uint64_t value; if (!GetValidatedUnsignedInt(env, arg, UINT16_MAX, "uint16", &value)) { if (env->isolate()->IsExecutionTerminating()) return 0; - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be a uint16").c_str()); return 0; } @@ -330,7 +336,8 @@ uint8_t ToFFIArgument(Environment* env, *static_cast(ret) = static_cast(value); } else if (type == &ffi_type_sint32) { if (!arg->IsInt32()) { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be an int32").c_str()); return 0; } @@ -338,7 +345,8 @@ uint8_t ToFFIArgument(Environment* env, *static_cast(ret) = arg->Int32Value(context).FromJust(); } else if (type == &ffi_type_uint32) { if (!arg->IsUint32()) { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be a uint32").c_str()); return 0; } @@ -346,7 +354,8 @@ uint8_t ToFFIArgument(Environment* env, *static_cast(ret) = arg->Uint32Value(context).FromJust(); } else if (type == &ffi_type_sint64) { if (!arg->IsBigInt()) { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be an int64").c_str()); return 0; } @@ -354,13 +363,15 @@ uint8_t ToFFIArgument(Environment* env, bool lossless; *static_cast(ret) = arg.As()->Int64Value(&lossless); if (!lossless) { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be an int64").c_str()); return 0; } } else if (type == &ffi_type_uint64) { if (!arg->IsBigInt()) { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be a uint64").c_str()); return 0; } @@ -368,13 +379,15 @@ uint8_t ToFFIArgument(Environment* env, bool lossless; *static_cast(ret) = arg.As()->Uint64Value(&lossless); if (!lossless) { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be a uint64").c_str()); return 0; } } else if (type == &ffi_type_float) { if (!arg->IsNumber()) { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be a float").c_str()); return 0; } @@ -383,7 +396,8 @@ uint8_t ToFFIArgument(Environment* env, static_cast(arg->NumberValue(context).FromJust()); } else if (type == &ffi_type_double) { if (!arg->IsNumber()) { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be a double").c_str()); return 0; } @@ -405,7 +419,8 @@ uint8_t ToFFIArgument(Environment* env, std::shared_ptr store = view->Buffer()->GetBackingStore(); if (!store) { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Invalid ArrayBufferView backing store for argument " + std::to_string(index)) .c_str()); @@ -426,9 +441,11 @@ uint8_t ToFFIArgument(Environment* env, std::shared_ptr store = buffer->GetBackingStore(); if (!store) { - env->ThrowTypeError(("Invalid ArrayBuffer backing store for argument " + - std::to_string(index)) - .c_str()); + THROW_ERR_INVALID_ARG_VALUE( + env, + ("Invalid ArrayBuffer backing store for argument " + + std::to_string(index)) + .c_str()); return 0; } @@ -438,22 +455,25 @@ uint8_t ToFFIArgument(Environment* env, uint64_t pointer = arg.As()->Uint64Value(&lossless); if (!lossless || pointer > static_cast( std::numeric_limits::max())) { - env->ThrowTypeError(("Argument " + std::to_string(index) + - " must be a non-negative pointer bigint") - .c_str()); + THROW_ERR_INVALID_ARG_VALUE(env, + ("Argument " + std::to_string(index) + + " must be a non-negative pointer bigint") + .c_str()); return 0; } *static_cast(ret) = pointer; } else { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Argument " + std::to_string(index) + " must be a buffer, an ArrayBuffer, a string, or a bigint") .c_str()); return 0; } } else { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_VALUE( + env, ("Unsupported FFI type for argument " + std::to_string(index)).c_str()); return 0; } diff --git a/src/node_errors.h b/src/node_errors.h index 8f14b75b10493c..5cbd8f37f44bb9 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -79,6 +79,9 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details); V(ERR_DLOPEN_FAILED, Error) \ V(ERR_ENCODING_INVALID_ENCODED_DATA, TypeError) \ V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \ + V(ERR_FFI_INVALID_POINTER, Error) \ + V(ERR_FFI_LIBRARY_CLOSED, Error) \ + V(ERR_FFI_SYSCALL_FAILED, Error) \ V(ERR_FS_CP_EINVAL, Error) \ V(ERR_FS_CP_DIR_TO_NON_DIR, Error) \ V(ERR_FS_CP_NON_DIR_TO_DIR, Error) \ @@ -218,6 +221,7 @@ ERRORS_WITH_CODE(V) V(ERR_CRYPTO_UNSUPPORTED_OPERATION, "Unsupported crypto operation") \ V(ERR_CRYPTO_JOB_INIT_FAILED, "Failed to initialize crypto job config") \ V(ERR_DLOPEN_FAILED, "DLOpen failed") \ + V(ERR_FFI_LIBRARY_CLOSED, "Library is closed") \ V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, \ "Context not associated with Node.js environment") \ V(ERR_ILLEGAL_CONSTRUCTOR, "Illegal constructor") \ diff --git a/src/node_ffi.cc b/src/node_ffi.cc index d0c578cfedd46a..dc5143d811b1e4 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -87,7 +87,7 @@ bool DynamicLibrary::ResolveSymbol(Environment* env, const std::string& name, void** ret) { if (handle_ == nullptr) { - env->ThrowError("Library is closed"); + THROW_ERR_FFI_LIBRARY_CLOSED(env); return false; } @@ -99,7 +99,7 @@ bool DynamicLibrary::ResolveSymbol(Environment* env, } else { if (uv_dlsym(&lib_, name.c_str(), &ptr) != 0) { std::string msg = std::string("dlsym failed: ") + uv_dlerror(&lib_); - env->ThrowError(msg.c_str()); + THROW_ERR_FFI_SYSCALL_FAILED(env, msg.c_str()); return false; } } @@ -160,7 +160,7 @@ bool DynamicLibrary::PrepareFunction(Environment* env, break; } - env->ThrowError(msg); + THROW_ERR_FFI_SYSCALL_FAILED(env, msg); return false; } @@ -171,7 +171,7 @@ bool DynamicLibrary::PrepareFunction(Environment* env, if (!SignaturesMatch(*fn, return_type, args)) { std::string msg = "Function " + name + " was already requested with a different signature"; - env->ThrowError(msg.c_str()); + THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str()); return false; } } @@ -238,22 +238,34 @@ void DynamicLibrary::New(const FunctionCallbackInfo& args) { THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); +#ifndef _WIN32 + if (args.Length() < 1 || (!args[0]->IsString() && !args[0]->IsNull())) { + THROW_ERR_INVALID_ARG_TYPE(env, "Library path must be a string or null"); + return; + } +#else if (args.Length() < 1 || !args[0]->IsString()) { - env->ThrowTypeError("Library path must be a string"); + THROW_ERR_INVALID_ARG_TYPE(env, "Library path must be a string"); return; } +#endif + char* library_path = nullptr; DynamicLibrary* lib = new DynamicLibrary(env, args.This()); - Utf8Value filename(env->isolate(), args[0]); - if (ThrowIfContainsNullBytes(env, filename, "Library path")) { - return; + + if (args[0]->IsString()) { + Utf8Value filename(env->isolate(), args[0]); + if (ThrowIfContainsNullBytes(env, filename, "Library path")) { + return; + } + library_path = *filename; + lib->path_ = std::string(*filename); } - lib->path_ = std::string(*filename); // Open the library - if (uv_dlopen(*filename, &lib->lib_) != 0) { + if (uv_dlopen(library_path, &lib->lib_) != 0) { std::string msg = std::string("dlopen failed: ") + uv_dlerror(&lib->lib_); - env->ThrowError(msg.c_str()); + THROW_ERR_FFI_SYSCALL_FAILED(env, msg.c_str()); return; } @@ -274,7 +286,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo& args) { FFIFunction* fn = info->fn.get(); if (fn == nullptr || fn->closed || fn->ptr == nullptr) { - env->ThrowError("Library is closed"); + THROW_ERR_FFI_LIBRARY_CLOSED(env); return; } @@ -286,7 +298,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo& args) { std::string msg = "Invalid argument count: expected " + std::to_string(expected_args) + ", got " + std::to_string(provided_args); - env->ThrowError(msg.c_str()); + THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str()); return; } @@ -307,7 +319,8 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo& args) { Utf8Value str(env->isolate(), args[i]); if (*str == nullptr) { - env->ThrowTypeError( + THROW_ERR_INVALID_ARG_TYPE( + env, ("Argument " + std::to_string(i) + " must be a string").c_str()); return; } @@ -337,6 +350,14 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo& args) { free(result); } +// This is the function that will be called by libffi when a callback +// is invoked from a dlopen library. It converts the arguments to JavaScript +// values and calls the original JavaScript callback function. +// It also handles the return value and exceptions properly. +// Note that since this function is called from native code, it must not throw +// exceptions or return promises, as there is no defined way to propagate them +// back to the caller. +// If such cases occur, the process will be aborted to avoid undefined behavior. void DynamicLibrary::InvokeCallback(ffi_cif* cif, void* ret, void** args, @@ -434,12 +455,12 @@ void DynamicLibrary::GetFunction(const FunctionCallbackInfo& args) { Isolate* isolate = env->isolate(); if (args.Length() < 1 || !args[0]->IsString()) { - env->ThrowTypeError("Function name must be a string"); + THROW_ERR_INVALID_ARG_TYPE(env, "Function name must be a string"); return; } if (args.Length() < 2 || !args[1]->IsObject() || args[1]->IsArray()) { - env->ThrowTypeError("Function signature must be an object"); + THROW_ERR_INVALID_ARG_TYPE(env, "Function signature must be an object"); return; } @@ -484,7 +505,7 @@ void DynamicLibrary::GetFunctions(const FunctionCallbackInfo& args) { DynamicLibrary* lib = Unwrap(args.This()); if (lib->handle_ == nullptr) { - env->ThrowError("Library is closed"); + THROW_ERR_FFI_LIBRARY_CLOSED(env); return; } @@ -495,7 +516,7 @@ void DynamicLibrary::GetFunctions(const FunctionCallbackInfo& args) { if (args.Length() > 0) { if (!args[0]->IsObject() || args[0]->IsArray()) { - env->ThrowTypeError("Functions signatures must be an object"); + THROW_ERR_INVALID_ARG_TYPE(env, "Functions signatures must be an object"); return; } @@ -528,7 +549,7 @@ void DynamicLibrary::GetFunctions(const FunctionCallbackInfo& args) { if (!signature->IsObject() || signature->IsArray()) { std::string msg = std::string("Signature of function ") + name.out() + " must be an object"; - env->ThrowTypeError(msg.c_str()); + THROW_ERR_INVALID_ARG_TYPE(env, msg.c_str()); return; } @@ -612,7 +633,7 @@ void DynamicLibrary::GetSymbol(const FunctionCallbackInfo& args) { Isolate* isolate = env->isolate(); if (args.Length() < 1 || !args[0]->IsString()) { - env->ThrowTypeError("Symbol name must be a string"); + THROW_ERR_INVALID_ARG_TYPE(env, "Symbol name must be a string"); return; } @@ -640,7 +661,7 @@ void DynamicLibrary::GetSymbols(const FunctionCallbackInfo& args) { DynamicLibrary* lib = Unwrap(args.This()); if (lib->handle_ == nullptr) { - env->ThrowError("Library is closed"); + THROW_ERR_FFI_LIBRARY_CLOSED(env); return; } @@ -680,8 +701,8 @@ void DynamicLibrary::RegisterCallback(const FunctionCallbackInfo& args) { Local fn; if (args.Length() < 1) { - env->ThrowTypeError( - "First argument must be a function or a signature object"); + THROW_ERR_INVALID_ARG_TYPE( + env, "First argument must be a function or a signature object"); return; } @@ -689,13 +710,13 @@ void DynamicLibrary::RegisterCallback(const FunctionCallbackInfo& args) { fn = args[0].As(); } else { if (!args[0]->IsObject() || args[0]->IsArray()) { - env->ThrowTypeError( - "First argument must be a function or a signature object"); + THROW_ERR_INVALID_ARG_TYPE( + env, "First argument must be a function or a signature object"); return; } if (args.Length() < 2 || !args[1]->IsFunction()) { - env->ThrowTypeError("Second argument must be a function"); + THROW_ERR_INVALID_ARG_TYPE(env, "Second argument must be a function"); return; } @@ -712,7 +733,7 @@ void DynamicLibrary::RegisterCallback(const FunctionCallbackInfo& args) { DynamicLibrary* lib = Unwrap(args.This()); if (lib->handle_ == nullptr) { - env->ThrowError("Library is closed"); + THROW_ERR_FFI_LIBRARY_CLOSED(env); return; } @@ -730,7 +751,7 @@ void DynamicLibrary::RegisterCallback(const FunctionCallbackInfo& args) { ffi_closure_alloc(sizeof(ffi_closure), &callback->ptr)); if (callback->closure == nullptr) { - env->ThrowError("ffi_closure_alloc failed"); + THROW_ERR_FFI_SYSCALL_FAILED(env, "ffi_closure_alloc failed"); delete callback; return; } @@ -755,7 +776,7 @@ void DynamicLibrary::RegisterCallback(const FunctionCallbackInfo& args) { break; } - env->ThrowError(msg); + THROW_ERR_FFI_SYSCALL_FAILED(env, msg); delete callback; return; } @@ -779,7 +800,7 @@ void DynamicLibrary::RegisterCallback(const FunctionCallbackInfo& args) { break; } - env->ThrowError(msg); + THROW_ERR_FFI_SYSCALL_FAILED(env, msg); delete callback; return; } @@ -796,12 +817,12 @@ void DynamicLibrary::UnregisterCallback( DynamicLibrary* lib = Unwrap(args.This()); if (lib->handle_ == nullptr) { - env->ThrowError("Library is closed"); + THROW_ERR_FFI_LIBRARY_CLOSED(env); return; } if (args.Length() < 1 || !args[0]->IsBigInt()) { - env->ThrowTypeError("The first argument must be a bigint"); + THROW_ERR_INVALID_ARG_TYPE(env, "The first argument must be a bigint"); return; } @@ -814,7 +835,7 @@ void DynamicLibrary::UnregisterCallback( auto existing = lib->callbacks_.find(ptr); if (existing == lib->callbacks_.end()) { - env->ThrowError("Callback not found"); + THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found"); return; } @@ -831,12 +852,12 @@ void DynamicLibrary::RefCallback(const FunctionCallbackInfo& args) { DynamicLibrary* lib = Unwrap(args.This()); if (lib->handle_ == nullptr) { - env->ThrowError("Library is closed"); + THROW_ERR_FFI_LIBRARY_CLOSED(env); return; } if (args.Length() < 1 || !args[0]->IsBigInt()) { - env->ThrowTypeError("The first argument must be a bigint"); + THROW_ERR_INVALID_ARG_TYPE(env, "The first argument must be a bigint"); return; } @@ -849,7 +870,7 @@ void DynamicLibrary::RefCallback(const FunctionCallbackInfo& args) { auto existing = lib->callbacks_.find(ptr); if (existing == lib->callbacks_.end()) { - env->ThrowError("Callback not found"); + THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found"); return; } @@ -861,12 +882,12 @@ void DynamicLibrary::UnrefCallback(const FunctionCallbackInfo& args) { DynamicLibrary* lib = Unwrap(args.This()); if (lib->handle_ == nullptr) { - env->ThrowError("Library is closed"); + THROW_ERR_FFI_LIBRARY_CLOSED(env); return; } if (args.Length() < 1 || !args[0]->IsBigInt()) { - env->ThrowTypeError("The first argument must be a bigint"); + THROW_ERR_INVALID_ARG_TYPE(env, "The first argument must be a bigint"); return; } @@ -879,7 +900,7 @@ void DynamicLibrary::UnrefCallback(const FunctionCallbackInfo& args) { auto existing = lib->callbacks_.find(ptr); if (existing == lib->callbacks_.end()) { - env->ThrowError("Callback not found"); + THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found"); return; } @@ -952,6 +973,8 @@ static void Initialize(Local target, SetMethod(context, target, "toString", ToString); SetMethod(context, target, "toBuffer", ToBuffer); SetMethod(context, target, "toArrayBuffer", ToArrayBuffer); + SetMethod(context, target, "exportBytes", ExportBytes); + SetMethod(context, target, "getRawPointer", GetRawPointer); SetMethod(context, target, "getInt8", GetInt8); SetMethod(context, target, "getUint8", GetUint8); diff --git a/src/node_ffi.h b/src/node_ffi.h index cbb0b82e4d22af..a4c518ee8171f0 100644 --- a/src/node_ffi.h +++ b/src/node_ffi.h @@ -144,6 +144,8 @@ void SetFloat64(const v8::FunctionCallbackInfo& args); void ToString(const v8::FunctionCallbackInfo& args); void ToBuffer(const v8::FunctionCallbackInfo& args); void ToArrayBuffer(const v8::FunctionCallbackInfo& args); +void ExportBytes(const v8::FunctionCallbackInfo& args); +void GetRawPointer(const v8::FunctionCallbackInfo& args); } // namespace node::ffi diff --git a/test/ffi/test-ffi-dynamic-library.js b/test/ffi/test-ffi-dynamic-library.js index b4ba6c3537f294..8ff2e9de816317 100644 --- a/test/ffi/test-ffi-dynamic-library.js +++ b/test/ffi/test-ffi-dynamic-library.js @@ -21,6 +21,21 @@ test('dlopen without definitions returns empty function map', () => { } }); +test('dlopen resolves symbols from the current process with null path', { + skip: common.isWindows, +}, () => { + const { lib, functions } = ffi.dlopen(null, { + uv_os_getpid: { result: 'i32', parameters: [] }, + }); + + try { + assert.ok(lib instanceof ffi.DynamicLibrary); + assert.strictEqual(functions.uv_os_getpid(), process.pid); + } finally { + lib.close(); + } +}); + test('dlopen resolves functions from definitions', () => { const { lib, functions } = ffi.dlopen(libraryPath, { add_i32: fixtureSymbols.add_i32, diff --git a/test/ffi/test-ffi-memory.js b/test/ffi/test-ffi-memory.js index 66a4bfd9d93e65..5a6667fa512bcb 100644 --- a/test/ffi/test-ffi-memory.js +++ b/test/ffi/test-ffi-memory.js @@ -12,6 +12,7 @@ const { fixtureSymbols, libraryPath } = require('./ffi-test-common'); const { lib, functions: symbols } = ffi.dlopen(libraryPath, { allocate_memory: fixtureSymbols.allocate_memory, deallocate_memory: fixtureSymbols.deallocate_memory, + pointer_to_usize: fixtureSymbols.pointer_to_usize, }); after(() => lib.close()); @@ -120,6 +121,24 @@ test('ffi toArrayBuffer supports copy and zero-copy views', () => { })); }); +test('ffi getRawPointer returns raw addresses for byte sources', () => { + const buffer = Buffer.from([1, 2, 3]); + const arrayBuffer = new Uint8Array([4, 5, 6, 7]).buffer; + const view = new Uint8Array(arrayBuffer, 2); + + const bufferPointer = ffi.getRawPointer(buffer); + const arrayBufferPointer = ffi.getRawPointer(arrayBuffer); + const viewPointer = ffi.getRawPointer(view); + + assert.strictEqual(typeof bufferPointer, 'bigint'); + assert.strictEqual(typeof arrayBufferPointer, 'bigint'); + assert.strictEqual(typeof viewPointer, 'bigint'); + + assert.strictEqual(bufferPointer, symbols.pointer_to_usize(buffer)); + assert.strictEqual(arrayBufferPointer, symbols.pointer_to_usize(arrayBuffer)); + assert.strictEqual(viewPointer, arrayBufferPointer + 2n); +}); + test('ffi exportString and exportBuffer copy data into native memory', () => { withAllocations(common.mustCall((alloc) => { const stringPtr = alloc(16); @@ -148,6 +167,22 @@ test('ffi exportString and exportBuffer copy data into native memory', () => { assert.throws(() => ffi.exportBuffer(Buffer.from([1, 2, 3, 4, 5, 6, 7]), bufferPtr, 6), { code: 'ERR_OUT_OF_RANGE', }); + + const arrayBufferPtr = alloc(8); + const arrayBuffer = new Uint8Array([8, 9, 10, 11]).buffer; + ffi.exportArrayBuffer(arrayBuffer, arrayBufferPtr, 4); + assert.deepStrictEqual([...ffi.toBuffer(arrayBufferPtr, 4)], [8, 9, 10, 11]); + + const viewPtr = alloc(8); + const viewSource = new Uint16Array([0x0102, 0x0304, 0x0506]); + const middleBytes = new Uint8Array(viewSource.buffer, 2, 2); + ffi.exportArrayBufferView(middleBytes, viewPtr, 2); + assert.deepStrictEqual([...ffi.toBuffer(viewPtr, 2)], [0x04, 0x03]); + + const bufferViewPtr = alloc(8); + const bufferView = Buffer.from([1, 7, 2, 8, 3]); + ffi.exportArrayBufferView(bufferView.subarray(1, 4), bufferViewPtr, 3); + assert.deepStrictEqual([...ffi.toBuffer(bufferViewPtr, 3)], [7, 2, 8]); })); }); @@ -169,6 +204,8 @@ test('ffi validates memory access arguments', () => { assert.throws(() => ffi.toArrayBuffer(ptr, 'bad'), /The length must be a number/); assert.throws(() => ffi.toArrayBuffer(-1n, 4), /The first argument must be a non-negative bigint/); assert.throws(() => ffi.toArrayBuffer(0n, 1), /Cannot create an ArrayBuffer from a null pointer/); + assert.throws(() => ffi.getRawPointer('bad'), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => ffi.getRawPointer(1), { code: 'ERR_INVALID_ARG_TYPE' }); assert.throws(() => ffi.getInt32(0n), /Cannot dereference a null pointer/); assert.throws(() => ffi.getInt32(-1n), /The pointer must be a non-negative bigint/); assert.throws(() => ffi.getInt8(maxPointer, 8), /pointer and offset exceed the platform address range/); @@ -201,6 +238,12 @@ test('ffi validates memory access arguments', () => { assert.throws(() => ffi.exportBuffer('bad', ptr, 4), { code: 'ERR_INVALID_ARG_TYPE' }); assert.throws(() => ffi.exportBuffer(Buffer.from([1]), ptr, -1), { code: 'ERR_OUT_OF_RANGE' }); assert.throws(() => ffi.exportBuffer(Buffer.from([1, 2]), ptr, 1), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => ffi.exportArrayBuffer('bad', ptr, 4), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => ffi.exportArrayBuffer(new ArrayBuffer(1), ptr, -1), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => ffi.exportArrayBuffer(new ArrayBuffer(2), ptr, 1), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => ffi.exportArrayBufferView('bad', ptr, 4), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => ffi.exportArrayBufferView(new Uint8Array([1]), ptr, -1), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => ffi.exportArrayBufferView(new Uint8Array([1, 2]), ptr, 1), { code: 'ERR_OUT_OF_RANGE' }); assert.throws(() => ffi.toBuffer(maxPointer, 8), /pointer and length exceed the platform address range/); assert.throws(() => ffi.toArrayBuffer(maxPointer, 8), /pointer and length exceed the platform address range/); assert.throws(() => ffi.toBuffer(1n, bufferConstants.MAX_LENGTH + 1), { code: 'ERR_BUFFER_TOO_LARGE' }); diff --git a/test/ffi/test-ffi-module.js b/test/ffi/test-ffi-module.js index 2dc18c9eaa41b7..1cd2b840097d6b 100644 --- a/test/ffi/test-ffi-module.js +++ b/test/ffi/test-ffi-module.js @@ -83,6 +83,8 @@ test('ffi exports expected API surface', () => { 'dlclose', 'dlopen', 'dlsym', + 'exportArrayBuffer', + 'exportArrayBufferView', 'exportBuffer', 'exportString', 'getFloat32', @@ -91,6 +93,7 @@ test('ffi exports expected API surface', () => { 'getInt32', 'getInt64', 'getInt8', + 'getRawPointer', 'getUint16', 'getUint32', 'getUint64', @@ -117,8 +120,11 @@ test('ffi exports expected API surface', () => { assert.strictEqual(typeof ffi.dlopen, 'function'); assert.strictEqual(typeof ffi.dlclose, 'function'); assert.strictEqual(typeof ffi.dlsym, 'function'); + assert.strictEqual(typeof ffi.exportArrayBuffer, 'function'); + assert.strictEqual(typeof ffi.exportArrayBufferView, 'function'); assert.strictEqual(typeof ffi.exportString, 'function'); assert.strictEqual(typeof ffi.exportBuffer, 'function'); + assert.strictEqual(typeof ffi.getRawPointer, 'function'); assert.strictEqual(typeof ffi.getInt8, 'function'); assert.strictEqual(typeof ffi.getUint8, 'function'); assert.strictEqual(typeof ffi.getInt16, 'function'); diff --git a/test/ffi/test-ffi-permissions.js b/test/ffi/test-ffi-permissions.js index f5b66c1d5d8ffe..c07f0bbdb439d7 100644 --- a/test/ffi/test-ffi-permissions.js +++ b/test/ffi/test-ffi-permissions.js @@ -58,6 +58,18 @@ test('permission model blocks ffi memory and helper APIs', () => { ffi.exportBuffer(Buffer.alloc(0), 1n, 0); }, denied); + assert.throws(() => { + ffi.exportArrayBuffer(new ArrayBuffer(0), 1n, 0); + }, denied); + + assert.throws(() => { + ffi.exportArrayBufferView(new Uint8Array(0), 1n, 0); + }, denied); + + assert.throws(() => { + ffi.getRawPointer(Buffer.alloc(0)); + }, denied); + assert.throws(() => { ffi.dlclose({ close() {} }); }, denied);