Commit 617e082ff20 for nodejs

commit 617e082ff20a486031df2ca117bb5f1cb226df32
Author: Donghoon Kang <d159123@naver.com>
Date:   Sat Oct 3 18:51:13 2026 +0900

    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 <d159123@naver.com>
    Assisted-by: Codex:Astra-medium
    PR-URL: https://github.com/nodejs/node/pull/66197
    Fixes: https://github.com/nodejs/node/issues/66198
    Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
    Reviewed-By: Paolo Insogna <paolo@cowtech.it>

diff --git a/doc/api/ffi.md b/doc/api/ffi.md
index 342acfbec50..ee56074a682 100644
--- a/doc/api/ffi.md
+++ b/doc/api/ffi.md
@@ -548,7 +548,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:

diff --git a/lib/internal/ffi-shared-buffer.js b/lib/internal/ffi-shared-buffer.js
index 13002193cd9..68b39ab62f9 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 38f4f4e3437..531d4d41005 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,
@@ -83,15 +85,26 @@ 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;
+
+  // 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);
+  }
+
   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}`);
   }
+
+  return value;
 }

 function needsRawPointerConversion(type) {
@@ -223,7 +236,8 @@ function getFastArgumentIndexes(argumentsTypes) {
 }

 function convertFastArg(type, value, stringState, index) {
-  validateFastIntegerArg(type, value, index);
+  value = validateAndConvertFastIntegerArg(type, value, index);
+
   return needsPointerConversion(type) ?
     convertPointerArg(type, value, stringState, index) : value;
 }
@@ -295,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 = a0;
       if (needsNullPointerConversion(t0) &&
           (arg === null || arg === undefined)) {
         arg = 0n;
diff --git a/src/ffi/types.cc b/src/ffi/types.cc
index bf3e7e675ed..5a910e63a3e 100644
--- a/src/ffi/types.cc
+++ b/src/ffi/types.cc
@@ -648,28 +648,43 @@ Maybe<FFIArgumentCategory> ToFFIArgument(Environment* env,

     *static_cast<uint32_t*>(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<BigInt>()->Int64Value(&lossless);
+      if (!lossless) {
+        THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be an int64", index);
+        return {};
+      }

-    bool lossless;
-    *static_cast<int64_t*>(ret) = arg.As<BigInt>()->Int64Value(&lossless);
-    if (!lossless) {
-      THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be an int64", index);
-      return {};
+      *static_cast<int64_t*>(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<int64_t*>(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<BigInt>()->Uint64Value(&lossless);
+      if (!lossless) {
+        THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be a uint64", index);
+        return {};
+      }

-    bool lossless;
-    *static_cast<uint64_t*>(ret) = arg.As<BigInt>()->Uint64Value(&lossless);
-    if (!lossless) {
-      THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be a uint64", index);
-      return {};
+      *static_cast<uint64_t*>(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<uint64_t*>(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 71c54a49fef..3e24b7cd3d3 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 bc3be0072db..0dbddb244b7 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 e2994c89523..866f9175221 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 a56df8daeab..81f905bc06e 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: [] },