child_process: serialize advanced IPC messages natively

The `advanced` IPC serialization codec was implemented in JavaScript
(ChildProcessSerializer / ChildProcessDeserializer in
lib/internal/child_process/serialization.js). It allocated a wrapper
serializer/deserializer per message and crossed the JS/C++ boundary
several times for every message (writeHeader, writeValue, releaseBuffer,
readHeader, readValue and friends).

Move the codec into a native `ipc_serdes` binding that drives the V8
ValueSerializer/ValueDeserializer with a C++ delegate. The wire format
is preserved byte-for-byte: a big-endian uint32 length prefix followed
by the V8 payload, with ArrayBufferViews tagged as host objects so that
Node Buffers round-trip as Buffers rather than plain Uint8Arrays. The
JSON codec is left unchanged.

A cctest (test/cctest/test_node_ipc_serdes.cc) exercises the binding
directly, covering round-trips of primitives, objects, typed arrays and
Buffers (including the Buffer-vs-Uint8Array distinction) and asserting
the big-endian length-prefix framing.

Round-trip throughput
(benchmark/child_process/child-process-ipc-roundtrip):

  payload   before     after      change
  64 B      ~300k/s    ~800k/s    +166%
  1 KiB     ~272k/s    ~616k/s    +126%
  16 KiB    ~91k/s     ~120k/s    +32%
  64 KiB    ~30k/s     ~35k/s     +16%

The gain is largest for small messages, where per-message JavaScript
overhead dominated, and tapers for large messages, where the actual
serialization (already native) dominates.

Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
PR-URL: https://github.com/nodejs/node/pull/63933
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
This commit is contained in:
Yagiz Nizipli authored and GitHub committed 2026-06-18 18:14:46 +00:00
1 parent f6156ce004
commit a1074b8460
9 files changed
+739 -50

No files matched your search

