Skip to content

ffi: getFunction() evaluates signature getters twice, causing native/JavaScript ABI mismatch #64558

Description

@trivikr

Version

main

Platform

macOS 26.5.2

Subsystem

ffi

What steps will reproduce the bug?

import { DynamicLibrary, suffix } from 'node:ffi';

// Use Node.js's prebuilt FFI test fixture, which exports sum_8_i32().
const libraryPath =
  `../node/test/ffi/fixture_library/build/Release/ffi_test_library.${suffix}`;

const lib = new DynamicLibrary(libraryPath);
let reads = 0;

const signature = {
  return: 'i32',

  get arguments() {
    const args = ++reads === 1 ?
      Array(8).fill('i32') :
      ['i32'];

    console.log(`read ${reads}: returning ${args.length} arguments`);
    console.log(
      new Error().stack.split('\n').slice(2, 4).join('\n'),
    );

    return args;
  },
};

const fn = lib.getFunction('sum_8_i32', signature);

console.log(`JavaScript wrapper arity: ${fn.length}`);
console.log(`native sum_8_i32 result: ${fn(1)}`);

lib.close();

How often does it reproduce? Is there a required condition?

Always

What is the expected behavior? Why is that the expected behavior?

read 1: returning 8 arguments
    at DynamicLibrary.getFunction (node:ffi:105:15)
    at file:///Users/trivikram/workspace/test-repro/repro.js:27:16
JavaScript wrapper arity: 8
node:internal/ffi-shared-buffer:110
  const err = new TypeError(msg);
              ^

TypeError: Invalid argument count: expected 8, got 1
    at throwFFIArgError (node:internal/ffi-shared-buffer:110:15)
    at throwFFIArgCountError (node:internal/ffi-shared-buffer:116:3)
    at sum_8_i32 (node:internal/ffi-shared-buffer:515:7)
    at file:///Users/trivikram/workspace/test-repro/repro.js:30:41
    at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
    at async node:internal/modules/esm/loader:650:26
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
  code: 'ERR_INVALID_ARG_VALUE'
}

Node should snapshot the signature once, producing an eight-argument wrapper that rejects the one-argument call.

For clean expected output, call the function with eight arguments:

console.log(
  `native sum_8_i32 result: ${fn(1, 2, 3, 4, 5, 6, 7, 8)}`,
);

which will output the following instead of error

native sum_8_i32 result: 36

What do you see instead?

read 1: returning 8 arguments
    at DynamicLibrary.getFunction (node:ffi:105:15)
    at file:///Users/trivikram/workspace/test-repro/repro.js:27:16
read 2: returning 1 arguments
    at DynamicLibrary.getFunction (node:ffi:106:41)
    at file:///Users/trivikram/workspace/test-repro/repro.js:27:16
JavaScript wrapper arity: 1
native sum_8_i32 result: 1

The signature getter is evaluated twice, giving the native call interface eight arguments but the JavaScript wrapper only one, so an unsafe native call succeeds.

Additional information

No response

Metadata

Metadata

Assignees

Labels

ffiIssues and PRs related to experimental Foreign Function Interface support.

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions