src,lib: refactor unsafe buffer creation to remove zero-fill toggle
authorChALkeR Nikita Skovoroda <chalkerx@gmail.com>
Mon, 6 Apr 2026 14:13:34 +0000 (16:13 +0200)
committerBastien Roucariès <rouca@debian.org>
Mon, 6 Apr 2026 14:18:52 +0000 (16:18 +0200)
This removes the zero-fill toggle mechanism that allowed JavaScript
to control ArrayBuffer initialization via shared memory. Instead,
unsafe buffer creation now uses a dedicated C++ API.

Refs: https://hackerone.com/reports/3405778
Co-Authored-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Co-Authored-By: Joyee Cheung <joyeec9h3@gmail.com>
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
PR-URL: https://github.com/nodejs-private/node-private/pull/759
Backport-PR-URL: https://github.com/nodejs-private/node-private/pull/799
CVE-ID: CVE-2025-55131

origin: backport, https://github.com/nodejs/node/commit/51f4de4b4a52b5b0eb2c63ecbb4126577e05f636

Gbp-Pq: Name CVE-2025-55131.patch

deps/v8/include/v8-array-buffer.h
deps/v8/src/api/api.cc
lib/internal/buffer.js
lib/internal/process/pre_execution.js
src/api/environment.cc
src/node_buffer.cc