+8 -50
View File
@@ -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,25 @@ 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);
// Pass the stable Buffer constructor so the native codec can classify Node
// Buffers via `value.constructor === Buffer` without reading the tamperable
// Buffer.prototype.constructor.
const serializedMessage = serialize(message, Buffer);
const result = channel.writeBuffer(req, serializedMessage, handle);
+1
View File
@@ -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',
+1
View File
@@ -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) \
+1
View File
@@ -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) \
+385
View File
@@ -0,0 +1,385 @@
#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 <cstring>
#include <utility>
// 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<Object> 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<Object> MakeView(Environment* env,
uint32_t type_index,
Local<ArrayBuffer> ab,
size_t byte_offset,
size_t byte_length) {
const size_t length = byte_length / BytesPerElement(type_index);
Local<Object> 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<Uint8Array> 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:
IPCSerializerDelegate(Environment* env, Local<Value> buffer_constructor)
: env_(env), buffer_constructor_(buffer_constructor) {}
void set_serializer(ValueSerializer* serializer) { serializer_ = serializer; }
void ThrowDataCloneError(Local<String> message) override {
env_->isolate()->ThrowException(Exception::Error(message));
}
Maybe<bool> WriteHostObject(Isolate* isolate, Local<Object> object) override {
if (object->IsArrayBufferView()) {
serializer_->WriteUint32(kArrayBufferViewTag);
// Matches v8.js DefaultSerializer._writeHostObject: a Node Buffer is
// identified by `value.constructor === Buffer`. The comparison is made
// against the stable Buffer reference captured by serialization.js
// (`require('buffer').Buffer`), not a value read back from
// Buffer.prototype, which is itself tamperable.
Local<Context> context = env_->context();
Local<Value> view_constructor;
if (!object->Get(context, FIXED_ONE_BYTE_STRING(isolate, "constructor"))
.ToLocal(&view_constructor)) {
return Nothing<bool>();
}
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<bool>();
}
}
ArrayBufferViewContents<char> contents(object);
serializer_->WriteUint32(type_index);
serializer_->WriteUint32(static_cast<uint32_t>(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> context = env_->context();
Local<v8::Array> names;
if (!object->GetOwnPropertyNames(context).ToLocal(&names)) {
return Nothing<bool>();
}
Local<Object> copy = Object::New(isolate);
const uint32_t len = names->Length();
for (uint32_t i = 0; i < len; i++) {
Local<Value> key;
if (!names->Get(context, i).ToLocal(&key)) return Nothing<bool>();
Local<Value> val;
if (!object->Get(context, key).ToLocal(&val)) return Nothing<bool>();
if (copy->CreateDataProperty(context, key.As<Name>(), val).IsNothing())
return Nothing<bool>();
}
return serializer_->WriteValue(context, copy);
}
private:
Environment* env_;
// Stable `require('buffer').Buffer` reference passed from serialization.js,
// used to classify Node Buffers without reading Buffer.prototype.constructor
// (which can be tampered with).
Local<Value> buffer_constructor_;
ValueSerializer* serializer_ = nullptr;
};
class IPCDeserializerDelegate : public ValueDeserializer::Delegate {
public:
IPCDeserializerDelegate(Environment* env, Local<ArrayBuffer> ab)
: env_(env), ab_(ab) {}
void set_deserializer(ValueDeserializer* deserializer) {
deserializer_ = deserializer;
}
MaybeLocal<Object> ReadHostObject(Isolate* isolate) override {
uint32_t tag;
if (!deserializer_->ReadUint32(&tag)) return {};
if (tag == kNotArrayBufferViewTag) {
Local<Value> 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<Object>();
}
// 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<const uint8_t*>(data) -
static_cast<const uint8_t*>(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<BackingStore> store =
ArrayBuffer::NewBackingStore(isolate, byte_length);
memcpy(store->Data(), data, byte_length);
Local<ArrayBuffer> copy = ArrayBuffer::New(isolate, std::move(store));
return MakeView(env_, type_index, copy, 0, byte_length);
}
private:
Environment* env_;
Local<ArrayBuffer> ab_;
ValueDeserializer* deserializer_ = nullptr;
};
static void Serialize(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
IPCSerializerDelegate delegate(env, args[1]);
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<uint8_t*, size_t> result = serializer.Release();
uint8_t* buf = result.first;
const size_t size = result.second;
const uint32_t payload_length = static_cast<uint32_t>(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<Object> out;
if (Buffer::New(env, reinterpret_cast<char*>(buf), size).ToLocal(&out)) {
args.GetReturnValue().Set(out);
}
}
static void Deserialize(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
CHECK(args[0]->IsArrayBufferView());
Local<ArrayBufferView> view = args[0].As<ArrayBufferView>();
Local<ArrayBuffer> ab = view->Buffer();
const size_t byte_offset = view->ByteOffset();
const size_t byte_length = view->ByteLength();
const uint8_t* data = static_cast<const uint8_t*>(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> value;
if (deserializer.ReadValue(context).ToLocal(&value)) {
args.GetReturnValue().Set(value);
}
}
static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> 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)
+210
View File
@@ -0,0 +1,210 @@
#include "node.h"
#include "node_binding.h"
#include "node_buffer.h"
#include "node_test_fixture.h"
#include <string>
using node::Environment;
namespace {
// Bootstraps the environment (so Buffer and friends are initialized) and
// returns the `ipc_serdes` internal binding object.
v8::Local<v8::Object> GetIpcSerdesBinding(Environment* env,
v8::Local<v8::Context> context) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
node::LoadEnvironment(env, "");
v8::Local<v8::Function> get_internal_binding =
v8::Function::New(context, node::binding::GetInternalBinding)
.ToLocalChecked();
v8::Local<v8::Value> binding_name =
v8::String::NewFromUtf8Literal(isolate, "ipc_serdes");
v8::Local<v8::Value> binding =
get_internal_binding
->Call(context, v8::Undefined(isolate), 1, &binding_name)
.ToLocalChecked();
return binding.As<v8::Object>();
}
v8::Local<v8::Function> GetFunction(v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const char* name) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::Local<v8::Value> value =
object
->Get(context,
v8::String::NewFromUtf8(isolate, name).ToLocalChecked())
.ToLocalChecked();
return value.As<v8::Function>();
}
// Serializes `input`, validates the 4-byte big-endian length prefix, strips it
// (as parseChannelMessages does), and returns the deserialized value.
v8::Local<v8::Value> RoundTrip(v8::Local<v8::Context> context,
v8::Local<v8::Function> serialize,
v8::Local<v8::Function> deserialize,
v8::Local<v8::Value> input) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
// serialize(message, Buffer): the second argument is the stable Buffer
// constructor used to classify Node Buffers.
v8::Local<v8::Value> buffer_ctor =
context->Global()
->Get(context, v8::String::NewFromUtf8Literal(isolate, "Buffer"))
.ToLocalChecked();
v8::Local<v8::Value> serialize_args[] = {input, buffer_ctor};
v8::Local<v8::Value> serialized =
serialize->Call(context, v8::Undefined(isolate), 2, serialize_args)
.ToLocalChecked();
EXPECT_TRUE(serialized->IsUint8Array());
v8::Local<v8::Uint8Array> buf = serialized.As<v8::Uint8Array>();
EXPECT_GE(buf->ByteLength(), static_cast<size_t>(4));
node::ArrayBufferViewContents<uint8_t> bytes(serialized);
const uint32_t payload_length =
(static_cast<uint32_t>(bytes.data()[0]) << 24) |
(static_cast<uint32_t>(bytes.data()[1]) << 16) |
(static_cast<uint32_t>(bytes.data()[2]) << 8) |
static_cast<uint32_t>(bytes.data()[3]);
EXPECT_EQ(payload_length, buf->ByteLength() - 4);
v8::Local<v8::Value> 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<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::Object> binding = GetIpcSerdesBinding(*test_env, context);
v8::Local<v8::Function> serialize =
GetFunction(context, binding, "serialize");
v8::Local<v8::Function> deserialize =
GetFunction(context, binding, "deserialize");
v8::Local<v8::Value> 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<v8::Value> input : inputs) {
v8::Local<v8::Value> 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<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::Object> binding = GetIpcSerdesBinding(*test_env, context);
v8::Local<v8::Function> serialize =
GetFunction(context, binding, "serialize");
v8::Local<v8::Function> deserialize =
GetFunction(context, binding, "deserialize");
v8::Local<v8::Object> 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<v8::Value> result =
RoundTrip(context, serialize, deserialize, input);
ASSERT_TRUE(result->IsObject());
v8::Local<v8::Object> obj = result.As<v8::Object>();
v8::Local<v8::Value> foo =
obj->Get(context, v8::String::NewFromUtf8Literal(isolate_, "foo"))
.ToLocalChecked();
EXPECT_EQ(foo->Int32Value(context).FromJust(), 42);
v8::Local<v8::Value> 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<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::Object> binding = GetIpcSerdesBinding(*test_env, context);
v8::Local<v8::Function> serialize =
GetFunction(context, binding, "serialize");
v8::Local<v8::Function> deserialize =
GetFunction(context, binding, "deserialize");
v8::Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate_, 4);
uint8_t* data = static_cast<uint8_t*>(ab->Data());
for (uint8_t i = 0; i < 4; i++) data[i] = i + 1;
v8::Local<v8::Uint8Array> input = v8::Uint8Array::New(ab, 0, 4);
v8::Local<v8::Value> result =
RoundTrip(context, serialize, deserialize, input);
ASSERT_TRUE(result->IsUint8Array());
v8::Local<v8::Uint8Array> out = result.As<v8::Uint8Array>();
ASSERT_EQ(out->ByteLength(), static_cast<size_t>(4));
node::ArrayBufferViewContents<uint8_t> 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<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::Object> binding = GetIpcSerdesBinding(*test_env, context);
v8::Local<v8::Function> serialize =
GetFunction(context, binding, "serialize");
v8::Local<v8::Function> deserialize =
GetFunction(context, binding, "deserialize");
v8::Local<v8::Object> input =
node::Buffer::Copy(isolate_, "Hello!", 6).ToLocalChecked();
v8::Local<v8::Value> result =
RoundTrip(context, serialize, deserialize, input);
ASSERT_TRUE(result->IsUint8Array());
v8::Local<v8::Object> out = result.As<v8::Object>();
// 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<size_t>(6));
EXPECT_EQ(memcmp(node::Buffer::Data(out), "Hello!", 6), 0);
}
} // namespace
@@ -0,0 +1,123 @@
// 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) Tampering with Buffer.prototype.constructor must not fool the
// classifier: a genuine Buffer is then classified by its (now non-Buffer)
// constructor, matching v8.DefaultSerializer, which compares against a
// stable Buffer reference rather than re-reading Buffer.prototype.
{
const original = Buffer.prototype.constructor;
Buffer.prototype.constructor = Uint8Array;
let message;
try {
message = await inspect(Buffer.from('abc'));
} finally {
Buffer.prototype.constructor = original;
}
assert.strictEqual(message.isBuffer, false);
}
// 5) 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());
+2
View File
@@ -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;
+8
View File
@@ -0,0 +1,8 @@
declare namespace InternalIPCSerdesBinding {
type Buffer = Uint8Array;
}
export interface IPCSerdesBinding {
serialize(message: unknown): InternalIPCSerdesBinding.Buffer;
deserialize(buffer: ArrayBufferView): unknown;
}