From d73bf5f43efc3495b0d295d636831d0979bbc43a Mon Sep 17 00:00:00 2001 From: HoonDongKang Date: Mon, 28 Sep 2026 13:30:22 +0900 Subject: [PATCH 1/3] ffi: accept safe integer numbers for 64-bit arguments Allow safe integer numbers as int64 and uint64 arguments alongside bigint values. Reject negative numbers for uint64 and numbers outside the safe integer range. Keep 64-bit return values as bigint. Apply validation and conversion across the Fast API, shared-buffer, and generic argument conversion paths. Add coverage for Number and BigInt boundaries, invalid inputs, and single-argument calls before and after optimization. Signed-off-by: HoonDongKang Assisted-by: Codex:Astra-medium --- lib/internal/ffi-shared-buffer.js | 17 +++- lib/internal/ffi/fast-api.js | 35 ++++++-- src/ffi/types.cc | 51 +++++++---- test/ffi/fixture_library/ffi_test_library.c | 24 +++++ test/ffi/test-ffi-calls.js | 30 +++++-- test/ffi/test-ffi-fast-integer-validation.js | 80 ++++++++++++++++- test/ffi/test-ffi-shared-buffer.js | 95 +++++++++++++++++++- 7 files changed, 292 insertions(+), 40 deletions(-) diff --git a/lib/internal/ffi-shared-buffer.js b/lib/internal/ffi-shared-buffer.js index 13002193cd9c..68b39ab62f9e 100644 --- a/lib/internal/ffi-shared-buffer.js +++ b/lib/internal/ffi-shared-buffer.js @@ -1,6 +1,7 @@ 'use strict'; const { + BigInt, DataView, DataViewPrototypeGetBigInt64, DataViewPrototypeGetBigUint64, @@ -23,6 +24,7 @@ const { DataViewPrototypeSetUint32, DataViewPrototypeSetUint8, NumberIsInteger, + NumberIsSafeInteger, ObjectDefineProperty, ReflectApply, TypeError, @@ -132,14 +134,25 @@ function writeNumericArg(view, info, offset, arg, index) { return; } if (kind === 'i64') { - if (typeof arg !== 'bigint' || arg < I64_MIN || arg > I64_MAX) { + if (typeof arg === 'number') { + if (!NumberIsSafeInteger(arg)) { + throwFFIArgError(`Argument ${index} must be ${info.label}`); + } + arg = BigInt(arg); + } else if (typeof arg !== 'bigint' || arg < I64_MIN || arg > I64_MAX) { throwFFIArgError(`Argument ${index} must be ${info.label}`); } sI64(view, offset, arg, true); return; } + if (kind === 'u64') { - if (typeof arg !== 'bigint' || arg < 0n || arg > U64_MAX) { + if (typeof arg === 'number') { + if (!NumberIsSafeInteger(arg) || arg < 0) { + throwFFIArgError(`Argument ${index} must be ${info.label}`); + } + arg = BigInt(arg); + } else if (typeof arg !== 'bigint' || arg < 0n || arg > U64_MAX) { throwFFIArgError(`Argument ${index} must be ${info.label}`); } sU64(view, offset, arg, true); diff --git a/lib/internal/ffi/fast-api.js b/lib/internal/ffi/fast-api.js index 38f4f4e34372..c78960dc3bc0 100644 --- a/lib/internal/ffi/fast-api.js +++ b/lib/internal/ffi/fast-api.js @@ -3,8 +3,10 @@ const { ArrayBufferPrototypeGetDetached, ArrayPrototypeIncludes, + BigInt, DataViewPrototypeGetBuffer, NumberIsInteger, + NumberIsSafeInteger, ObjectDefineProperty, ReflectApply, SafeWeakMap, @@ -86,14 +88,35 @@ function throwFFIArgCountError(expected, 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) { + + let valid; + + if (info.kind === 'number') { + valid = typeof value === 'number' && NumberIsInteger(value) && + value >= info.min && value <= info.max; + } else if (typeof value === 'bigint') { + valid = value >= info.min && value <= info.max; + } else { + valid = typeof value === 'number' && NumberIsSafeInteger(value) && + (info.min < 0n || value >= 0); + } + + if (!valid) { throwFFIArgError(`Argument ${index} must be ${info.label}`); } } +function convertFastIntegerArg(type, value) { + const info = fastIntegerTypeInfo[type]; + if (info !== undefined && + info.kind === 'bigint' && + typeof value === 'number') { + return BigInt(value); + } + + return value; +} + function needsRawPointerConversion(type) { return type === 'buffer' || type === 'arraybuffer'; } @@ -224,6 +247,8 @@ function getFastArgumentIndexes(argumentsTypes) { function convertFastArg(type, value, stringState, index) { validateFastIntegerArg(type, value, index); + value = convertFastIntegerArg(type, value); + return needsPointerConversion(type) ? convertPointerArg(type, value, stringState, index) : value; } @@ -297,7 +322,7 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { } validateFastIntegerArg(t0, a0, 0); validateFastPointerArg(t0, a0, 0); - let arg = a0; + let arg = convertFastIntegerArg(t0, a0); if (needsNullPointerConversion(t0) && (arg === null || arg === undefined)) { arg = 0n; diff --git a/src/ffi/types.cc b/src/ffi/types.cc index bf3e7e675ed6..5a910e63a3e3 100644 --- a/src/ffi/types.cc +++ b/src/ffi/types.cc @@ -648,28 +648,43 @@ Maybe ToFFIArgument(Environment* env, *static_cast(ret) = arg->Uint32Value(context).FromJust(); } else if (type == &ffi_type_sint64) { - if (!arg->IsBigInt()) { - THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be an int64", index); - return {}; - } + if (arg->IsBigInt()) { + bool lossless; + int64_t value = arg.As()->Int64Value(&lossless); + if (!lossless) { + THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be an int64", index); + return {}; + } - bool lossless; - *static_cast(ret) = arg.As()->Int64Value(&lossless); - if (!lossless) { - THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be an int64", index); - return {}; + *static_cast(ret) = value; + } else { + int64_t value; + if (!GetStrictSignedInteger( + arg, -kMaxSafeJsInteger, kMaxSafeJsInteger, &value)) { + THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be an int64", index); + return {}; + } + + *static_cast(ret) = value; } } else if (type == &ffi_type_uint64) { - if (!arg->IsBigInt()) { - THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be a uint64", index); - return {}; - } + if (arg->IsBigInt()) { + bool lossless; + uint64_t value = arg.As()->Uint64Value(&lossless); + if (!lossless) { + THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be a uint64", index); + return {}; + } - bool lossless; - *static_cast(ret) = arg.As()->Uint64Value(&lossless); - if (!lossless) { - THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be a uint64", index); - return {}; + *static_cast(ret) = value; + } else { + uint64_t value; + if (!GetStrictUnsignedInteger(arg, kMaxSafeJsInteger, &value)) { + THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be a uint64", index); + return {}; + } + + *static_cast(ret) = value; } } else if (type == &ffi_type_float) { if (!arg->IsNumber()) { diff --git a/test/ffi/fixture_library/ffi_test_library.c b/test/ffi/fixture_library/ffi_test_library.c index 71c54a49fefe..3e24b7cd3d31 100644 --- a/test/ffi/fixture_library/ffi_test_library.c +++ b/test/ffi/fixture_library/ffi_test_library.c @@ -49,6 +49,30 @@ FFI_EXPORT int32_t identity_i32(int32_t value) { return value; } +FFI_EXPORT int64_t identity_i64(int64_t value) { + return value; +} + +FFI_EXPORT uint64_t identity_u64(uint64_t value) { + return value; +} + +FFI_EXPORT int64_t identity_i64_fallback(void* pointer, + int64_t value, + void (*callback)(void)) { + (void)pointer; + (void)callback; + return value; +} + +FFI_EXPORT uint64_t identity_u64_fallback(void* pointer, + uint64_t value, + void (*callback)(void)) { + (void)pointer; + (void)callback; + return value; +} + FFI_EXPORT char identity_char(char value) { return value; } diff --git a/test/ffi/test-ffi-calls.js b/test/ffi/test-ffi-calls.js index bc3be0072db1..0dbddb244b77 100644 --- a/test/ffi/test-ffi-calls.js +++ b/test/ffi/test-ffi-calls.js @@ -22,6 +22,10 @@ test('ffi calls support integer arithmetic and char semantics', () => { assert.strictEqual(symbols.add_u32(0xFFFFFFFF, 1), 0); assert.strictEqual(symbols.add_i64(20n, 22n), 42n); assert.strictEqual(symbols.add_u64(20n, 22n), 42n); + assert.strictEqual(symbols.add_i64(20, 22), symbols.add_i64(20n, 22n)); + assert.strictEqual(symbols.add_u64(20, 22), symbols.add_u64(20n, 22n)); + assert.strictEqual(symbols.add_i64(-20, 22n), 2n); + assert.strictEqual(symbols.add_u64(20n, 22), 42n); if (symbols.char_is_signed()) { assert.strictEqual(symbols.identity_char(-1), -1); @@ -104,15 +108,18 @@ test('ffi strings and buffers cross the boundary correctly', () => { symbols.free_string(duplicated); const buffer = Buffer.from([1, 2, 3, 4]); - assert.strictEqual(symbols.sum_buffer(buffer, BigInt(buffer.length)), 10n); - symbols.reverse_buffer(buffer, BigInt(buffer.length)); + assert.strictEqual( + symbols.sum_buffer(buffer, buffer.length), + symbols.sum_buffer(buffer, BigInt(buffer.length)), + ); + symbols.reverse_buffer(buffer, buffer.length); assert.deepStrictEqual([...buffer], [4, 3, 2, 1]); const typed = new Uint8Array([5, 6, 7, 8]); - assert.strictEqual(symbols.sum_buffer(typed, BigInt(typed.byteLength)), 26n); + assert.strictEqual(symbols.sum_buffer(typed, typed.byteLength), 26n); const arrayBuffer = new Uint8Array([9, 10, 11, 12]).buffer; - assert.strictEqual(symbols.sum_buffer(arrayBuffer, BigInt(arrayBuffer.byteLength)), 42n); + assert.strictEqual(symbols.sum_buffer(arrayBuffer, arrayBuffer.byteLength), 42n); } finally { lib.close(); } @@ -204,13 +211,20 @@ test('ffi validates invalid arguments', () => { assert.throws(() => symbols.add_i16(40_000, 1), /Argument 0 must be an int16/); assert.throws(() => symbols.add_u16(Number.NaN, 1), /Argument 0 must be a uint16/); assert.throws(() => symbols.add_u16(70_000, 1), /Argument 0 must be a uint16/); - assert.throws(() => symbols.add_i64(1, 2n), /Argument 0 must be an int64/); - assert.throws(() => symbols.add_i64(1.5, 2n), /Argument 0 must be an int64/); + assert.throws(() => symbols.add_i64(1.5, 2), /Argument 0 must be an int64/); + assert.throws(() => symbols.add_i64(Number.NaN, 2), /Argument 0 must be an int64/); + assert.throws(() => symbols.add_i64(Number.POSITIVE_INFINITY, 2), /Argument 0 must be an int64/); + assert.throws(() => symbols.add_i64(Number.NEGATIVE_INFINITY, 2), /Argument 0 must be an int64/); + assert.throws(() => symbols.add_i64(Number.MAX_SAFE_INTEGER + 1, 2), /Argument 0 must be an int64/); + assert.throws(() => symbols.add_i64(Number.MIN_SAFE_INTEGER - 1, 2), /Argument 0 must be an int64/); assert.throws(() => symbols.add_i64(2n ** 63n, 2n), /Argument 0 must be an int64/); assert.throws(() => symbols.add_i64(-(2n ** 63n) - 1n, 2n), /Argument 0 must be an int64/); assert.throws(() => symbols.add_u64('1', 2n), /Argument 0 must be a uint64/); - assert.throws(() => symbols.add_u64(1, 2n), /Argument 0 must be a uint64/); - assert.throws(() => symbols.add_u64(Number.NaN, 2n), /Argument 0 must be a uint64/); + assert.throws(() => symbols.add_u64(-1, 2), /Argument 0 must be a uint64/); + assert.throws(() => symbols.add_u64(1.5, 2), /Argument 0 must be a uint64/); + assert.throws(() => symbols.add_u64(Number.NaN, 2), /Argument 0 must be a uint64/); + assert.throws(() => symbols.add_u64(Number.POSITIVE_INFINITY, 2), /Argument 0 must be a uint64/); + assert.throws(() => symbols.add_u64(Number.MAX_SAFE_INTEGER + 1, 2), /Argument 0 must be a uint64/); assert.throws(() => symbols.add_u64(-1n, 2n), /Argument 0 must be a uint64/); assert.throws(() => symbols.add_u64(2n ** 64n, 2n), /Argument 0 must be a uint64/); assert.throws(() => symbols.identity_pointer(-1n), /Argument 0 must be a non-negative pointer bigint/); diff --git a/test/ffi/test-ffi-fast-integer-validation.js b/test/ffi/test-ffi-fast-integer-validation.js index e6bedfab967c..53a9cd9c79c9 100644 --- a/test/ffi/test-ffi-fast-integer-validation.js +++ b/test/ffi/test-ffi-fast-integer-validation.js @@ -32,9 +32,9 @@ test('fast FFI validates integer argument ranges', () => { function callU32(value) { return functions.add_u32(value, 0); } - function callI64(value) { return functions.add_i64(value, 0n); } + function callI64(value) { return functions.add_i64(value, 0); } - function callU64(value) { return functions.add_u64(value, 0n); } + function callU64(value) { return functions.add_u64(value, 0); } for (const [fn, value] of [ [callI8, 0], @@ -43,8 +43,8 @@ test('fast FFI validates integer argument ranges', () => { [callU16, 0], [callI32, 0], [callU32, 0], - [callI64, 0n], - [callU64, 0n], + [callI64, 0], + [callU64, 0], ]) { optimize(fn, value); } @@ -62,6 +62,24 @@ test('fast FFI validates integer argument ranges', () => { assert.throws(() => callU32(-1), expect); assert.throws(() => callU32(1.5), expect); assert.throws(() => callU32('1'), expect); + assert.strictEqual(callI64(Number.MAX_SAFE_INTEGER), + BigInt(Number.MAX_SAFE_INTEGER)); + assert.strictEqual(callI64(Number.MIN_SAFE_INTEGER), + BigInt(Number.MIN_SAFE_INTEGER)); + assert.strictEqual(callU64(Number.MAX_SAFE_INTEGER), + BigInt(Number.MAX_SAFE_INTEGER)); + assert.strictEqual(callI64((2n ** 63n) - 1n), (2n ** 63n) - 1n); + assert.strictEqual(callU64((2n ** 64n) - 1n), (2n ** 64n) - 1n); + assert.throws(() => callI64(Number.MAX_SAFE_INTEGER + 1), expect); + assert.throws(() => callI64(Number.MIN_SAFE_INTEGER - 1), expect); + assert.throws(() => callI64(1.5), expect); + assert.throws(() => callI64(Number.NaN), expect); + assert.throws(() => callI64(Number.POSITIVE_INFINITY), expect); + assert.throws(() => callU64(-1), expect); + assert.throws(() => callU64(Number.MAX_SAFE_INTEGER + 1), expect); + assert.throws(() => callU64(1.5), expect); + assert.throws(() => callU64(Number.NaN), expect); + assert.throws(() => callU64(Number.POSITIVE_INFINITY), expect); assert.throws(() => callI64(2n ** 63n), expect); assert.throws(() => callU64(2n ** 64n), expect); } finally { @@ -70,6 +88,60 @@ test('fast FFI validates integer argument ranges', () => { } }); +test('fast FFI converts single i64/u64 Number arguments before and after optimization', () => { + const { lib, functions } = ffi.dlopen(libraryPath, { + identity_i64: { return: 'int64', arguments: ['int64'] }, + identity_u64: { return: 'uint64', arguments: ['uint64'] }, + }); + + try { + // The native signature must have one argument to exercise the single-argument wrapper. + function callI64(value) { return functions.identity_i64(value); } + + function callU64(value) { return functions.identity_u64(value); } + + for (const optimized of [false, true]) { + if (optimized) { + optimize(callI64, -42); + optimize(callU64, 42); + } + + for (const value of [0, -0, 42, Number.MAX_SAFE_INTEGER, 0n, 42n]) { + assert.strictEqual(callI64(value), BigInt(value)); + assert.strictEqual(callU64(value), BigInt(value)); + } + for (const value of [-42, Number.MIN_SAFE_INTEGER, + -(2n ** 63n), (2n ** 63n) - 1n]) { + assert.strictEqual(callI64(value), BigInt(value)); + } + assert.strictEqual(callU64((2n ** 64n) - 1n), (2n ** 64n) - 1n); + + const signedError = { + code: 'ERR_INVALID_ARG_VALUE', + message: 'Argument 0 must be an int64', + }; + const unsignedError = { + code: 'ERR_INVALID_ARG_VALUE', + message: 'Argument 0 must be a uint64', + }; + for (const value of [Number.MAX_SAFE_INTEGER + 1, Number.MIN_SAFE_INTEGER - 1, + 1.5, NaN, Infinity, -Infinity, '1', null, undefined, true, {}]) { + assert.throws(() => callI64(value), signedError); + assert.throws(() => callU64(value), unsignedError); + } + for (const value of [-(2n ** 63n) - 1n, 2n ** 63n]) { + assert.throws(() => callI64(value), signedError); + } + for (const value of [-1, -1n, 2n ** 64n]) { + assert.throws(() => callU64(value), unsignedError); + } + } + } finally { + eval('%WaitForBackgroundOptimization()'); + lib.close(); + } +}); + test('fast FFI validates pointer BigInt ranges', () => { const lib = new ffi.DynamicLibrary(libraryPath); try { diff --git a/test/ffi/test-ffi-shared-buffer.js b/test/ffi/test-ffi-shared-buffer.js index a56df8daeabd..81f905bc06e9 100644 --- a/test/ffi/test-ffi-shared-buffer.js +++ b/test/ffi/test-ffi-shared-buffer.js @@ -74,7 +74,7 @@ test('f32/f64 round-trip', () => { } }); -test('i64/u64 BigInt round-trip', () => { +test('i64/u64 Number and BigInt round-trip', () => { const { lib, functions } = ffi.dlopen(libraryPath, { add_i64: { return: 'i64', arguments: ['i64', 'i64'] }, add_u64: { return: 'u64', arguments: ['u64', 'u64'] }, @@ -82,6 +82,10 @@ test('i64/u64 BigInt round-trip', () => { try { assert.strictEqual(functions.add_i64(10n, 20n), 30n); assert.strictEqual(functions.add_u64(10n, 20n), 30n); + assert.strictEqual(functions.add_i64(10, 20), 30n); + assert.strictEqual(functions.add_u64(10, 20), 30n); + assert.strictEqual(functions.add_i64(-10, 20n), 10n); + assert.strictEqual(functions.add_u64(10n, 20), 30n); } finally { lib.close(); } @@ -300,7 +304,7 @@ test('integer boundaries for i8/u8/i16/u16/i32/u32', () => { } }); -test('i64/u64 BigInt boundaries and Number/BigInt type mismatches', () => { +test('i64/u64 Number and BigInt boundaries', () => { const { lib, functions } = ffi.dlopen(libraryPath, { add_i64: { return: 'i64', arguments: ['i64', 'i64'] }, add_u64: { return: 'u64', arguments: ['u64', 'u64'] }, @@ -315,6 +319,9 @@ test('i64/u64 BigInt boundaries and Number/BigInt type mismatches', () => { assert.strictEqual(functions.add_i64(I64_MIN, 0n), I64_MIN); assert.strictEqual(functions.add_u64(U64_MAX, 0n), U64_MAX); assert.strictEqual(functions.add_u64(0n, 0n), 0n); + assert.strictEqual(functions.add_i64(Number.MAX_SAFE_INTEGER, 0), BigInt(Number.MAX_SAFE_INTEGER)); + assert.strictEqual(functions.add_i64(Number.MIN_SAFE_INTEGER, 0), BigInt(Number.MIN_SAFE_INTEGER)); + assert.strictEqual(functions.add_u64(Number.MAX_SAFE_INTEGER, 0), BigInt(Number.MAX_SAFE_INTEGER)); const expect = { code: 'ERR_INVALID_ARG_VALUE' }; assert.throws(() => functions.add_i64(I64_MAX + 1n, 0n), expect); @@ -322,13 +329,95 @@ test('i64/u64 BigInt boundaries and Number/BigInt type mismatches', () => { assert.throws(() => functions.add_u64(U64_MAX + 1n, 0n), expect); assert.throws(() => functions.add_u64(-1n, 0n), expect); - assert.throws(() => functions.add_i64(1, 2n), expect); + assert.throws(() => functions.add_i64(Number.MAX_SAFE_INTEGER + 1, 0), expect); + assert.throws(() => functions.add_i64(Number.MIN_SAFE_INTEGER - 1, 0), expect); + assert.throws(() => functions.add_i64(1.5, 0), expect); + assert.throws(() => functions.add_i64(Number.NaN, 0), expect); + assert.throws(() => functions.add_i64(Number.POSITIVE_INFINITY, 0), expect); + assert.throws(() => functions.add_u64(-1, 0), expect); + assert.throws(() => functions.add_u64(Number.MAX_SAFE_INTEGER + 1, 0), expect); + assert.throws(() => functions.add_u64(1.5, 0), expect); + assert.throws(() => functions.add_u64(Number.NaN, 0), expect); + assert.throws(() => functions.add_u64(Number.POSITIVE_INFINITY, 0), expect); assert.throws(() => functions.add_i64(1n, '2'), expect); } finally { lib.close(); } }); +test('SB wrapper accepts safe Number values for i64/u64', () => { + const { lib, functions } = ffi.dlopen(libraryPath, { + identity_i64_fallback: { return: 'i64', arguments: ['pointer', 'i64', 'function'] }, + identity_u64_fallback: { return: 'u64', arguments: ['pointer', 'u64', 'function'] }, + }); + + try { + // A function argument excludes Fast API; BigInt and null pointers use SB. + for (const value of [0, -0, 42, Number.MAX_SAFE_INTEGER, 0n, 42n]) { + assert.strictEqual(functions.identity_i64_fallback(0n, value, null), BigInt(value)); + assert.strictEqual(functions.identity_u64_fallback(0n, value, null), BigInt(value)); + } + for (const value of [-42, Number.MIN_SAFE_INTEGER, -(2n ** 63n), (2n ** 63n) - 1n]) { + assert.strictEqual(functions.identity_i64_fallback(0n, value, null), BigInt(value)); + } + assert.strictEqual(functions.identity_u64_fallback(0n, (2n ** 64n) - 1n, null), + (2n ** 64n) - 1n); + + const signedError = { code: 'ERR_INVALID_ARG_VALUE' }; + const unsignedError = { code: 'ERR_INVALID_ARG_VALUE' }; + for (const value of [Number.MAX_SAFE_INTEGER + 1, Number.MIN_SAFE_INTEGER - 1, + 1.5, NaN, Infinity, -Infinity, '1', null, undefined, true, {}]) { + assert.throws(() => functions.identity_i64_fallback(0n, value, null), signedError); + assert.throws(() => functions.identity_u64_fallback(0n, value, null), unsignedError); + } + for (const value of [-(2n ** 63n) - 1n, 2n ** 63n]) { + assert.throws(() => functions.identity_i64_fallback(0n, value, null), signedError); + } + for (const value of [-1, -1n, 2n ** 64n]) { + assert.throws(() => functions.identity_u64_fallback(0n, value, null), unsignedError); + } + } finally { + lib.close(); + } +}); + +test('generic conversion accepts safe Number values for i64/u64', () => { + const { lib, functions } = ffi.dlopen(libraryPath, { + identity_i64_fallback: { return: 'i64', arguments: ['pointer', 'i64', 'function'] }, + identity_u64_fallback: { return: 'u64', arguments: ['pointer', 'u64', 'function'] }, + }); + const buffer = Buffer.alloc(1); + + try { + // A Buffer pointer selects generic conversion before validating i64/u64. + for (const value of [0, -0, 42, Number.MAX_SAFE_INTEGER, 0n, 42n]) { + assert.strictEqual(functions.identity_i64_fallback(buffer, value, null), BigInt(value)); + assert.strictEqual(functions.identity_u64_fallback(buffer, value, null), BigInt(value)); + } + for (const value of [-42, Number.MIN_SAFE_INTEGER, -(2n ** 63n), (2n ** 63n) - 1n]) { + assert.strictEqual(functions.identity_i64_fallback(buffer, value, null), BigInt(value)); + } + assert.strictEqual(functions.identity_u64_fallback(buffer, (2n ** 64n) - 1n, null), + (2n ** 64n) - 1n); + + const signedError = { code: 'ERR_INVALID_ARG_VALUE' }; + const unsignedError = { code: 'ERR_INVALID_ARG_VALUE' }; + for (const value of [Number.MAX_SAFE_INTEGER + 1, Number.MIN_SAFE_INTEGER - 1, + 1.5, NaN, Infinity, -Infinity, '1', null, undefined, true, {}]) { + assert.throws(() => functions.identity_i64_fallback(buffer, value, null), signedError); + assert.throws(() => functions.identity_u64_fallback(buffer, value, null), unsignedError); + } + for (const value of [-(2n ** 63n) - 1n, 2n ** 63n]) { + assert.throws(() => functions.identity_i64_fallback(buffer, value, null), signedError); + } + for (const value of [-1, -1n, 2n ** 64n]) { + assert.throws(() => functions.identity_u64_fallback(buffer, value, null), unsignedError); + } + } finally { + lib.close(); + } +}); + test('char type picks signed/unsigned range based on host ABI', () => { const { lib, functions } = ffi.dlopen(libraryPath, { char_is_signed: { return: 'i32', arguments: [] }, From bf23996e405c9c4c3a4773c1ffa3b5759decc99f Mon Sep 17 00:00:00 2001 From: HoonDongKang Date: Mon, 28 Sep 2026 13:31:03 +0900 Subject: [PATCH 2/3] doc: clarify accepted number values for 64-bit FFI argumentents Signed-off-by: HoonDongKang Assisted-by: Codex:Astra-medium --- doc/api/ffi.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 29ff34015a7e..56080b96de28 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -540,7 +540,16 @@ For 8-, 16-, and 32-bit integer types and for floating-point types, pass JavaScript `number` values that match the declared type. For 64-bit integer types (`int64` and `uint64`), pass JavaScript `bigint` -values. +values within the declared type's range or safe integer `number` values. +For `int64`, numbers must be between `Number.MIN_SAFE_INTEGER` and +`Number.MAX_SAFE_INTEGER`, inclusive. For `uint64`, numbers must be between +`0` and `Number.MAX_SAFE_INTEGER`, inclusive. This allows buffer lengths such +as `buffer.byteLength` to be passed without an explicit `BigInt()` conversion. +Use `bigint` for integers outside JavaScript's safe integer range. + +Invalid arguments, including fractional numbers, `NaN`, infinities, and values +outside these ranges, throw `ERR_INVALID_ARG_VALUE`. Return values for 64-bit +integer types are always exposed as `bigint` values. For pointer-like arguments: From b0a3f6d782404bc908d780d686bd238641205072 Mon Sep 17 00:00:00 2001 From: HoonDongKang Date: Mon, 28 Sep 2026 16:52:32 +0900 Subject: [PATCH 3/3] ffi: combine fast integer validation and conversion Validate and convert integer arguments in a single helper to avoid repeated type metadata lookups and conversion checks. Handle safe integer Number inputs for 64-bit arguments separately, while preserving the existing type and range checks for BigInt inputs. Return other argument types unchanged for subsequent pointer conversion. Signed-off-by: HoonDongKang Assisted-by: Codex:Astra-medium --- lib/internal/ffi/fast-api.js | 40 +++++++++++++----------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/lib/internal/ffi/fast-api.js b/lib/internal/ffi/fast-api.js index c78960dc3bc0..531d4d410057 100644 --- a/lib/internal/ffi/fast-api.js +++ b/lib/internal/ffi/fast-api.js @@ -85,34 +85,24 @@ function throwFFIArgCountError(expected, actual) { `Invalid argument count: expected ${expected}, got ${actual}`); } -function validateFastIntegerArg(type, value, index) { +function validateAndConvertFastIntegerArg(type, value, index) { const info = fastIntegerTypeInfo[type]; - if (info === undefined) return; + if (info === undefined) return value; - let valid; - - if (info.kind === 'number') { - valid = typeof value === 'number' && NumberIsInteger(value) && - value >= info.min && value <= info.max; - } else if (typeof value === 'bigint') { - valid = value >= info.min && value <= info.max; - } else { - valid = typeof value === 'number' && NumberIsSafeInteger(value) && - (info.min < 0n || value >= 0); + // The native Fast API expects BigInt for 64-bit integer arguments. + if (info.kind === 'bigint' && typeof value === 'number') { + if (!NumberIsSafeInteger(value) || (info.min === 0n && value < 0)) { + throwFFIArgError(`Argument ${index} must be ${info.label}`); + } + return BigInt(value); } - if (!valid) { + 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 convertFastIntegerArg(type, value) { - const info = fastIntegerTypeInfo[type]; - if (info !== undefined && - info.kind === 'bigint' && - typeof value === 'number') { - return BigInt(value); - } return value; } @@ -246,8 +236,7 @@ function getFastArgumentIndexes(argumentsTypes) { } function convertFastArg(type, value, stringState, index) { - validateFastIntegerArg(type, value, index); - value = convertFastIntegerArg(type, value); + value = validateAndConvertFastIntegerArg(type, value, index); return needsPointerConversion(type) ? convertPointerArg(type, value, stringState, index) : value; @@ -320,9 +309,8 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) { if (arguments.length !== 1) { throwFFIArgCountError(1, arguments.length); } - validateFastIntegerArg(t0, a0, 0); + let arg = validateAndConvertFastIntegerArg(t0, a0, 0); validateFastPointerArg(t0, a0, 0); - let arg = convertFastIntegerArg(t0, a0); if (needsNullPointerConversion(t0) && (arg === null || arg === undefined)) { arg = 0n;