From 40d2aa3c090c3da76aeb584710c5254748139c2d Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:19:57 -0700 Subject: [PATCH] ffi: validate fast integer argument ranges Validate narrow integer and 64-bit BigInt arguments before entering the Fast API trampoline. This prevents out-of-range values from being silently truncated or wrapped and matches the generic FFI path. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.6-sol --- lib/internal/ffi/fast-api.js | 68 +++++++++++++++++--- src/ffi/fast.cc | 14 ++++ src/ffi/fast.h | 1 + src/node_ffi.cc | 11 ++-- test/ffi/test-ffi-fast-integer-validation.js | 56 ++++++++++++++++ 5 files changed, 135 insertions(+), 15 deletions(-) create mode 100644 test/ffi/test-ffi-fast-integer-validation.js diff --git a/lib/internal/ffi/fast-api.js b/lib/internal/ffi/fast-api.js index 15b13af23ceaa1..11e8c005dee98c 100644 --- a/lib/internal/ffi/fast-api.js +++ b/lib/internal/ffi/fast-api.js @@ -2,6 +2,7 @@ const { ArrayPrototypeIncludes, + NumberIsInteger, ObjectDefineProperty, ReflectApply, StringPrototypeIncludes, @@ -19,6 +20,7 @@ const { } = require('internal/util/types'); const { + charIsSigned, getRawPointer, kFastArguments, kFastBufferInvoke, @@ -28,6 +30,33 @@ const { const kFastBuffer = Symbol('kFastBuffer'); const kStringConversionBuffer = Symbol('kStringConversionBuffer'); +const U64_MAX = 0xFFFFFFFFFFFFFFFFn; +const I64_MAX = 0x7FFFFFFFFFFFFFFFn; +const I64_MIN = -0x8000000000000000n; + +// These ranges mirror ToFFIArgument in src/ffi/types.cc. V8's Fast API +// exposes narrow integers as 32-bit values and uses truncating BigInt +// conversions, so the public FFI ranges must be checked before the raw call. +const fastIntegerTypeInfo = { + __proto__: null, + i8: { kind: 'number', min: -128, max: 127, label: 'an int8' }, + int8: { kind: 'number', min: -128, max: 127, label: 'an int8' }, + char: charIsSigned ? + { kind: 'number', min: -128, max: 127, label: 'an int8' } : + { kind: 'number', min: 0, max: 255, label: 'a uint8' }, + u8: { kind: 'number', min: 0, max: 255, label: 'a uint8' }, + uint8: { kind: 'number', min: 0, max: 255, label: 'a uint8' }, + bool: { kind: 'number', min: 0, max: 255, label: 'a uint8' }, + i16: { kind: 'number', min: -32768, max: 32767, label: 'an int16' }, + int16: { kind: 'number', min: -32768, max: 32767, label: 'an int16' }, + u16: { kind: 'number', min: 0, max: 65535, label: 'a uint16' }, + uint16: { kind: 'number', min: 0, max: 65535, label: 'a uint16' }, + i64: { kind: 'bigint', min: I64_MIN, max: I64_MAX, label: 'an int64' }, + int64: { kind: 'bigint', min: I64_MIN, max: I64_MAX, label: 'an int64' }, + u64: { kind: 'bigint', min: 0n, max: U64_MAX, label: 'a uint64' }, + uint64: { kind: 'bigint', min: 0n, max: U64_MAX, label: 'a uint64' }, +}; + function throwFFIArgError(msg) { // eslint-disable-next-line no-restricted-syntax const err = new TypeError(msg); @@ -40,6 +69,17 @@ function throwFFIArgCountError(expected, actual) { `Invalid argument count: expected ${expected}, got ${actual}`); } +function validateFastIntegerArg(type, value, index) { + const info = fastIntegerTypeInfo[type]; + if (info === undefined) return; + const validType = info.kind === 'number' ? + typeof value === 'number' && NumberIsInteger(value) : + typeof value === 'bigint'; + if (!validType || value < info.min || value > info.max) { + throwFFIArgError(`Argument ${index} must be ${info.label}`); + } +} + function needsRawPointerConversion(type, rawFn) { if (rawFn !== undefined && rawFn[kFastBuffer] === true && (type === 'buffer' || type === 'arraybuffer')) { @@ -134,10 +174,11 @@ function convertPointerArg(type, value, owner, index) { return value; } -function getPointerConversionIndexes(argumentsTypes, rawFn) { +function getFastArgumentIndexes(argumentsTypes, rawFn) { let indexes = null; for (let i = 0; i < argumentsTypes.length; i++) { - if (!needsPointerConversion(argumentsTypes[i], rawFn)) { + if (fastIntegerTypeInfo[argumentsTypes[i]] === undefined && + !needsPointerConversion(argumentsTypes[i], rawFn)) { continue; } if (indexes === null) { @@ -148,6 +189,12 @@ function getPointerConversionIndexes(argumentsTypes, rawFn) { return indexes; } +function convertFastArg(type, value, rawFn, owner, index) { + validateFastIntegerArg(type, value, index); + return needsPointerConversion(type, rawFn) ? + convertPointerArg(type, value, owner, index) : value; +} + function initializeFastBufferMetadata(rawFn, argumentTypes) { if (rawFn === undefined || rawFn === null || argumentTypes === undefined) { return; @@ -192,7 +239,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { return rawFn; } - const indexes = getPointerConversionIndexes(argumentTypes, rawFn); + const indexes = getFastArgumentIndexes(argumentTypes, rawFn); if (indexes === null) { return rawFn; } @@ -209,6 +256,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { if (arguments.length !== 1) { throwFFIArgCountError(1, arguments.length); } + validateFastIntegerArg(t0, a0, 0); let arg = a0; if (needsNullPointerConversion(t0) && (arg === null || arg === undefined)) { @@ -232,8 +280,8 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { if (arguments.length !== 2) { throwFFIArgCountError(2, arguments.length); } - return rawFn(c0 ? convertPointerArg(t0, a0, owner, 0) : a0, - c1 ? convertPointerArg(t1, a1, owner, 1) : a1); + return rawFn(c0 ? convertFastArg(t0, a0, rawFn, owner, 0) : a0, + c1 ? convertFastArg(t1, a1, rawFn, owner, 1) : a1); }; } else if (nargs === 3) { const c0 = ArrayPrototypeIncludes(indexes, 0); @@ -246,9 +294,9 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { if (arguments.length !== 3) { throwFFIArgCountError(3, arguments.length); } - return rawFn(c0 ? convertPointerArg(t0, a0, owner, 0) : a0, - c1 ? convertPointerArg(t1, a1, owner, 1) : a1, - c2 ? convertPointerArg(t2, a2, owner, 2) : a2); + return rawFn(c0 ? convertFastArg(t0, a0, rawFn, owner, 0) : a0, + c1 ? convertFastArg(t1, a1, rawFn, owner, 1) : a1, + c2 ? convertFastArg(t2, a2, rawFn, owner, 2) : a2); }; } else { wrapper = function(...args) { @@ -257,8 +305,8 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { } for (let i = 0; i < indexes.length; i++) { const index = indexes[i]; - args[index] = convertPointerArg( - argumentTypes[index], args[index], owner, index); + args[index] = convertFastArg( + argumentTypes[index], args[index], rawFn, owner, index); } return ReflectApply(rawFn, undefined, args); }; diff --git a/src/ffi/fast.cc b/src/ffi/fast.cc index 13c039c0da3f17..7d4b8b6c3a6bc8 100644 --- a/src/ffi/fast.cc +++ b/src/ffi/fast.cc @@ -160,6 +160,20 @@ bool SignatureNeedsRawPointerConversions(const FFIFunction& fn) { return false; } +bool SignatureNeedsFastIntegerValidation(const FFIFunction& fn) { + // V8 widens narrow integers to 32 bits and truncates BigInts to 64 bits for + // Fast API calls. These types need a JS range check before the trampoline. + for (const std::string& name : fn.arg_type_names) { + if (name == "bool" || name == "char" || name == "i8" || name == "int8" || + name == "u8" || name == "uint8" || name == "i16" || name == "int16" || + name == "u16" || name == "uint16" || name == "i64" || name == "int64" || + name == "u64" || name == "uint64") { + return true; + } + } + return false; +} + bool IsPointerTypeName(const std::string& name) { // `pointer`, `ptr`, and `function` all use the same uintptr ABI slot; only // the public type spelling differs. diff --git a/src/ffi/fast.h b/src/ffi/fast.h index bd0a2bac88f824..fc9f69970efebe 100644 --- a/src/ffi/fast.h +++ b/src/ffi/fast.h @@ -59,6 +59,7 @@ struct FastFFIMetadata { bool IsFastCallSupported(); bool SignatureNeedsRawPointerConversions(const FFIFunction& fn); +bool SignatureNeedsFastIntegerValidation(const FFIFunction& fn); bool IsPointerTypeName(const std::string& name); bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn); std::shared_ptr CloneWithFastBufferArgNames( diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 8e5729cb4ee0d8..23c58e8ea12813 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -253,10 +253,11 @@ MaybeLocal DynamicLibrary::CreateFunction( bool use_fast_api = info->fast_metadata != nullptr; bool use_sb = !use_fast_api && IsSBEligibleSignature(*fn); bool has_ptr_args = use_sb && SignatureHasPointerArgs(*fn); - // Fast API signatures that still accept JS pointer-like values need a JS - // wrapper with the native type names attached as hidden metadata. - bool needs_raw_pointer_conversions = - use_fast_api && SignatureNeedsRawPointerConversions(*fn); + // Fast API signatures that need JS-side argument conversion or range checks + // use a wrapper with the native type names attached as hidden metadata. + bool needs_fast_argument_wrapper = + use_fast_api && (SignatureNeedsRawPointerConversions(*fn) || + SignatureNeedsFastIntegerValidation(*fn)); // A single pointer-like parameter can get a separate Buffer-aware Fast API // entrypoint so Buffer calls avoid JS pointer extraction. bool needs_fast_buffer_invoke = @@ -381,7 +382,7 @@ MaybeLocal DynamicLibrary::CreateFunction( } } - if (needs_raw_pointer_conversions || needs_fast_buffer_invoke) { + if (needs_fast_argument_wrapper || needs_fast_buffer_invoke) { // Fast API wrappers need only the parameter type names. Result conversion // is still handled by V8's CFunction metadata, unlike the SharedBuffer path // which must also know how to read slot 0. diff --git a/test/ffi/test-ffi-fast-integer-validation.js b/test/ffi/test-ffi-fast-integer-validation.js new file mode 100644 index 00000000000000..49fd1364948c35 --- /dev/null +++ b/test/ffi/test-ffi-fast-integer-validation.js @@ -0,0 +1,56 @@ +// Flags: --experimental-ffi --allow-natives-syntax +'use strict'; + +const common = require('../common'); +common.skipIfFFIMissing(); + +const assert = require('node:assert'); +const { test } = require('node:test'); +const ffi = require('node:ffi'); +const { fixtureSymbols, libraryPath } = require('./ffi-test-common'); + +function optimize(fn, value) { + eval('%PrepareFunctionForOptimization(fn)'); + fn(value); + fn(value); + eval('%OptimizeFunctionOnNextCall(fn)'); + fn(value); +} + +test('fast FFI validates integer argument ranges', () => { + const { lib, functions } = ffi.dlopen(libraryPath, fixtureSymbols); + try { + function callI8(value) { return functions.add_i8(value, 0); } + + function callU8(value) { return functions.add_u8(value, 0); } + + function callI16(value) { return functions.add_i16(value, 0); } + + function callU16(value) { return functions.add_u16(value, 0); } + + function callI64(value) { return functions.add_i64(value, 0n); } + + function callU64(value) { return functions.add_u64(value, 0n); } + + for (const [fn, value] of [ + [callI8, 0], + [callU8, 0], + [callI16, 0], + [callU16, 0], + [callI64, 0n], + [callU64, 0n], + ]) { + optimize(fn, value); + } + + const expect = { code: 'ERR_INVALID_ARG_VALUE' }; + assert.throws(() => callI8(128), expect); + assert.throws(() => callU8(256), expect); + assert.throws(() => callI16(32768), expect); + assert.throws(() => callU16(65536), expect); + assert.throws(() => callI64(2n ** 63n), expect); + assert.throws(() => callU64(2n ** 64n), expect); + } finally { + lib.close(); + } +});