Skip to content

Commit

Permalink
src: serialize both BaseObject slots
Browse files Browse the repository at this point in the history
We previously only return startup data for the first slot for
BaseObjects because we can already serialize all the necessary
information in one go, but slots that do not get special startup
data would be serialized verbatim which means that the pointer
addresses are going to be part of the snapshot blob, resulting
in indeterminism.

This patch updates the serialization routines and capture information
for both of the two slots - the first slot with type information
and memory management type (which we can use in the future for
cppgc-managed objects) and the second slot with data about the
object itself. This way the embeedder slots can be serialized
in a reproducible manner in the snapshot.

PR-URL: #48996
Refs: nodejs/build#3043
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
  • Loading branch information
joyeecheung authored and UlisesGascon committed Sep 10, 2023
1 parent 30f26a9 commit e994688
Show file tree
Hide file tree
Showing 11 changed files with 84 additions and 41 deletions.
4 changes: 2 additions & 2 deletions src/encoding_binding.cc
Expand Up @@ -62,7 +62,7 @@ bool BindingData::PrepareForSerialization(Local<Context> context,
}

InternalFieldInfoBase* BindingData::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
InternalFieldInfo* info = internal_field_info_;
internal_field_info_ = nullptr;
return info;
Expand All @@ -72,7 +72,7 @@ void BindingData::Deserialize(Local<Context> context,
Local<Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
v8::HandleScope scope(context->GetIsolate());
Realm* realm = Realm::GetCurrent(context);
// Recreate the buffer in the constructor.
Expand Down
3 changes: 2 additions & 1 deletion src/env.cc
Expand Up @@ -12,6 +12,7 @@
#include "node_options-inl.h"
#include "node_process-inl.h"
#include "node_shadow_realm.h"
#include "node_snapshotable.h"
#include "node_v8_platform-inl.h"
#include "node_worker.h"
#include "req_wrap-inl.h"
Expand Down Expand Up @@ -1760,7 +1761,7 @@ void Environment::EnqueueDeserializeRequest(DeserializeRequestCallback cb,
Local<Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
DeserializeRequest request{cb, {isolate(), holder}, index, info};
deserialize_requests_.push_back(std::move(request));
}
Expand Down
4 changes: 2 additions & 2 deletions src/node_blob.cc
Expand Up @@ -532,7 +532,7 @@ void BlobBindingData::Deserialize(Local<Context> context,
Local<Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
HandleScope scope(context->GetIsolate());
Realm* realm = Realm::GetCurrent(context);
BlobBindingData* binding = realm->AddBindingData<BlobBindingData>(holder);
Expand All @@ -548,7 +548,7 @@ bool BlobBindingData::PrepareForSerialization(Local<Context> context,
}

InternalFieldInfoBase* BlobBindingData::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
InternalFieldInfo* info =
InternalFieldInfoBase::New<InternalFieldInfo>(type());
return info;
Expand Down
4 changes: 2 additions & 2 deletions src/node_file.cc
Expand Up @@ -3151,7 +3151,7 @@ void BindingData::Deserialize(Local<Context> context,
Local<Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
HandleScope scope(context->GetIsolate());
Realm* realm = Realm::GetCurrent(context);
InternalFieldInfo* casted_info = static_cast<InternalFieldInfo*>(info);
Expand Down Expand Up @@ -3179,7 +3179,7 @@ bool BindingData::PrepareForSerialization(Local<Context> context,
}

InternalFieldInfoBase* BindingData::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
InternalFieldInfo* info = internal_field_info_;
internal_field_info_ = nullptr;
return info;
Expand Down
4 changes: 2 additions & 2 deletions src/node_process_methods.cc
Expand Up @@ -552,7 +552,7 @@ bool BindingData::PrepareForSerialization(Local<Context> context,
}

InternalFieldInfoBase* BindingData::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
InternalFieldInfo* info =
InternalFieldInfoBase::New<InternalFieldInfo>(type());
return info;
Expand All @@ -562,7 +562,7 @@ void BindingData::Deserialize(Local<Context> context,
Local<Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
v8::HandleScope scope(context->GetIsolate());
Realm* realm = Realm::GetCurrent(context);
// Recreate the buffer in the constructor.
Expand Down
80 changes: 56 additions & 24 deletions src/node_snapshotable.cc
Expand Up @@ -1146,25 +1146,33 @@ std::string SnapshotableObject::GetTypeName() const {
void DeserializeNodeInternalFields(Local<Object> holder,
int index,
StartupData payload,
void* env) {
void* callback_data) {
if (payload.raw_size == 0) {
holder->SetAlignedPointerInInternalField(index, nullptr);
return;
}

per_process::Debug(DebugCategory::MKSNAPSHOT,
"Deserialize internal field %d of %p, size=%d\n",
static_cast<int>(index),
(*holder),
static_cast<int>(payload.raw_size));

if (payload.raw_size == 0) {
holder->SetAlignedPointerInInternalField(index, nullptr);
Environment* env = static_cast<Environment*>(callback_data);

// To deserialize the first field, check the type and re-tag the object.
if (index == BaseObject::kEmbedderType) {
int size = sizeof(EmbedderTypeInfo);
DCHECK_EQ(payload.raw_size, size);
EmbedderTypeInfo read_data;
memcpy(&read_data, payload.data, size);
// For now we only support non-cppgc objects.
CHECK_EQ(read_data.mode, EmbedderTypeInfo::MemoryMode::kBaseObject);
BaseObject::TagBaseObject(env->isolate_data(), holder);
return;
}

DCHECK_EQ(index, BaseObject::kEmbedderType);

Environment* env_ptr = static_cast<Environment*>(env);
// To deserialize the second field, enqueue a deserialize request.
DCHECK_IS_SNAPSHOT_SLOT(index);
const InternalFieldInfoBase* info =
reinterpret_cast<const InternalFieldInfoBase*>(payload.data);
// TODO(joyeecheung): we can add a constant kNodeEmbedderId to the
Expand All @@ -1177,7 +1185,7 @@ void DeserializeNodeInternalFields(Local<Object> holder,
"Object %p is %s\n", \
(*holder), \
#NativeTypeName); \
env_ptr->EnqueueDeserializeRequest( \
env->EnqueueDeserializeRequest( \
NativeTypeName::Deserialize, \
holder, \
index, \
Expand All @@ -1203,28 +1211,52 @@ void DeserializeNodeInternalFields(Local<Object> holder,
StartupData SerializeNodeContextInternalFields(Local<Object> holder,
int index,
void* callback_data) {
// We only do one serialization for the kEmbedderType slot, the result
// contains everything necessary for deserializing the entire object,
// including the fields whose index is bigger than kEmbedderType
// (most importantly, BaseObject::kSlot).
// For Node.js this design is enough for all the native binding that are
// serializable.
// For the moment we do not set any internal fields in ArrayBuffer
// or ArrayBufferViews, so just return nullptr.
if (holder->IsArrayBuffer() || holder->IsArrayBufferView()) {
CHECK_NULL(holder->GetAlignedPointerFromInternalField(index));
return StartupData{nullptr, 0};
}

// Use the V8 convention and serialize unknown objects verbatim.
Environment* env = static_cast<Environment*>(callback_data);
if (index != BaseObject::kEmbedderType ||
!BaseObject::IsBaseObject(env->isolate_data(), holder)) {
if (!BaseObject::IsBaseObject(env->isolate_data(), holder)) {
per_process::Debug(DebugCategory::MKSNAPSHOT,
"Serialize unknown object, index=%d, holder=%p\n",
static_cast<int>(index),
*holder);
return StartupData{nullptr, 0};
}

per_process::Debug(DebugCategory::MKSNAPSHOT,
"Serialize internal field, index=%d, holder=%p\n",
"Serialize BaseObject, index=%d, holder=%p\n",
static_cast<int>(index),
*holder);

void* native_ptr =
holder->GetAlignedPointerFromInternalField(BaseObject::kSlot);
per_process::Debug(DebugCategory::MKSNAPSHOT, "native = %p\n", native_ptr);
DCHECK(static_cast<BaseObject*>(native_ptr)->is_snapshotable());
SnapshotableObject* obj = static_cast<SnapshotableObject*>(native_ptr);
BaseObject* object_ptr = static_cast<BaseObject*>(
holder->GetAlignedPointerFromInternalField(BaseObject::kSlot));
// If the native object is already set to null, ignore it.
if (object_ptr == nullptr) {
return StartupData{nullptr, 0};
}

DCHECK(object_ptr->is_snapshotable());
SnapshotableObject* obj = static_cast<SnapshotableObject*>(object_ptr);

// To serialize the type field, save data in a EmbedderTypeInfo.
if (index == BaseObject::kEmbedderType) {
int size = sizeof(EmbedderTypeInfo);
char* data = new char[size];
// We need to use placement new because V8 calls delete[] on the returned
// data.
// TODO(joyeecheung): support cppgc objects.
new (data) EmbedderTypeInfo(obj->type(),
EmbedderTypeInfo::MemoryMode::kBaseObject);
return StartupData{data, size};
}

// To serialize the slot field, invoke Serialize() method on the object.
DCHECK_IS_SNAPSHOT_SLOT(index);

per_process::Debug(DebugCategory::MKSNAPSHOT,
"Object %p is %s, ",
Expand Down Expand Up @@ -1380,7 +1412,7 @@ bool BindingData::PrepareForSerialization(Local<Context> context,
}

InternalFieldInfoBase* BindingData::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
InternalFieldInfo* info = internal_field_info_;
internal_field_info_ = nullptr;
return info;
Expand All @@ -1390,7 +1422,7 @@ void BindingData::Deserialize(Local<Context> context,
Local<Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
v8::HandleScope scope(context->GetIsolate());
Realm* realm = Realm::GetCurrent(context);
// Recreate the buffer in the constructor.
Expand Down
10 changes: 10 additions & 0 deletions src/node_snapshotable.h
Expand Up @@ -68,6 +68,14 @@ struct InternalFieldInfoBase {
InternalFieldInfoBase() = default;
};

struct EmbedderTypeInfo {
enum class MemoryMode : uint8_t { kBaseObject, kCppGC };
EmbedderTypeInfo(EmbedderObjectType t, MemoryMode m) : type(t), mode(m) {}
EmbedderTypeInfo() = default;
EmbedderObjectType type;
MemoryMode mode;
};

// An interface for snapshotable native objects to inherit from.
// Use the SERIALIZABLE_OBJECT_METHODS() macro in the class to define
// the following methods to implement:
Expand Down Expand Up @@ -123,6 +131,8 @@ void SerializeSnapshotableObjects(Realm* realm,
v8::SnapshotCreator* creator,
RealmSerializeInfo* info);

#define DCHECK_IS_SNAPSHOT_SLOT(index) DCHECK_EQ(index, BaseObject::kSlot)

namespace mksnapshot {
class BindingData : public SnapshotableObject {
public:
Expand Down
4 changes: 2 additions & 2 deletions src/node_url.cc
Expand Up @@ -54,7 +54,7 @@ bool BindingData::PrepareForSerialization(v8::Local<v8::Context> context,
}

InternalFieldInfoBase* BindingData::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
InternalFieldInfo* info =
InternalFieldInfoBase::New<InternalFieldInfo>(type());
return info;
Expand All @@ -64,7 +64,7 @@ void BindingData::Deserialize(v8::Local<v8::Context> context,
v8::Local<v8::Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
v8::HandleScope scope(context->GetIsolate());
Realm* realm = Realm::GetCurrent(context);
BindingData* binding = realm->AddBindingData<BindingData>(holder);
Expand Down
4 changes: 2 additions & 2 deletions src/node_util.cc
Expand Up @@ -246,7 +246,7 @@ bool WeakReference::PrepareForSerialization(Local<Context> context,
}

InternalFieldInfoBase* WeakReference::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
InternalFieldInfo* info =
InternalFieldInfoBase::New<InternalFieldInfo>(type());
info->target = target_index_;
Expand All @@ -258,7 +258,7 @@ void WeakReference::Deserialize(Local<Context> context,
Local<Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
HandleScope scope(context->GetIsolate());

InternalFieldInfo* weak_info = reinterpret_cast<InternalFieldInfo*>(info);
Expand Down
4 changes: 2 additions & 2 deletions src/node_v8.cc
Expand Up @@ -152,7 +152,7 @@ void BindingData::Deserialize(Local<Context> context,
Local<Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
HandleScope scope(context->GetIsolate());
Realm* realm = Realm::GetCurrent(context);
// Recreate the buffer in the constructor.
Expand All @@ -163,7 +163,7 @@ void BindingData::Deserialize(Local<Context> context,
}

InternalFieldInfoBase* BindingData::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
InternalFieldInfo* info = internal_field_info_;
internal_field_info_ = nullptr;
return info;
Expand Down
4 changes: 2 additions & 2 deletions src/timers.cc
Expand Up @@ -94,7 +94,7 @@ bool BindingData::PrepareForSerialization(Local<Context> context,
}

InternalFieldInfoBase* BindingData::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
InternalFieldInfo* info =
InternalFieldInfoBase::New<InternalFieldInfo>(type());
return info;
Expand All @@ -104,7 +104,7 @@ void BindingData::Deserialize(Local<Context> context,
Local<Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
DCHECK_IS_SNAPSHOT_SLOT(index);
v8::HandleScope scope(context->GetIsolate());
Realm* realm = Realm::GetCurrent(context);
// Recreate the buffer in the constructor.
Expand Down

0 comments on commit e994688

Please sign in to comment.