From 8d87696c9b2275f06b2afc4fbacd13bd949f7ae1 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:24:49 -0700 Subject: [PATCH] ffi: fix use-after-free in PrepareFunction PrepareFunction() looked up the function cache before parsing the signature. Parsing can run user getters, and a getter that calls lib.close() clears the cache, which invalidates the iterator. For a function that was already cached, reading it afterwards used freed memory and crashed the process. Look up the cache after the signature is parsed. If the library was closed during parsing, ResolveSymbol() now throws ERR_FFI_LIBRARY_CLOSED. Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Assisted-by: claude:opus-5.5 --- src/node_ffi.cc | 4 +++- test/ffi/test-ffi-dynamic-library.js | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 732657a368ef..7e8ad60aa4d6 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -144,12 +144,14 @@ Maybe DynamicLibrary::ResolveSymbol(Environment* env, Maybe DynamicLibrary::PrepareFunction( Environment* env, const std::string& name, Local signature) { std::shared_ptr fn; - auto existing = functions_.find(name); FunctionSignature parsed; if (!ParseFunctionSignature(env, name, signature).To(&parsed)) { return {}; } + // Look up the cache only after parsing: the signature's getters run user + // code that may close the library, which clears `functions_`. + auto existing = functions_.find(name); auto [return_type, args, return_type_name, arg_type_names] = std::move(parsed); diff --git a/test/ffi/test-ffi-dynamic-library.js b/test/ffi/test-ffi-dynamic-library.js index 3240f194972c..62cc7e0b4d49 100644 --- a/test/ffi/test-ffi-dynamic-library.js +++ b/test/ffi/test-ffi-dynamic-library.js @@ -290,6 +290,21 @@ test('closed libraries reject subsequent operations', () => { assert.throws(() => lib.getSymbols(), /Library is closed/); }); +test('closing the library from a signature getter of a cached function', () => { + const lib = new ffi.DynamicLibrary(libraryPath); + lib.getFunction('add_i32', fixtureSymbols.add_i32); + + assert.throws(() => { + lib.getFunction('add_i32', { + arguments: ['i32', 'i32'], + get return() { + lib.close(); + return 'i32'; + }, + }); + }, { code: 'ERR_FFI_LIBRARY_CLOSED' }); +}); + test('optimized fast calls reject calls after the library is closed', () => { const { lib, functions } = ffi.dlopen(libraryPath, { multiply_f64: fixtureSymbols.multiply_f64,