diff --git a/common.gypi b/common.gypi index 20acf954bc02d4..55476f5ce2b184 100644 --- a/common.gypi +++ b/common.gypi @@ -38,7 +38,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.10', + 'v8_embedder_string': '-node.11', ##### V8 defaults for Node.js ##### diff --git a/deps/v8/src/builtins/builtins-arraybuffer.cc b/deps/v8/src/builtins/builtins-arraybuffer.cc index bdde40ea3d4042..047cbef4cd3b93 100644 --- a/deps/v8/src/builtins/builtins-arraybuffer.cc +++ b/deps/v8/src/builtins/builtins-arraybuffer.cc @@ -638,7 +638,8 @@ Tagged ArrayBufferTransfer(Isolate* isolate, // 8. If arrayBuffer.[[ArrayBufferDetachKey]] is not undefined, throw a // TypeError exception. - if (!array_buffer->is_detachable()) { + if (!IsUndefined(array_buffer->detach_key()) || + !array_buffer->is_detachable()) { THROW_NEW_ERROR_RETURN_FAILURE( isolate, NewTypeError(MessageTemplate::kDataCloneErrorNonDetachableArrayBuffer)); diff --git a/deps/v8/test/mjsunit/array-buffer-transfer-detach-key.js b/deps/v8/test/mjsunit/array-buffer-transfer-detach-key.js new file mode 100644 index 00000000000000..3c262fa88434c4 --- /dev/null +++ b/deps/v8/test/mjsunit/array-buffer-transfer-detach-key.js @@ -0,0 +1,22 @@ +// Copyright 2026 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Flags: --allow-natives-syntax + +function TestTransferSucceeds() { + const ab = new ArrayBuffer(100); + %ArrayBufferSetDetachKey(ab, undefined); + ab.transfer(); + assertEquals(0, ab.byteLength); // Detached. +} + +function TestTransferFails() { + const ab = new ArrayBuffer(100); + %ArrayBufferSetDetachKey(ab, Symbol()); + assertThrows(() => { ab.transfer(); }, TypeError); + assertEquals(100, ab.byteLength); // Not detached. +} + +TestTransferSucceeds(); +TestTransferFails(); diff --git a/deps/v8/test/unittests/BUILD.gn b/deps/v8/test/unittests/BUILD.gn index fe692b5f9ceeba..2a3a89ceb20e17 100644 --- a/deps/v8/test/unittests/BUILD.gn +++ b/deps/v8/test/unittests/BUILD.gn @@ -263,6 +263,7 @@ v8_source_set("v8_unittests_sources") { "api/remote-object-unittest.cc", "api/resource-constraints-unittest.cc", "api/smi-tagging-unittest.cc", + "api/v8-array-buffer-unittest.cc", "api/v8-array-unittest.cc", "api/v8-maybe-unittest.cc", "api/v8-memory-span-unittest.cc", diff --git a/deps/v8/test/unittests/api/v8-array-buffer-unittest.cc b/deps/v8/test/unittests/api/v8-array-buffer-unittest.cc new file mode 100644 index 00000000000000..4aaec1617f6bd4 --- /dev/null +++ b/deps/v8/test/unittests/api/v8-array-buffer-unittest.cc @@ -0,0 +1,40 @@ +// Copyright 2026 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "include/v8-array-buffer.h" + +#include "test/unittests/test-utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace v8 { +namespace { + +using ArrayBufferTest = TestWithContext; + +TEST_F(ArrayBufferTest, TransferWithDetachKey) { + Local ab = ArrayBuffer::New(isolate(), 1); + Local key = Symbol::New(isolate()); + ab->SetDetachKey(key); + Local global = context()->Global(); + Local property_name = + String::NewFromUtf8Literal(isolate(), "test_ab"); + global->Set(context(), property_name, ab).ToChecked(); + + { + TryCatch try_catch(isolate()); + CHECK(TryRunJS("globalThis.test_ab.transfer()").IsEmpty()); + } + + // Didnot transfer. + EXPECT_EQ(ab->ByteLength(), 1u); + + ab->SetDetachKey(Undefined(isolate())); + RunJS("globalThis.test_ab.transfer()"); + + // Transferred. + EXPECT_EQ(ab->ByteLength(), 0u); +} + +} // namespace +} // namespace v8 diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index 48747a4b5c74ee..32024e972932cd 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -256,6 +256,8 @@ In particular, this makes sense for objects that can be cloned, rather than transferred, and which are used by other objects on the sending side. For example, Node.js marks the `ArrayBuffer`s it uses for its [`Buffer` pool][`Buffer.allocUnsafe()`] with this. +`ArrayBuffer.prototype.transfer()` is disallowed on such array buffer +instances. This operation cannot be undone. diff --git a/lib/buffer.js b/lib/buffer.js index dc189712fda29f..9e34ea411518a3 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -129,6 +129,13 @@ const { createUnsafeBuffer, } = require('internal/buffer'); +const { + namespace: { + addDeserializeCallback, + isBuildingSnapshot, + }, +} = require('internal/v8/startup_snapshot'); + FastBuffer.prototype.constructor = Buffer; Buffer.prototype = FastBuffer.prototype; addBufferPrototypeMethods(Buffer.prototype); @@ -159,6 +166,13 @@ function createPool() { poolOffset = 0; } createPool(); +if (isBuildingSnapshot()) { + addDeserializeCallback(() => { + // TODO(legendecas): ArrayBuffer.[[ArrayBufferDetachKey]] is not been serialized. + // Remove this callback when snapshot serialization supports it. + createPool(); + }); +} function alignPool() { // Ensure aligned slices diff --git a/lib/internal/buffer.js b/lib/internal/buffer.js index 430fbe93ed400b..af714b872e5b38 100644 --- a/lib/internal/buffer.js +++ b/lib/internal/buffer.js @@ -6,6 +6,7 @@ const { Float64Array, MathFloor, Number, + Symbol, Uint8Array, } = primordials; @@ -15,6 +16,8 @@ const { ERR_OUT_OF_RANGE, } = require('internal/errors').codes; const { validateNumber } = require('internal/validators'); +const { isArrayBuffer } = require('util/types'); + const { asciiSlice, base64Slice, @@ -31,6 +34,7 @@ const { ucs2Write, utf8WriteStatic, createUnsafeArrayBuffer, + setDetachKey, } = internalBinding('buffer'); const { @@ -1068,6 +1072,10 @@ function markAsUntransferable(obj) { if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null) return; // This object is a primitive and therefore already untransferable. obj[untransferable_object_private_symbol] = true; + + if (isArrayBuffer(obj)) { + setDetachKey(obj, Symbol('unique_detach_key_for_untransferable_arraybuffer')); + } } // This simply checks if the object is marked as untransferable and doesn't diff --git a/src/node_buffer.cc b/src/node_buffer.cc index 49df0b4284748e..90d07a55d3d080 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -1354,6 +1354,15 @@ static void Atob(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(error_code); } +static void SetDetachKey(const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 2); + CHECK(args[0]->IsArrayBuffer()); + + Local ab = args[0].As(); + Local key = args[1]; + ab->SetDetachKey(key); +} + namespace { std::pair DecomposeBufferToParts(Local buffer) { @@ -1638,6 +1647,8 @@ void Initialize(Local target, "utf8WriteStatic", SlowWriteString, &fast_write_string_utf8); + + SetMethod(context, target, "setDetachKey", SetDetachKey); } } // anonymous namespace @@ -1692,6 +1703,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(Atob); registry->Register(Btoa); + + registry->Register(SetDetachKey); } } // namespace Buffer diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index d69a299625d9f2..56d5ad9ee179ba 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -51,6 +51,7 @@ expected.beforePreExec = new Set([ 'NativeModule events', 'Internal Binding buffer', 'Internal Binding string_decoder', + 'NativeModule util/types', 'NativeModule internal/buffer', 'NativeModule buffer', 'Internal Binding messaging', diff --git a/test/parallel/test-buffer-pool-untransferable.js b/test/parallel/test-buffer-pool-untransferable.js index 596bb6b6c91422..bf851625bf83e8 100644 --- a/test/parallel/test-buffer-pool-untransferable.js +++ b/test/parallel/test-buffer-pool-untransferable.js @@ -21,3 +21,9 @@ assert.throws(() => port1.postMessage(a, [ a.buffer ]), { // Verify that the pool ArrayBuffer has not actually been transferred: assert.strictEqual(a.buffer, b.buffer); assert.strictEqual(a.length, length); + +// Verify that ArrayBuffer.prototype.transfer() also throws. +assert.throws(() => a.buffer.transfer(), { + name: 'TypeError', +}); +assert.strictEqual(a.buffer, b.buffer);