You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ffi: registerCallback(callback, signature) silently creates a void () callback #66996
import{dlopen,toBuffer}from'node:ffi';const{ lib,functions: { qsort }}=dlopen(null,{qsort: {arguments: ['buffer','u64','u64','pointer'],return: 'void'},});constsignature={arguments: ['pointer','pointer'],return: 'i32'};letreceived;functioncompare(a,b){received=[typeofa,typeofb];if(typeofa!=='bigint')return-1;// never reaches native codereturntoBuffer(a,4,false).readInt32LE()-toBuffer(b,4,false).readInt32LE();}functionsort(callback){constdata=newInt32Array([5,3,9,1,7]);qsort(data,5n,4n,callback);return`[${data}], compare received ${received}`;}console.log('(signature, fn):',sort(lib.registerCallback(signature,compare)));console.log('(fn, signature):',sort(lib.registerCallback(compare,signature)));lib.close();
How often does it reproduce? Is there a required condition?
Always
What is the expected behavior? Why is that the expected behavior?
library.registerCallback(compare, signature) should throw ERR_INVALID_ARG_TYPE. The documented order is registerCallback([signature,] callback), and the signature argument is validated when it comes first.
What do you see instead?
(signature, fn): [1,3,5,7,9], compare received bigint,bigint(fn, signature): [5,3,9,1,7], compare received undefined,undefined
The reversed call returns a valid pointer for a void () closure. Native code calls it with its real signature. The JS function receives no arguments, its return value is dropped, and the caller reads a return register that libffi never wrote. That was 0 on macOS arm64, but the ABI doesn't define it.
Activity