Commit e0619bb7e39 for nodejs

commit e0619bb7e39092f191685f156449f8783fe712f7
Author: Trivikram Kamat <trivikr.dev@gmail.com>
Date:   Wed Oct 21 18:42:13 2026 -0700

    ffi: fix handle leak in fast buffer arguments

    node_ffi_fast_buffer_data() runs inside a V8 Fast API call, where no
    HandleScope is open. ArrayBufferView::Buffer() created a Local on
    every optimized call, which accumulated in the caller's scope until
    the current synchronous run ended. Open one HandleScope for the whole
    helper, matching Node's other fast callbacks.

    Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com>
    Assisted-by: claude:opus-5.5
    PR-URL: https://github.com/nodejs/node/pull/66867
    Fixes: https://github.com/nodejs/node/issues/66866
    Reviewed-By: Anna Henningsen <anna@addaleax.net>
    Reviewed-By: Paolo Insogna <paolo@cowtech.it>

diff --git a/src/ffi/fast.cc b/src/ffi/fast.cc
index 4f6606d5025..aaba55e07ba 100644
--- a/src/ffi/fast.cc
+++ b/src/ffi/fast.cc
@@ -245,7 +245,13 @@ extern "C" uintptr_t node_ffi_fast_buffer_data(v8::Local<v8::Value> value,
   // returns zero after throwing, preventing the native target from seeing an
   // invalid pointer value.
   constexpr uintptr_t kInvalidBuffer = std::numeric_limits<uintptr_t>::max();
-  v8::Isolate* isolate = options != nullptr ? options->isolate : nullptr;
+  // The trampoline always passes the FastApiCallbackOptions declared in the
+  // CFunctionInfo. No HandleScope is active during a Fast API call, and both
+  // ArrayBufferView::Buffer() and the error paths below create handles, so
+  // open one for the whole helper. Otherwise every call leaks handles into
+  // the caller's scope.
+  v8::Isolate* isolate = options->isolate;
+  v8::HandleScope scope(isolate);

   // Accept only the memory-backed JS values supported by ToFFIArgument in the
   // native helper. Other pointer conversions, including strings and direct
@@ -254,41 +260,26 @@ extern "C" uintptr_t node_ffi_fast_buffer_data(v8::Local<v8::Value> value,
   if (value->IsArrayBufferView()) {
     v8::Local<v8::ArrayBufferView> view = value.As<v8::ArrayBufferView>();
     if (view->Buffer()->WasDetached()) {
-      if (isolate != nullptr) {
-        // No HandleScope is active during a Fast API call, so open one before
-        // creating the error object.
-        v8::HandleScope scope(isolate);
-        THROW_ERR_INVALID_ARG_VALUE(
-            isolate,
-            "Argument %u is an ArrayBufferView backed by a detached "
-            "ArrayBuffer",
-            index);
-      }
+      THROW_ERR_INVALID_ARG_VALUE(
+          isolate,
+          "Argument %u is an ArrayBufferView backed by a detached "
+          "ArrayBuffer",
+          index);
       return kInvalidBuffer;
     }
     return PointerFromValue(value);
   }
   if (value->IsArrayBuffer()) {
     if (value.As<v8::ArrayBuffer>()->WasDetached()) {
-      if (isolate != nullptr) {
-        // No HandleScope is active during a Fast API call, so open one before
-        // creating the error object.
-        v8::HandleScope scope(isolate);
-        THROW_ERR_INVALID_ARG_VALUE(
-            isolate, "Argument %u is a detached ArrayBuffer", index);
-      }
+      THROW_ERR_INVALID_ARG_VALUE(
+          isolate, "Argument %u is a detached ArrayBuffer", index);
       return kInvalidBuffer;
     }
     return PointerFromValue(value);
   }

-  if (isolate != nullptr) {
-    // No HandleScope is active during a Fast API call, so open one before
-    // creating the error object.
-    v8::HandleScope scope(isolate);
-    THROW_ERR_INVALID_ARG_VALUE(
-        isolate, "Argument %u must be a buffer or an ArrayBuffer", index);
-  }
+  THROW_ERR_INVALID_ARG_VALUE(
+      isolate, "Argument %u must be a buffer or an ArrayBuffer", index);
   return kInvalidBuffer;
 }

diff --git a/test/ffi/test-ffi-fast-buffer.js b/test/ffi/test-ffi-fast-buffer.js
index edf4a297aa4..946b7610249 100644
--- a/test/ffi/test-ffi-fast-buffer.js
+++ b/test/ffi/test-ffi-fast-buffer.js
@@ -6,6 +6,7 @@ common.skipIfFFIMissing();

 const assert = require('node:assert');
 const { test } = require('node:test');
+const { queryObjects } = require('node:v8');
 const { internalBinding } = require('internal/test/binding');
 const {
   kSbSharedBuffer,
@@ -100,6 +101,36 @@ test('optimized pointer arguments reject direct SharedArrayBuffers', () => {
   }
 });

+test('optimized buffer calls do not retain handles', () => {
+  const { lib, functions } = ffi.dlopen(libraryPath, {
+    first_byte: { arguments: ['buffer'], return: 'u8' },
+  });
+  // A leaked handle to the backing store keeps it alive until the current
+  // synchronous run ends, so count live backing stores after the calls.
+  class TrackedArrayBuffer extends ArrayBuffer {}
+
+  function callFirstByte(view) {
+    return functions.first_byte(view);
+  }
+
+  try {
+    const warm = new Uint8Array(1);
+    eval('%PrepareFunctionForOptimization(callFirstByte)');
+    for (let i = 0; i < 1000; i++) callFirstByte(warm);
+    eval('%OptimizeFunctionOnNextCall(callFirstByte)');
+    callFirstByte(warm);
+
+    for (let i = 0; i < 100; i++) {
+      callFirstByte(new Uint8Array(new TrackedArrayBuffer(1)));
+    }
+    assert.strictEqual(
+      queryObjects(TrackedArrayBuffer, { format: 'count' }), 0);
+  } finally {
+    eval('%WaitForBackgroundOptimization()');
+    lib.close();
+  }
+});
+
 test('fast FFI string buffers survive reentrant callbacks', {
   // Bundled libffi callbacks crash on SmartOS.
   skip: common.isSunOS,