mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
f6156ce004
commit
a1074b8460
9 files changed
+739
-50
No files matched your search
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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)
|
||||
@@ -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());
|
||||
Vendored
+2
@@ -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
@@ -0,0 +1,8 @@
|
||||
declare namespace InternalIPCSerdesBinding {
|
||||
type Buffer = Uint8Array;
|
||||
}
|
||||
|
||||
export interface IPCSerdesBinding {
|
||||
serialize(message: unknown): InternalIPCSerdesBinding.Buffer;
|
||||
deserialize(buffer: ArrayBufferView): unknown;
|
||||
}
|
||||
Reference in new issue
Block a user