Commit 24b9ca9ecf2 for nodejs

commit 24b9ca9ecf2f2c04ddc01fca1ced53c369f71b9b
Author: Christian Aurich Zanettini Martins <christian.aurichzm@gmail.com>
Date:   Thu Sep 24 09:04:14 2026 -0300

    ffi: avoid memcpy() with a null pointer

    Zero-length FFI memory operations accept a null pointer, but
    ToArrayBuffer() and ExportBytes() call memcpy() unconditionally.
    Passing a null pointer to memcpy() is undefined behavior even when
    the size is zero.

    Skip the copy when the length is zero, matching what Buffer::Copy()
    already does for the same case.

    An isolated reproduction compiled with -fsanitize=undefined reports
    "null pointer passed as argument 1, which is declared to never be
    null". The added test covers the reachable zero-length paths, but it
    passes without the fix: libc does not fault on memcpy(NULL, NULL, 0),
    and --enable-ubsan does not set -fno-sanitize-recover, so UBSan
    reports the call without failing the process.

    Signed-off-by: Christian Aurich Zanettini Martins <christian.aurichzm@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66200
    Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
    Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
    Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>

diff --git a/src/ffi/data.cc b/src/ffi/data.cc
index dbeba94cd1b..ad92caf716e 100644
--- a/src/ffi/data.cc
+++ b/src/ffi/data.cc
@@ -662,7 +662,7 @@ void ToArrayBuffer(const FunctionCallbackInfo<Value>& args) {
   if (copy) {
     std::unique_ptr<BackingStore> store =
         ArrayBuffer::NewBackingStore(isolate, len);
-    memcpy(store->Data(), reinterpret_cast<void*>(ptr), len);
+    if (len > 0) memcpy(store->Data(), reinterpret_cast<void*>(ptr), len);
     ab = ArrayBuffer::New(isolate, std::move(store));
   } else {
     std::unique_ptr<BackingStore> store = ArrayBuffer::NewBackingStore(
@@ -740,7 +740,9 @@ void ExportBytes(const FunctionCallbackInfo<Value>& args) {
     return;
   }

-  std::memcpy(reinterpret_cast<void*>(ptr), view.data(), view.length());
+  if (view.length() > 0) {
+    std::memcpy(reinterpret_cast<void*>(ptr), view.data(), view.length());
+  }
 }

 void GetRawPointer(const FunctionCallbackInfo<Value>& args) {
diff --git a/test/ffi/test-ffi-memory.js b/test/ffi/test-ffi-memory.js
index 53fe3928029..3ed1dbcb117 100644
--- a/test/ffi/test-ffi-memory.js
+++ b/test/ffi/test-ffi-memory.js
@@ -243,6 +243,14 @@ test('ffi toString returns null for a null pointer', () => {
   assert.strictEqual(ffi.toString(0n), null);
 });

+test('ffi accepts zero-length copies through a null pointer', () => {
+  assert.strictEqual(ffi.toBuffer(0n, 0).length, 0);
+  assert.strictEqual(ffi.toArrayBuffer(0n, 0).byteLength, 0);
+  assert.strictEqual(ffi.exportBuffer(Buffer.alloc(0), 0n, 0), undefined);
+  assert.strictEqual(ffi.exportArrayBuffer(new ArrayBuffer(0), 0n, 0), undefined);
+  assert.strictEqual(ffi.exportArrayBufferView(new Uint8Array(0), 0n, 0), undefined);
+});
+
 test('ffi validates memory access arguments', () => {
   withAllocations(common.mustCall((alloc) => {
     const ptr = alloc(8);