index cc5d2d4323000ab4b503c0c2bc745e3f7aa4c00a..bf1df3e7a52042eb4eacb2be3c2352c5bb136991 100644 (file)
@@ -223,6 +223,13 @@ class V8_EXPORT ArrayBuffer : public Object {
    */
   static std::unique_ptr<BackingStore> NewBackingStore(Isolate* isolate,
                                                        size_t byte_length);
+  /**
+   * Returns a new standalone BackingStore with uninitialized memory and
+   * return nullptr on failure.
+   * This variant is for not breaking ABI on Node.js LTS. DO NOT USE.
+   */
+  static std::unique_ptr<BackingStore> NewBackingStoreForNodeLTS(
+      Isolate* isolate, size_t byte_length);
   /**
    * Returns a new standalone BackingStore that takes over the ownership of
    * the given buffer. The destructor of the BackingStore invokes the given
index 3b1a81680b0323b8531cbde1e260fe9948e09ff4..6a2b5010a1bdb22b51023da21e0aafc84b621b15 100644 (file)
@@ -8014,6 +8014,23 @@ std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStore(
       static_cast<v8::BackingStore*>(backing_store.release()));
 }
 
+std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStoreForNodeLTS(
+    Isolate* v8_isolate, size_t byte_length) {
+  i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(v8_isolate);
+  API_RCS_SCOPE(i_isolate, ArrayBuffer, NewBackingStore);
+  CHECK_LE(byte_length, i::JSArrayBuffer::kMaxByteLength);
+  ENTER_V8_NO_SCRIPT_NO_EXCEPTION(i_isolate);
+  std::unique_ptr<i::BackingStoreBase> backing_store =
+      i::BackingStore::Allocate(i_isolate, byte_length,
+                                i::SharedFlag::kNotShared,
+                                i::InitializedFlag::kUninitialized);
+  if (!backing_store) {
+    return nullptr;
+  }
+  return std::unique_ptr<v8::BackingStore>(
+      static_cast<v8::BackingStore*>(backing_store.release()));
+}
+
 std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStore(
     void* data, size_t byte_length, v8::BackingStore::DeleterCallback deleter,
     void* deleter_data) {
index fbe9de249348b3afed077f9730fbf531f1e9a3ec..23df382f14ddf0d76a0a83d73e2ac7d5938525cc 100644 (file)
@@ -30,7 +30,7 @@ const {
   hexWrite,
   ucs2Write,
   utf8Write,
-  getZeroFillToggle,
+  createUnsafeArrayBuffer,
 } = internalBinding('buffer');
 
 const {
@@ -1053,26 +1053,14 @@ function markAsUntransferable(obj) {
   obj[untransferable_object_private_symbol] = true;
 }
 
-// A toggle used to access the zero fill setting of the array buffer allocator
-// in C++.
-// |zeroFill| can be undefined when running inside an isolate where we
-// do not own the ArrayBuffer allocator.  Zero fill is always on in that case.
-let zeroFill = getZeroFillToggle();
 function createUnsafeBuffer(size) {
-  zeroFill[0] = 0;
-  try {
+  if (size <= 64) {
+    // Allocated in heap, doesn't call backing store anyway
+    // This is the same that the old impl did implicitly, but explicit now
     return new FastBuffer(size);
-  } finally {
-    zeroFill[0] = 1;
   }
-}
 
-// The connection between the JS land zero fill toggle and the
-// C++ one in the NodeArrayBufferAllocator gets lost if the toggle
-// is deserialized from the snapshot, because V8 owns the underlying
-// memory of this toggle. This resets the connection.
-function reconnectZeroFillToggle() {
-  zeroFill = getZeroFillToggle();
+  return new FastBuffer(createUnsafeArrayBuffer(size));
 }
 
 module.exports = {
@@ -1082,5 +1070,4 @@ module.exports = {
   createUnsafeBuffer,
   readUInt16BE,
   readUInt32BE,
-  reconnectZeroFillToggle,
 };
index 4795be82e74b6cbcaa40f552ba08ad6cc4a5bdee..f95ab3deba3001525d75397ed18631a293ea1ed0 100644 (file)
@@ -16,7 +16,6 @@ const {
   getOptionValue,
   refreshOptions,
 } = require('internal/options');
-const { reconnectZeroFillToggle } = require('internal/buffer');
 const {
   defineOperation,
   exposeInterface,
@@ -56,7 +55,6 @@ function prepareExecution(options) {
   const { expandArgv1, initializeModules, isMainThread } = options;
 
   refreshRuntimeOptions();
-  reconnectZeroFillToggle();
 
   // Patch the process object and get the resolved main entry point.
   const mainEntry = patchProcessObject(expandArgv1);
index de58a26fde5bae4ad28e926eab42179c8fd716c1..cbe771996c5b8a45cb9995531f466bddb5079156 100644 (file)
@@ -104,8 +104,9 @@ void* NodeArrayBufferAllocator::Allocate(size_t size) {
     ret = allocator_->Allocate(size);
   else
     ret = allocator_->AllocateUninitialized(size);
-  if (LIKELY(ret != nullptr))
+  if (ret != nullptr) [[likely]] {
     total_mem_usage_.fetch_add(size, std::memory_order_relaxed);
+  }
   return ret;
 }
 
index 4bc7336e8d6033fab2a707d169ec73ec7dea8cd3..8d9da8de8d09e33d5a1176090d8828ec23dcf6a3 100644 (file)
@@ -1261,35 +1261,6 @@ void SetBufferPrototype(const FunctionCallbackInfo<Value>& args) {
   env->set_buffer_prototype_object(proto);
 }
 
-void GetZeroFillToggle(const FunctionCallbackInfo<Value>& args) {
-  Environment* env = Environment::GetCurrent(args);
-  NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator();
-  Local<ArrayBuffer> ab;
-  // It can be a nullptr when running inside an isolate where we
-  // do not own the ArrayBuffer allocator.
-  if (allocator == nullptr) {
-    // Create a dummy Uint32Array - the JS land can only toggle the C++ land
-    // setting when the allocator uses our toggle. With this the toggle in JS
-    // land results in no-ops.
-    ab = ArrayBuffer::New(env->isolate(), sizeof(uint32_t));
-  } else {
-    uint32_t* zero_fill_field = allocator->zero_fill_field();
-    std::unique_ptr<BackingStore> backing =
-        ArrayBuffer::NewBackingStore(zero_fill_field,
-                                     sizeof(*zero_fill_field),
-                                     [](void*, size_t, void*) {},
-                                     nullptr);
-    ab = ArrayBuffer::New(env->isolate(), std::move(backing));
-  }
-
-  ab->SetPrivate(
-      env->context(),
-      env->untransferable_object_private_symbol(),
-      True(env->isolate())).Check();
-
-  args.GetReturnValue().Set(Uint32Array::New(ab, 0, 1));
-}
-
 void DetachArrayBuffer(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   if (args[0]->IsArrayBuffer()) {
@@ -1357,6 +1328,54 @@ void CopyArrayBuffer(const FunctionCallbackInfo<Value>& args) {
   memcpy(dest, src, bytes_to_copy);
 }
 
+// Converts a number parameter to size_t suitable for ArrayBuffer sizes
+// Could be larger than uint32_t
+// See v8::internal::TryNumberToSize and v8::internal::NumberToSize
+inline size_t CheckNumberToSize(Local<Value> number) {
+  CHECK(number->IsNumber());
+  double value = number.As<Number>()->Value();
+  // See v8::internal::TryNumberToSize on this (and on < comparison)
+  double maxSize = static_cast<double>(std::numeric_limits<size_t>::max());
+  CHECK(value >= 0 && value < maxSize);
+  size_t size = static_cast<size_t>(value);
+#ifdef V8_ENABLE_SANDBOX
+  CHECK_LE(size, kMaxSafeBufferSizeForSandbox);
+#endif
+  return size;
+}
+
+void CreateUnsafeArrayBuffer(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  if (args.Length() != 1) {
+    env->ThrowRangeError("Invalid array buffer length");
+    return;
+  }
+
+  size_t size = CheckNumberToSize(args[0]);
+
+  Isolate* isolate = env->isolate();
+
+  Local<ArrayBuffer> buf;
+
+  NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator();
+  // 0-length, or zero-fill flag is set, or building snapshot
+  if (size == 0 || per_process::cli_options->zero_fill_all_buffers ||
+      allocator == nullptr) {
+    buf = ArrayBuffer::New(isolate, size);
+  } else {
+    std::unique_ptr<BackingStore> store =
+        ArrayBuffer::NewBackingStoreForNodeLTS(isolate, size);
+    if (!store) {
+      // This slightly differs from the old behavior,
+      // as in v8 that's a RangeError, and this is an Error with code
+      return env->ThrowRangeError("Array buffer allocation failed");
+    }
+    buf = ArrayBuffer::New(isolate, std::move(store));
+  }
+
+  args.GetReturnValue().Set(buf);
+}
+
 void Initialize(Local<Object> target,
                 Local<Value> unused,
                 Local<Context> context,
@@ -1379,6 +1398,8 @@ void Initialize(Local<Object> target,
 
   SetMethod(context, target, "detachArrayBuffer", DetachArrayBuffer);
   SetMethod(context, target, "copyArrayBuffer", CopyArrayBuffer);
+  SetMethodNoSideEffect(
+      context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer);
 
   SetMethod(context, target, "swap16", Swap16);
   SetMethod(context, target, "swap32", Swap32);
@@ -1418,8 +1439,6 @@ void Initialize(Local<Object> target,
   SetMethod(context, target, "hexWrite", StringWrite<HEX>);
   SetMethod(context, target, "ucs2Write", StringWrite<UCS2>);
   SetMethod(context, target, "utf8Write", StringWrite<UTF8>);
-
-  SetMethod(context, target, "getZeroFillToggle", GetZeroFillToggle);
 }
 
 }  // anonymous namespace
@@ -1463,10 +1482,11 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
   registry->Register(StringWrite<HEX>);
   registry->Register(StringWrite<UCS2>);
   registry->Register(StringWrite<UTF8>);
-  registry->Register(GetZeroFillToggle);
 
   registry->Register(DetachArrayBuffer);
   registry->Register(CopyArrayBuffer);
+
+  registry->Register(CreateUnsafeArrayBuffer);
 }
 
 }  // namespace Buffer