Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions lib/internal/ffi/fast-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const {
ArrayPrototypeIncludes,
NumberIsInteger,
ObjectDefineProperty,
ReflectApply,
StringPrototypeIncludes,
Expand All @@ -19,6 +20,7 @@ const {
} = require('internal/util/types');

const {
charIsSigned,
getRawPointer,
kFastArguments,
kFastBufferInvoke,
Expand All @@ -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);
Expand All @@ -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')) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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)) {
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -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);
};
Expand Down
14 changes: 14 additions & 0 deletions src/ffi/fast.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/ffi/fast.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<FFIFunction> CloneWithFastBufferArgNames(
Expand Down
11 changes: 6 additions & 5 deletions src/node_ffi.cc
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,11 @@ MaybeLocal<Function> 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 =
Expand Down Expand Up @@ -381,7 +382,7 @@ MaybeLocal<Function> 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.
Expand Down
56 changes: 56 additions & 0 deletions test/ffi/test-ffi-fast-integer-validation.js
Original file line number Diff line number Diff line change
@@ -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();
}
});
Loading