diff --git a/lib/internal/child_process/serialization.js b/lib/internal/child_process/serialization.js index 6078570a26fc7f..f32af96e8b57b7 100644 --- a/lib/internal/child_process/serialization.js +++ b/lib/internal/child_process/serialization.js @@ -10,9 +10,7 @@ const { } = primordials; const { Buffer } = require('buffer'); const { StringDecoder } = require('string_decoder'); -const v8 = require('v8'); -const { isArrayBufferView } = require('internal/util/types'); -const assert = require('internal/assert'); +const { serialize, deserialize } = internalBinding('ipc_serdes'); const { streamBaseState, kLastWriteWasAsync } = internalBinding('stream_wrap'); const kMessageBuffer = Symbol('kMessageBuffer'); @@ -20,33 +18,6 @@ const kMessageBufferSize = Symbol('kMessageBufferSize'); const kJSONBuffer = Symbol('kJSONBuffer'); const kStringDecoder = Symbol('kStringDecoder'); -// Extend V8's serializer APIs to give more JSON-like behaviour in -// some cases; in particular, for native objects this serializes them the same -// way that JSON does rather than throwing an exception. -const kArrayBufferViewTag = 0; -const kNotArrayBufferViewTag = 1; -class ChildProcessSerializer extends v8.DefaultSerializer { - _writeHostObject(object) { - if (isArrayBufferView(object)) { - this.writeUint32(kArrayBufferViewTag); - return super._writeHostObject(object); - } - this.writeUint32(kNotArrayBufferViewTag); - this.writeValue({ ...object }); - } -} - -class ChildProcessDeserializer extends v8.DefaultDeserializer { - _readHostObject() { - const tag = this.readUint32(); - if (tag === kArrayBufferViewTag) - return super._readHostObject(); - - assert(tag === kNotArrayBufferViewTag); - return this.readValue(); - } -} - // Messages are parsed in either of the following formats: // - Newline-delimited JSON, or // - V8-serialized buffers, prefixed with their length as a big endian uint32 @@ -90,38 +61,22 @@ const advanced = { channel[kMessageBufferSize], ); - const deserializer = new ChildProcessDeserializer( - TypedArrayPrototypeSubarray(concatenatedBuffer, 4, fullMessageSize), - ); + const serializedMessage = + TypedArrayPrototypeSubarray(concatenatedBuffer, 4, fullMessageSize); messageBufferHead = TypedArrayPrototypeSubarray(concatenatedBuffer, fullMessageSize); channel[kMessageBufferSize] = messageBufferHead.length; channel[kMessageBuffer] = channel[kMessageBufferSize] !== 0 ? [messageBufferHead] : []; - deserializer.readHeader(); - yield deserializer.readValue(); + yield deserialize(serializedMessage); } channel.buffering = channel[kMessageBufferSize] > 0; }, writeChannelMessage(channel, req, message, handle) { - const ser = new ChildProcessSerializer(); - // Add 4 bytes, to later populate with message length - ser.writeRawBytes(Buffer.allocUnsafe(4)); - ser.writeHeader(); - ser.writeValue(message); - - const serializedMessage = ser.releaseBuffer(); - const serializedMessageLength = serializedMessage.length - 4; - - serializedMessage.set([ - serializedMessageLength >> 24 & 0xFF, - serializedMessageLength >> 16 & 0xFF, - serializedMessageLength >> 8 & 0xFF, - serializedMessageLength & 0xFF, - ], 0); + const serializedMessage = serialize(message); const result = channel.writeBuffer(req, serializedMessage, handle); diff --git a/node.gyp b/node.gyp index 6243c74bc1db04..47956a8b22640e 100644 --- a/node.gyp +++ b/node.gyp @@ -136,6 +136,7 @@ 'src/node_http_parser.cc', 'src/node_http2.cc', 'src/node_i18n.cc', + 'src/node_ipc_serdes.cc', 'src/node_locks.cc', 'src/node_main_instance.cc', 'src/node_messaging.cc', diff --git a/src/node_binding.cc b/src/node_binding.cc index 6ba22f5519b4c4..044eadd0eafcda 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -59,6 +59,7 @@ V(http_parser) \ V(inspector) \ V(internal_only_v8) \ + V(ipc_serdes) \ V(js_stream) \ V(js_udp_wrap) \ V(locks) \ diff --git a/src/node_external_reference.h b/src/node_external_reference.h index 04b4a45a0ef2cc..1e987ce2d4f314 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -84,6 +84,7 @@ class ExternalReferenceRegistry { V(heap_utils) \ V(http_parser) \ V(internal_only_v8) \ + V(ipc_serdes) \ V(locks) \ V(messaging) \ V(mksnapshot) \ diff --git a/src/node_ipc_serdes.cc b/src/node_ipc_serdes.cc new file mode 100644 index 00000000000000..0f6b84e25b869e --- /dev/null +++ b/src/node_ipc_serdes.cc @@ -0,0 +1,384 @@ +#include "env-inl.h" +#include "node_buffer.h" +#include "node_errors.h" +#include "node_external_reference.h" +#include "node_internals.h" +#include "util-inl.h" + +#include +#include + +// Native implementation of the `advanced` child_process IPC serialization +// codec previously implemented in lib/internal/child_process/serialization.js +// (the ChildProcessSerializer / ChildProcessDeserializer classes). The wire +// format is preserved byte-for-byte: +// [4-byte big-endian payload length][V8 ValueSerializer payload] +// ArrayBufferViews are treated as host objects (matching v8.DefaultSerializer) +// and tagged so that Node Buffers round-trip as Buffers rather than plain +// Uint8Arrays. + +namespace node { + +using v8::ArrayBuffer; +using v8::ArrayBufferView; +using v8::BackingStore; +using v8::BigInt64Array; +using v8::BigUint64Array; +using v8::Context; +using v8::DataView; +using v8::Exception; +using v8::Float16Array; +using v8::Float32Array; +using v8::Float64Array; +using v8::FunctionCallbackInfo; +using v8::Int16Array; +using v8::Int32Array; +using v8::Int8Array; +using v8::Isolate; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Name; +using v8::Nothing; +using v8::Object; +using v8::String; +using v8::Uint16Array; +using v8::Uint32Array; +using v8::Uint8Array; +using v8::Uint8ClampedArray; +using v8::Value; +using v8::ValueDeserializer; +using v8::ValueSerializer; + +namespace ipc_serdes { + +// Tags written before each host object, matching serialization.js. +static constexpr uint32_t kArrayBufferViewTag = 0; +static constexpr uint32_t kNotArrayBufferViewTag = 1; + +// ArrayBufferView type indices, matching arrayBufferViewTypeToIndex() in +// lib/v8.js. Index 10 is reserved for Node's Buffer (FastBuffer). +static constexpr uint32_t kInvalidViewIndex = 0xFFFFFFFF; +static constexpr uint32_t kBufferIndex = 10; + +static uint32_t GetViewTypeIndex(Local view) { + // Mirrors arrayBufferViewTypeToIndex() in lib/v8.js, classifying the view by + // its class tag. Node Buffers are detected separately by the caller via the + // `value.constructor === Buffer` check, matching DefaultSerializer. + if (view->IsInt8Array()) return 0; + if (view->IsUint8Array()) return 1; + if (view->IsUint8ClampedArray()) return 2; + if (view->IsInt16Array()) return 3; + if (view->IsUint16Array()) return 4; + if (view->IsInt32Array()) return 5; + if (view->IsUint32Array()) return 6; + if (view->IsFloat32Array()) return 7; + if (view->IsFloat64Array()) return 8; + if (view->IsDataView()) return 9; + if (view->IsBigInt64Array()) return 11; + if (view->IsBigUint64Array()) return 12; + if (view->IsFloat16Array()) return 13; + return kInvalidViewIndex; +} + +static size_t BytesPerElement(uint32_t type_index) { + switch (type_index) { + case 3: // Int16Array + case 4: // Uint16Array + case 13: // Float16Array + return 2; + case 5: // Int32Array + case 6: // Uint32Array + case 7: // Float32Array + return 4; + case 8: // Float64Array + case 11: // BigInt64Array + case 12: // BigUint64Array + return 8; + default: // Int8/Uint8/Uint8Clamped/DataView/Buffer + return 1; + } +} + +static MaybeLocal MakeView(Environment* env, + uint32_t type_index, + Local ab, + size_t byte_offset, + size_t byte_length) { + const size_t length = byte_length / BytesPerElement(type_index); + Local result; + switch (type_index) { + case 0: + result = Int8Array::New(ab, byte_offset, length); + break; + case 1: + result = Uint8Array::New(ab, byte_offset, length); + break; + case 2: + result = Uint8ClampedArray::New(ab, byte_offset, length); + break; + case 3: + result = Int16Array::New(ab, byte_offset, length); + break; + case 4: + result = Uint16Array::New(ab, byte_offset, length); + break; + case 5: + result = Int32Array::New(ab, byte_offset, length); + break; + case 6: + result = Uint32Array::New(ab, byte_offset, length); + break; + case 7: + result = Float32Array::New(ab, byte_offset, length); + break; + case 8: + result = Float64Array::New(ab, byte_offset, length); + break; + case 9: + result = DataView::New(ab, byte_offset, byte_length); + break; + case kBufferIndex: { + Local buf; + if (!Buffer::New(env, ab, byte_offset, byte_length).ToLocal(&buf)) { + return {}; + } + result = buf; + break; + } + case 11: + result = BigInt64Array::New(ab, byte_offset, length); + break; + case 12: + result = BigUint64Array::New(ab, byte_offset, length); + break; + case 13: + result = Float16Array::New(ab, byte_offset, length); + break; + default: + THROW_ERR_INVALID_STATE(env, "Invalid host object type index"); + return {}; + } + return result; +} + +class IPCSerializerDelegate : public ValueSerializer::Delegate { + public: + explicit IPCSerializerDelegate(Environment* env) : env_(env) {} + + void set_serializer(ValueSerializer* serializer) { serializer_ = serializer; } + + void ThrowDataCloneError(Local message) override { + env_->isolate()->ThrowException(Exception::Error(message)); + } + + Maybe WriteHostObject(Isolate* isolate, Local object) override { + if (object->IsArrayBufferView()) { + serializer_->WriteUint32(kArrayBufferViewTag); + + // Matches v8.js DefaultSerializer._writeHostObject: a Node Buffer is + // identified by `value.constructor === Buffer` (not by its prototype), + // so that reassigning `.constructor` changes classification exactly as + // it did in the JavaScript codec. + Local context = env_->context(); + Local constructor_key = + FIXED_ONE_BYTE_STRING(isolate, "constructor"); + Local view_constructor; + Local buffer_constructor; + if (!object->Get(context, constructor_key).ToLocal(&view_constructor) || + !env_->buffer_prototype_object() + ->Get(context, constructor_key) + .ToLocal(&buffer_constructor)) { + return Nothing(); + } + uint32_t type_index; + if (view_constructor->StrictEquals(buffer_constructor)) { + type_index = kBufferIndex; + } else { + type_index = GetViewTypeIndex(object); + if (type_index == kInvalidViewIndex) { + THROW_ERR_INVALID_STATE(env_, "Unserializable host object"); + return Nothing(); + } + } + ArrayBufferViewContents contents(object); + serializer_->WriteUint32(type_index); + serializer_->WriteUint32(static_cast(contents.length())); + serializer_->WriteRawBytes(contents.data(), contents.length()); + return Just(true); + } + + // Non-view host object: serialize a shallow copy of its own enumerable + // properties, matching `writeValue({ ...object })` in serialization.js. + // Use CreateDataProperty (not Set) so inherited setters are not invoked, + // exactly as the object-spread did. + serializer_->WriteUint32(kNotArrayBufferViewTag); + Local context = env_->context(); + Local names; + if (!object->GetOwnPropertyNames(context).ToLocal(&names)) { + return Nothing(); + } + Local copy = Object::New(isolate); + const uint32_t len = names->Length(); + for (uint32_t i = 0; i < len; i++) { + Local key; + if (!names->Get(context, i).ToLocal(&key)) return Nothing(); + Local val; + if (!object->Get(context, key).ToLocal(&val)) return Nothing(); + if (copy->CreateDataProperty(context, key.As(), val).IsNothing()) + return Nothing(); + } + return serializer_->WriteValue(context, copy); + } + + private: + Environment* env_; + ValueSerializer* serializer_ = nullptr; +}; + +class IPCDeserializerDelegate : public ValueDeserializer::Delegate { + public: + IPCDeserializerDelegate(Environment* env, Local ab) + : env_(env), ab_(ab) {} + + void set_deserializer(ValueDeserializer* deserializer) { + deserializer_ = deserializer; + } + + MaybeLocal ReadHostObject(Isolate* isolate) override { + uint32_t tag; + if (!deserializer_->ReadUint32(&tag)) return {}; + + if (tag == kNotArrayBufferViewTag) { + Local value; + if (!deserializer_->ReadValue(env_->context()).ToLocal(&value)) { + return {}; + } + if (!value->IsObject()) { + THROW_ERR_INVALID_STATE(env_, "Host object must be an object"); + return {}; + } + return value.As(); + } + + // Only the two tags written by WriteHostObject are valid. Reject anything + // else, matching `assert(tag === kNotArrayBufferViewTag)` in the JS codec. + if (tag != kArrayBufferViewTag) { + THROW_ERR_INVALID_STATE(env_, "Invalid host object tag"); + return {}; + } + + uint32_t type_index; + uint32_t byte_length; + if (!deserializer_->ReadUint32(&type_index) || + !deserializer_->ReadUint32(&byte_length)) { + return {}; + } + const void* data; + if (!deserializer_->ReadRawBytes(byte_length, &data)) return {}; + + const size_t bytes_per_element = BytesPerElement(type_index); + const size_t offset_in_ab = static_cast(data) - + static_cast(ab_->Data()); + + // Reuse the backing ArrayBuffer when the data is suitably aligned, + // otherwise copy into a fresh aligned buffer. Mirrors _readHostObject() + // in lib/v8.js. + if (offset_in_ab % bytes_per_element == 0) { + return MakeView(env_, type_index, ab_, offset_in_ab, byte_length); + } + std::shared_ptr store = + ArrayBuffer::NewBackingStore(isolate, byte_length); + memcpy(store->Data(), data, byte_length); + Local copy = ArrayBuffer::New(isolate, std::move(store)); + return MakeView(env_, type_index, copy, 0, byte_length); + } + + private: + Environment* env_; + Local ab_; + ValueDeserializer* deserializer_ = nullptr; +}; + +static void Serialize(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + Local context = env->context(); + + IPCSerializerDelegate delegate(env); + ValueSerializer serializer(isolate, &delegate); + delegate.set_serializer(&serializer); + serializer.SetTreatArrayBufferViewsAsHostObjects(true); + + // Reserve 4 bytes for the big-endian payload length, then write the + // standard V8 header and the value, matching serialization.js. + const uint8_t length_placeholder[4] = {0, 0, 0, 0}; + serializer.WriteRawBytes(length_placeholder, sizeof(length_placeholder)); + serializer.WriteHeader(); + + bool wrote; + if (!serializer.WriteValue(context, args[0]).To(&wrote) || !wrote) { + // A pending exception was set by the delegate or V8. + return; + } + + std::pair result = serializer.Release(); + uint8_t* buf = result.first; + const size_t size = result.second; + const uint32_t payload_length = static_cast(size - 4); + buf[0] = (payload_length >> 24) & 0xFF; + buf[1] = (payload_length >> 16) & 0xFF; + buf[2] = (payload_length >> 8) & 0xFF; + buf[3] = payload_length & 0xFF; + + Local out; + if (Buffer::New(env, reinterpret_cast(buf), size).ToLocal(&out)) { + args.GetReturnValue().Set(out); + } +} + +static void Deserialize(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + Local context = env->context(); + + CHECK(args[0]->IsArrayBufferView()); + Local view = args[0].As(); + Local ab = view->Buffer(); + const size_t byte_offset = view->ByteOffset(); + const size_t byte_length = view->ByteLength(); + const uint8_t* data = static_cast(ab->Data()) + byte_offset; + + IPCDeserializerDelegate delegate(env, ab); + ValueDeserializer deserializer(isolate, data, byte_length, &delegate); + delegate.set_deserializer(&deserializer); + + bool read_header; + if (!deserializer.ReadHeader(context).To(&read_header)) return; + + Local value; + if (deserializer.ReadValue(context).ToLocal(&value)) { + args.GetReturnValue().Set(value); + } +} + +static void Initialize(Local target, + Local unused, + Local context, + void* priv) { + SetMethod(context, target, "serialize", Serialize); + SetMethod(context, target, "deserialize", Deserialize); +} + +static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(Serialize); + registry->Register(Deserialize); +} + +} // namespace ipc_serdes +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL(ipc_serdes, node::ipc_serdes::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE(ipc_serdes, + node::ipc_serdes::RegisterExternalReferences) diff --git a/test/cctest/test_node_ipc_serdes.cc b/test/cctest/test_node_ipc_serdes.cc new file mode 100644 index 00000000000000..68389814743d04 --- /dev/null +++ b/test/cctest/test_node_ipc_serdes.cc @@ -0,0 +1,203 @@ +#include "node.h" +#include "node_binding.h" +#include "node_buffer.h" +#include "node_test_fixture.h" + +#include + +using node::Environment; + +namespace { + +// Bootstraps the environment (so Buffer and friends are initialized) and +// returns the `ipc_serdes` internal binding object. +v8::Local GetIpcSerdesBinding(Environment* env, + v8::Local context) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + node::LoadEnvironment(env, ""); + v8::Local get_internal_binding = + v8::Function::New(context, node::binding::GetInternalBinding) + .ToLocalChecked(); + v8::Local binding_name = + v8::String::NewFromUtf8Literal(isolate, "ipc_serdes"); + v8::Local binding = + get_internal_binding + ->Call(context, v8::Undefined(isolate), 1, &binding_name) + .ToLocalChecked(); + return binding.As(); +} + +v8::Local GetFunction(v8::Local context, + v8::Local object, + const char* name) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::Local value = + object + ->Get(context, + v8::String::NewFromUtf8(isolate, name).ToLocalChecked()) + .ToLocalChecked(); + return value.As(); +} + +// Serializes `input`, validates the 4-byte big-endian length prefix, strips it +// (as parseChannelMessages does), and returns the deserialized value. +v8::Local RoundTrip(v8::Local context, + v8::Local serialize, + v8::Local deserialize, + v8::Local input) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::Local serialized = + serialize->Call(context, v8::Undefined(isolate), 1, &input) + .ToLocalChecked(); + EXPECT_TRUE(serialized->IsUint8Array()); + v8::Local buf = serialized.As(); + EXPECT_GE(buf->ByteLength(), static_cast(4)); + + node::ArrayBufferViewContents bytes(serialized); + const uint32_t payload_length = + (static_cast(bytes.data()[0]) << 24) | + (static_cast(bytes.data()[1]) << 16) | + (static_cast(bytes.data()[2]) << 8) | + static_cast(bytes.data()[3]); + EXPECT_EQ(payload_length, buf->ByteLength() - 4); + + v8::Local payload = v8::Uint8Array::New( + buf->Buffer(), buf->ByteOffset() + 4, buf->ByteLength() - 4); + return deserialize->Call(context, v8::Undefined(isolate), 1, &payload) + .ToLocalChecked(); +} + +class IPCSerdesTest : public EnvironmentTestFixture {}; + +TEST_F(IPCSerdesTest, RoundTripsPrimitives) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env test_env{handle_scope, argv}; + v8::Local context = isolate_->GetCurrentContext(); + + v8::Local binding = GetIpcSerdesBinding(*test_env, context); + v8::Local serialize = + GetFunction(context, binding, "serialize"); + v8::Local deserialize = + GetFunction(context, binding, "deserialize"); + + v8::Local inputs[] = { + v8::Number::New(isolate_, 42), + v8::Number::New(isolate_, -3.14), + v8::String::NewFromUtf8Literal(isolate_, "hello world"), + v8::Boolean::New(isolate_, true), + v8::Null(isolate_), + }; + + for (v8::Local input : inputs) { + v8::Local result = + RoundTrip(context, serialize, deserialize, input); + EXPECT_TRUE(result->StrictEquals(input)); + } +} + +TEST_F(IPCSerdesTest, RoundTripsObject) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env test_env{handle_scope, argv}; + v8::Local context = isolate_->GetCurrentContext(); + + v8::Local binding = GetIpcSerdesBinding(*test_env, context); + v8::Local serialize = + GetFunction(context, binding, "serialize"); + v8::Local deserialize = + GetFunction(context, binding, "deserialize"); + + v8::Local input = v8::Object::New(isolate_); + input + ->Set(context, + v8::String::NewFromUtf8Literal(isolate_, "foo"), + v8::Number::New(isolate_, 42)) + .Check(); + input + ->Set(context, + v8::String::NewFromUtf8Literal(isolate_, "bar"), + v8::String::NewFromUtf8Literal(isolate_, "baz")) + .Check(); + + v8::Local result = + RoundTrip(context, serialize, deserialize, input); + ASSERT_TRUE(result->IsObject()); + v8::Local obj = result.As(); + + v8::Local foo = + obj->Get(context, v8::String::NewFromUtf8Literal(isolate_, "foo")) + .ToLocalChecked(); + EXPECT_EQ(foo->Int32Value(context).FromJust(), 42); + + v8::Local bar = + obj->Get(context, v8::String::NewFromUtf8Literal(isolate_, "bar")) + .ToLocalChecked(); + v8::String::Utf8Value bar_utf8(isolate_, bar); + EXPECT_STREQ(*bar_utf8, "baz"); +} + +TEST_F(IPCSerdesTest, RoundTripsTypedArray) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env test_env{handle_scope, argv}; + v8::Local context = isolate_->GetCurrentContext(); + + v8::Local binding = GetIpcSerdesBinding(*test_env, context); + v8::Local serialize = + GetFunction(context, binding, "serialize"); + v8::Local deserialize = + GetFunction(context, binding, "deserialize"); + + v8::Local ab = v8::ArrayBuffer::New(isolate_, 4); + uint8_t* data = static_cast(ab->Data()); + for (uint8_t i = 0; i < 4; i++) data[i] = i + 1; + v8::Local input = v8::Uint8Array::New(ab, 0, 4); + + v8::Local result = + RoundTrip(context, serialize, deserialize, input); + ASSERT_TRUE(result->IsUint8Array()); + v8::Local out = result.As(); + ASSERT_EQ(out->ByteLength(), static_cast(4)); + + node::ArrayBufferViewContents out_bytes(result); + for (uint8_t i = 0; i < 4; i++) { + EXPECT_EQ(out_bytes.data()[i], i + 1); + } + + // A plain Uint8Array must not come back as a Node Buffer. + Environment* env = *test_env; + EXPECT_FALSE( + out->GetPrototypeV2()->StrictEquals(env->buffer_prototype_object())); +} + +TEST_F(IPCSerdesTest, BufferRoundTripsAsBuffer) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env test_env{handle_scope, argv}; + v8::Local context = isolate_->GetCurrentContext(); + + v8::Local binding = GetIpcSerdesBinding(*test_env, context); + v8::Local serialize = + GetFunction(context, binding, "serialize"); + v8::Local deserialize = + GetFunction(context, binding, "deserialize"); + + v8::Local input = + node::Buffer::Copy(isolate_, "Hello!", 6).ToLocalChecked(); + + v8::Local result = + RoundTrip(context, serialize, deserialize, input); + ASSERT_TRUE(result->IsUint8Array()); + v8::Local out = result.As(); + + // The key correctness property: a Buffer round-trips as a Buffer, not a + // plain Uint8Array. + Environment* env = *test_env; + EXPECT_TRUE( + out->GetPrototypeV2()->StrictEquals(env->buffer_prototype_object())); + EXPECT_EQ(node::Buffer::Length(out), static_cast(6)); + EXPECT_EQ(memcmp(node::Buffer::Data(out), "Hello!", 6), 0); +} + +} // namespace diff --git a/test/parallel/test-child-process-advanced-serialization-host-objects.js b/test/parallel/test-child-process-advanced-serialization-host-objects.js new file mode 100644 index 00000000000000..4edd87fe26d336 --- /dev/null +++ b/test/parallel/test-child-process-advanced-serialization-host-objects.js @@ -0,0 +1,107 @@ +// Flags: --expose-internals +'use strict'; + +// Regression tests for the native `advanced` IPC codec (see PR #63933). +// Host-object classification and tag validation must match the previous +// JavaScript (v8.DefaultSerializer-based) behavior exactly. +const common = require('../common'); +const assert = require('assert'); +const { fork } = require('child_process'); +const v8 = require('v8'); +const { MessageChannel } = require('worker_threads'); +const { internalBinding } = require('internal/test/binding'); + +if (process.argv[2] === 'inspect') { + process.on('message', (value) => { + process.send({ + isBuffer: Buffer.isBuffer(value), + keys: Object.keys(value), + visible: value?.visible, + }); + }); + return; +} + +function inspect(value) { + return new Promise((resolve, reject) => { + const child = fork(__filename, ['inspect'], { + serialization: 'advanced', + stdio: ['ignore', 'ignore', 'inherit', 'ipc'], + }); + child.once('message', (message) => { + child.disconnect(); + resolve(message); + }); + try { + child.send(value); + } catch (err) { + child.disconnect(); + reject(err); + } + }); +} + +async function main() { + // 1) Spreading a non-ArrayBufferView host object must copy own properties + // with [[DefineOwnProperty]] semantics, i.e. without invoking inherited + // setters (the JS codec used `{ ...object }`). + { + const { port1, port2 } = new MessageChannel(); + port1.visible = 1; + Object.defineProperty(Object.prototype, 'visible', { + configurable: true, + get() { return undefined; }, + set() { throw new Error('setter called'); }, + }); + let message; + try { + message = await inspect(port1); + } finally { + delete Object.prototype.visible; + port1.close(); + port2.close(); + } + assert.strictEqual(message.visible, 1); + assert.strictEqual(message.keys[0], 'visible'); + } + + // 2) A Buffer whose `.constructor` is reassigned is classified by its + // constructor, exactly like v8.DefaultSerializer: it is no longer a + // Buffer on the receiving side. + { + const buf = Buffer.from('abc'); + buf.constructor = Uint8Array; + const message = await inspect(buf); + assert.strictEqual(message.isBuffer, false); + } + + // 3) Conversely, a Uint8Array with `.constructor === Buffer` round-trips as + // a Buffer. + { + const uint8 = new Uint8Array([1, 2, 3]); + uint8.constructor = Buffer; + const message = await inspect(uint8); + assert.strictEqual(message.isBuffer, true); + } + + // 4) A host-object tag other than the two the codec emits (0 and 1) must be + // rejected rather than silently accepted. + { + const { deserialize } = internalBinding('ipc_serdes'); + + class BadTagSerializer extends v8.DefaultSerializer { + _writeHostObject(value) { + this.writeUint32(2); // Valid tags are only 0 or 1 + return super._writeHostObject(value); + } + } + const ser = new BadTagSerializer(); + ser.writeHeader(); + ser.writeValue(Buffer.from('x')); + const payload = ser.releaseBuffer(); + + assert.throws(() => deserialize(payload), { code: 'ERR_INVALID_STATE' }); + } +} + +main().then(common.mustCall()); diff --git a/typings/globals.d.ts b/typings/globals.d.ts index ddd5885faa057f..0875bd3d37adff 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -9,6 +9,7 @@ import { DebugBinding } from './internalBinding/debug'; import { EncodingBinding } from './internalBinding/encoding_binding'; import { HttpParserBinding } from './internalBinding/http_parser'; import { InspectorBinding } from './internalBinding/inspector'; +import { IPCSerdesBinding } from './internalBinding/ipc_serdes'; import { FsBinding } from './internalBinding/fs'; import { FsDirBinding } from './internalBinding/fs_dir'; import { ICUBinding } from './internalBinding/icu'; @@ -47,6 +48,7 @@ interface InternalBindingMap { http_parser: HttpParserBinding; icu: ICUBinding; inspector: InspectorBinding; + ipc_serdes: IPCSerdesBinding; locks: LocksBinding; messaging: MessagingBinding; modules: ModulesBinding; diff --git a/typings/internalBinding/ipc_serdes.d.ts b/typings/internalBinding/ipc_serdes.d.ts new file mode 100644 index 00000000000000..dc00f068e4270a --- /dev/null +++ b/typings/internalBinding/ipc_serdes.d.ts @@ -0,0 +1,8 @@ +declare namespace InternalIPCSerdesBinding { + type Buffer = Uint8Array; +} + +export interface IPCSerdesBinding { + serialize(message: unknown): InternalIPCSerdesBinding.Buffer; + deserialize(buffer: ArrayBufferView): unknown; +}