From 99743ad460ea5b9795ba9d70a074e75d7362a3d1 Mon Sep 17 00:00:00 2001 From: mtrofin Date: Mon, 20 Mar 2017 12:03:23 -0700 Subject: [PATCH] [wasm] Transferrable modules We want to restrict structured cloning in Chrome to: - postMessage senders and receivers that are co-located in the same process - indexedDB (just https). For context, on the Chrome side, we will achieve the postMessage part by using a mechanism similar to transferrables: the SerializedScriptValue will have a list of wasm modules, separate from the serialized data stream; and this list won't be copied cross process boundaries. The IDB part is achieved by explicitly opting in reading/writing to the serialization stream. To block attack vectors in IPC cases, the default for deserialization will be to expect data in the wasm transfers list. This change is the V8 side necessary to enabling this design. We introduce TransferrableModule, an opaque datatype exposed to the embedder. Internally, TransferrableModules are just serialized data, because we don't have a better mechanism, at the moment, for de-contextualizing/re-contextualizing wasm modules (wrt Isolate and Context). The chrome defaults will be implemented in the serialization/deserialization delegates on that side. For the v8 side of things, in the absence of a serialization delegate, the V8 serializer will write to serialization stream. In the absence of a deserialization delegate, the deserializer won't work. This asymmetry is intentional - it communicates to the embedder the need to make a policy decision, otherwise wasm serialization/deserialization won't work "out of the box". BUG=v8:6079 Review-Url: https://codereview.chromium.org/2748473004 Cr-Commit-Position: refs/heads/master@{#43955} --- include/v8.h | 51 ++++ src/api.cc | 48 ++++ src/value-serializer.cc | 56 +++- src/value-serializer.h | 7 + test/cctest/wasm/test-run-wasm-module.cc | 37 +++ test/unittests/value-serializer-unittest.cc | 289 ++++++++++++++++++-- 6 files changed, 464 insertions(+), 24 deletions(-) diff --git a/include/v8.h b/include/v8.h index 2f3c776007..38584e8af0 100644 --- a/include/v8.h +++ b/include/v8.h @@ -108,6 +108,7 @@ class Private; class Uint32; class Utils; class Value; +class WasmCompiledModule; template class Local; template class MaybeLocal; @@ -1709,6 +1710,8 @@ class V8_EXPORT ValueSerializer { virtual Maybe GetSharedArrayBufferId( Isolate* isolate, Local shared_array_buffer); + virtual Maybe GetWasmModuleTransferId( + Isolate* isolate, Local module); /* * Allocates memory for the buffer of at least the size provided. The actual * size (which may be greater or equal) is written to |actual_size|. If no @@ -1819,6 +1822,13 @@ class V8_EXPORT ValueDeserializer { * MaybeLocal() returned. */ virtual MaybeLocal ReadHostObject(Isolate* isolate); + + /* + * Get a WasmCompiledModule given a transfer_id previously provided + * by ValueSerializer::GetWasmModuleTransferId + */ + virtual MaybeLocal GetWasmModuleFromId( + Isolate* isolate, uint32_t transfer_id); }; ValueDeserializer(Isolate* isolate, const uint8_t* data, size_t size); @@ -1861,6 +1871,11 @@ class V8_EXPORT ValueDeserializer { */ void SetSupportsLegacyWireFormat(bool supports_legacy_wire_format); + /* + * Expect inline wasm in the data stream (rather than in-memory transfer) + */ + void SetExpectInlineWasm(bool allow_inline_wasm); + /* * Reads the underlying wire format version. Likely mostly to be useful to * legacy code reading old wire format versions. Must be called after @@ -3903,6 +3918,37 @@ class V8_EXPORT WasmCompiledModule : public Object { typedef std::pair, size_t> SerializedModule; // A buffer that is owned by the caller. typedef std::pair CallerOwnedBuffer; + + // An opaque, native heap object for transferring wasm modules. It + // supports move semantics, and does not support copy semantics. + class TransferrableModule final { + public: + TransferrableModule(TransferrableModule&& src) = default; + TransferrableModule(const TransferrableModule& src) = delete; + + TransferrableModule& operator=(TransferrableModule&& src) = default; + TransferrableModule& operator=(const TransferrableModule& src) = delete; + + private: + typedef std::pair, size_t> OwnedBuffer; + friend class WasmCompiledModule; + TransferrableModule(OwnedBuffer&& code, OwnedBuffer&& bytes) + : compiled_code(std::move(code)), wire_bytes(std::move(bytes)) {} + + OwnedBuffer compiled_code = {nullptr, 0}; + OwnedBuffer wire_bytes = {nullptr, 0}; + }; + + // Get an in-memory, non-persistable, and context-independent (meaning, + // suitable for transfer to another Isolate and Context) representation + // of this wasm compiled module. + TransferrableModule GetTransferrableModule(); + + // Efficiently re-create a WasmCompiledModule, without recompiling, from + // a TransferrableModule. + static MaybeLocal FromTransferrableModule( + Isolate* isolate, const TransferrableModule&); + // Get the wasm-encoded bytes that were used to compile this module. Local GetWasmWireBytes(); @@ -3924,6 +3970,11 @@ class V8_EXPORT WasmCompiledModule : public Object { static MaybeLocal Compile(Isolate* isolate, const uint8_t* start, size_t length); + static CallerOwnedBuffer AsCallerOwned( + const TransferrableModule::OwnedBuffer& buff) { + return {buff.first.get(), buff.second}; + } + WasmCompiledModule(); static void CheckCast(Value* obj); }; diff --git a/src/api.cc b/src/api.cc index 39e39bbb5e..c7ec5722e4 100644 --- a/src/api.cc +++ b/src/api.cc @@ -3125,6 +3125,11 @@ Maybe ValueSerializer::Delegate::GetSharedArrayBufferId( return Nothing(); } +Maybe ValueSerializer::Delegate::GetWasmModuleTransferId( + Isolate* v8_isolate, Local module) { + return Nothing(); +} + void* ValueSerializer::Delegate::ReallocateBufferMemory(void* old_buffer, size_t size, size_t* actual_size) { @@ -3213,6 +3218,15 @@ MaybeLocal ValueDeserializer::Delegate::ReadHostObject( return MaybeLocal(); } +MaybeLocal ValueDeserializer::Delegate::GetWasmModuleFromId( + Isolate* v8_isolate, uint32_t id) { + i::Isolate* isolate = reinterpret_cast(v8_isolate); + isolate->ScheduleThrow(*isolate->factory()->NewError( + isolate->error_function(), + i::MessageTemplate::kDataCloneDeserializationError)); + return MaybeLocal(); +} + struct ValueDeserializer::PrivateData { PrivateData(i::Isolate* i, i::Vector data, Delegate* delegate) : isolate(i), deserializer(i, data, delegate) {} @@ -3275,6 +3289,10 @@ void ValueDeserializer::SetSupportsLegacyWireFormat( private_->supports_legacy_wire_format = supports_legacy_wire_format; } +void ValueDeserializer::SetExpectInlineWasm(bool expect_inline_wasm) { + private_->deserializer.set_expect_inline_wasm(expect_inline_wasm); +} + uint32_t ValueDeserializer::GetWireFormatVersion() const { CHECK(!private_->has_aborted); return private_->deserializer.GetWireFormatVersion(); @@ -7506,6 +7524,36 @@ Local WasmCompiledModule::GetWasmWireBytes() { return Local::Cast(Utils::ToLocal(wire_bytes)); } +// Currently, wasm modules are bound, both to Isolate and to +// the Context they were created in. The currently-supported means to +// decontextualize and then re-contextualize a module is via +// serialization/deserialization. +WasmCompiledModule::TransferrableModule +WasmCompiledModule::GetTransferrableModule() { + i::DisallowHeapAllocation no_gc; + WasmCompiledModule::SerializedModule compiled_part = Serialize(); + + Local wire_bytes = GetWasmWireBytes(); + size_t wire_size = static_cast(wire_bytes->Length()); + uint8_t* bytes = new uint8_t[wire_size]; + wire_bytes->WriteOneByte(bytes, 0, wire_bytes->Length()); + + return TransferrableModule( + std::move(compiled_part), + std::make_pair( + std::unique_ptr(const_cast(bytes)), + wire_size)); +} + +MaybeLocal WasmCompiledModule::FromTransferrableModule( + Isolate* isolate, + const WasmCompiledModule::TransferrableModule& transferrable_module) { + MaybeLocal ret = + Deserialize(isolate, AsCallerOwned(transferrable_module.compiled_code), + AsCallerOwned(transferrable_module.wire_bytes)); + return ret; +} + WasmCompiledModule::SerializedModule WasmCompiledModule::Serialize() { i::Handle obj = i::Handle::cast(Utils::OpenHandle(this)); diff --git a/src/value-serializer.cc b/src/value-serializer.cc index 1dcc8e462f..4b8b454c5d 100644 --- a/src/value-serializer.cc +++ b/src/value-serializer.cc @@ -126,6 +126,8 @@ enum class SerializationTag : uint8_t { // wasmWireByteLength:uint32_t, then raw data // compiledDataLength:uint32_t, then raw data kWasmModule = 'W', + // A wasm module object transfer. next value is its index. + kWasmModuleTransfer = 'w', // The delegate is responsible for processing all following data. // This "escapes" to whatever wire format the delegate chooses. kHostObject = '\\', @@ -803,6 +805,19 @@ Maybe ValueSerializer::WriteJSArrayBufferView(JSArrayBufferView* view) { } Maybe ValueSerializer::WriteWasmModule(Handle object) { + if (delegate_ != nullptr) { + Maybe transfer_id = delegate_->GetWasmModuleTransferId( + reinterpret_cast(isolate_), + v8::Local::Cast(Utils::ToLocal(object))); + RETURN_VALUE_IF_SCHEDULED_EXCEPTION(isolate_, Nothing()); + uint32_t id = 0; + if (transfer_id.To(&id)) { + WriteTag(SerializationTag::kWasmModuleTransfer); + WriteVarint(id); + return Just(true); + } + } + Handle compiled_part( WasmCompiledModule::cast(object->GetEmbedderField(0)), isolate_); WasmEncodingTag encoding_tag = WasmEncodingTag::kRawBytes; @@ -1150,6 +1165,8 @@ MaybeHandle ValueDeserializer::ReadObjectInternal() { } case SerializationTag::kWasmModule: return ReadWasmModule(); + case SerializationTag::kWasmModuleTransfer: + return ReadWasmModuleTransfer(); case SerializationTag::kHostObject: return ReadHostObject(); default: @@ -1595,8 +1612,32 @@ MaybeHandle ValueDeserializer::ReadJSArrayBufferView( return typed_array; } +MaybeHandle ValueDeserializer::ReadWasmModuleTransfer() { + if (FLAG_wasm_disable_structured_cloning || expect_inline_wasm()) { + return MaybeHandle(); + } + + uint32_t transfer_id = 0; + Local module_value; + if (!ReadVarint().To(&transfer_id) || delegate_ == nullptr || + !delegate_ + ->GetWasmModuleFromId(reinterpret_cast(isolate_), + transfer_id) + .ToLocal(&module_value)) { + RETURN_EXCEPTION_IF_SCHEDULED_EXCEPTION(isolate_, JSObject); + return MaybeHandle(); + } + uint32_t id = next_id_++; + Handle module = + Handle::cast(Utils::OpenHandle(*module_value)); + AddObjectWithID(id, module); + return module; +} + MaybeHandle ValueDeserializer::ReadWasmModule() { - if (FLAG_wasm_disable_structured_cloning) return MaybeHandle(); + if (FLAG_wasm_disable_structured_cloning || !expect_inline_wasm()) { + return MaybeHandle(); + } Vector encoding_tag; if (!ReadRawBytes(sizeof(WasmEncodingTag)).To(&encoding_tag) || @@ -1625,21 +1666,22 @@ MaybeHandle ValueDeserializer::ReadWasmModule() { // Try to deserialize the compiled module first. ScriptData script_data(compiled_bytes.start(), compiled_bytes.length()); Handle compiled_part; + MaybeHandle result; if (WasmCompiledModuleSerializer::DeserializeWasmModule( isolate_, &script_data, wire_bytes) .ToHandle(&compiled_part)) { - return WasmModuleObject::New( + result = WasmModuleObject::New( isolate_, Handle::cast(compiled_part)); - } - - // If that fails, recompile. - MaybeHandle result; - { + } else { wasm::ErrorThrower thrower(isolate_, "ValueDeserializer::ReadWasmModule"); result = wasm::SyncCompile(isolate_, &thrower, wasm::ModuleWireBytes(wire_bytes)); } RETURN_EXCEPTION_IF_SCHEDULED_EXCEPTION(isolate_, JSObject); + uint32_t id = next_id_++; + if (!result.is_null()) { + AddObjectWithID(id, result.ToHandleChecked()); + } return result; } diff --git a/src/value-serializer.h b/src/value-serializer.h index 7961b2ea0b..ef424698d0 100644 --- a/src/value-serializer.h +++ b/src/value-serializer.h @@ -31,6 +31,7 @@ class JSValue; class Object; class Oddball; class Smi; +class WasmModuleObject; enum class SerializationTag : uint8_t; @@ -218,6 +219,9 @@ class ValueDeserializer { bool ReadUint64(uint64_t* value) WARN_UNUSED_RESULT; bool ReadDouble(double* value) WARN_UNUSED_RESULT; bool ReadRawBytes(size_t length, const void** data) WARN_UNUSED_RESULT; + void set_expect_inline_wasm(bool expect_inline_wasm) { + expect_inline_wasm_ = expect_inline_wasm; + } private: // Reading the wire format. @@ -230,6 +234,7 @@ class ValueDeserializer { Maybe ReadZigZag() WARN_UNUSED_RESULT; Maybe ReadDouble() WARN_UNUSED_RESULT; Maybe> ReadRawBytes(int size) WARN_UNUSED_RESULT; + bool expect_inline_wasm() const { return expect_inline_wasm_; } // Reads a string if it matches the one provided. // Returns true if this was the case. Otherwise, nothing is consumed. @@ -263,6 +268,7 @@ class ValueDeserializer { MaybeHandle ReadJSArrayBufferView( Handle buffer) WARN_UNUSED_RESULT; MaybeHandle ReadWasmModule() WARN_UNUSED_RESULT; + MaybeHandle ReadWasmModuleTransfer() WARN_UNUSED_RESULT; MaybeHandle ReadHostObject() WARN_UNUSED_RESULT; /* @@ -285,6 +291,7 @@ class ValueDeserializer { PretenureFlag pretenure_; uint32_t version_ = 0; uint32_t next_id_ = 0; + bool expect_inline_wasm_ = false; // Always global handles. Handle id_map_; diff --git a/test/cctest/wasm/test-run-wasm-module.cc b/test/cctest/wasm/test-run-wasm-module.cc index 4e79e422f0..70c0eb0129 100644 --- a/test/cctest/wasm/test-run-wasm-module.cc +++ b/test/cctest/wasm/test-run-wasm-module.cc @@ -469,6 +469,43 @@ TEST(BlockWasmCodeGenAtDeserialization) { Cleanup(); } +TEST(TransferrableWasmModules) { + v8::internal::AccountingAllocator allocator; + Zone zone(&allocator, ZONE_NAME); + + ZoneBuffer buffer(&zone); + WasmSerializationTest::BuildWireBytes(&zone, &buffer); + + Isolate* from_isolate = CcTest::InitIsolateOnce(); + ErrorThrower thrower(from_isolate, ""); + std::vector store; + { + HandleScope scope(from_isolate); + testing::SetupIsolateForWasmModule(from_isolate); + + MaybeHandle module_object = SyncCompile( + from_isolate, &thrower, ModuleWireBytes(buffer.begin(), buffer.end())); + v8::Local v8_module = + v8::Local::Cast(v8::Utils::ToLocal( + Handle::cast(module_object.ToHandleChecked()))); + store.push_back(v8_module->GetTransferrableModule()); + } + + { + v8::Isolate::CreateParams create_params; + create_params.array_buffer_allocator = + from_isolate->array_buffer_allocator(); + v8::Isolate* to_isolate = v8::Isolate::New(create_params); + v8::HandleScope new_scope(to_isolate); + v8::Local deserialization_context = + v8::Context::New(to_isolate); + deserialization_context->Enter(); + v8::MaybeLocal mod = + v8::WasmCompiledModule::FromTransferrableModule(to_isolate, store[0]); + CHECK(!mod.IsEmpty()); + } +} + TEST(MemorySize) { { // Initial memory size is 16, see wasm-module-builder.cc diff --git a/test/unittests/value-serializer-unittest.cc b/test/unittests/value-serializer-unittest.cc index c0037efb01..9f49c6498a 100644 --- a/test/unittests/value-serializer-unittest.cc +++ b/test/unittests/value-serializer-unittest.cc @@ -74,6 +74,9 @@ class ValueSerializerTest : public TestWithIsolate { return deserialization_context_; } + bool ExpectInlineWasm() const { return expect_inline_wasm_; } + void SetExpectInlineWasm(bool value) { expect_inline_wasm_ = value; } + // Overridden in more specific fixtures. virtual ValueSerializer::Delegate* GetSerializerDelegate() { return nullptr; } virtual void BeforeEncode(ValueSerializer*) {} @@ -172,6 +175,7 @@ class ValueSerializerTest : public TestWithIsolate { static_cast(data.size()), GetDeserializerDelegate()); deserializer.SetSupportsLegacyWireFormat(true); + deserializer.SetExpectInlineWasm(ExpectInlineWasm()); BeforeDecode(&deserializer); ASSERT_TRUE(deserializer.ReadHeader(context).FromMaybe(false)); Local result; @@ -196,6 +200,7 @@ class ValueSerializerTest : public TestWithIsolate { static_cast(data.size()), GetDeserializerDelegate()); deserializer.SetSupportsLegacyWireFormat(true); + deserializer.SetExpectInlineWasm(ExpectInlineWasm()); BeforeDecode(&deserializer); ASSERT_TRUE(deserializer.ReadHeader(context).FromMaybe(false)); ASSERT_EQ(0u, deserializer.GetWireFormatVersion()); @@ -219,6 +224,7 @@ class ValueSerializerTest : public TestWithIsolate { static_cast(data.size()), GetDeserializerDelegate()); deserializer.SetSupportsLegacyWireFormat(true); + deserializer.SetExpectInlineWasm(ExpectInlineWasm()); BeforeDecode(&deserializer); Maybe header_result = deserializer.ReadHeader(context); if (header_result.IsNothing()) { @@ -275,6 +281,7 @@ class ValueSerializerTest : public TestWithIsolate { Local deserialization_context_; Local host_object_constructor_template_; i::Isolate* isolate_; + bool expect_inline_wasm_ = false; DISALLOW_COPY_AND_ASSIGN(ValueSerializerTest); }; @@ -2594,7 +2601,44 @@ TEST_F(ValueSerializerTestWithHostArrayBufferView, RoundTripUint8ArrayInput) { // mostly checks that the logic to embed it in structured clone serialization // works correctly. +// A simple module which exports an "increment" function. +// Copied from test/mjsunit/wasm/incrementer.wasm. +const unsigned char kIncrementerWasm[] = { + 0, 97, 115, 109, 1, 0, 0, 0, 1, 6, 1, 96, 1, 127, 1, 127, + 3, 2, 1, 0, 7, 13, 1, 9, 105, 110, 99, 114, 101, 109, 101, 110, + 116, 0, 0, 10, 9, 1, 7, 0, 32, 0, 65, 1, 106, 11, +}; + class ValueSerializerTestWithWasm : public ValueSerializerTest { + public: + static const char* kUnsupportedSerialization; + + ValueSerializerTestWithWasm() + : serialize_delegate_(&transfer_modules_), + deserialize_delegate_(&transfer_modules_) {} + + void Reset() { + current_serializer_delegate_ = nullptr; + transfer_modules_.clear(); + SetExpectInlineWasm(false); + } + + void EnableTransferSerialization() { + current_serializer_delegate_ = &serialize_delegate_; + } + + void EnableTransferDeserialization() { + current_deserializer_delegate_ = &deserialize_delegate_; + } + + void EnableThrowingSerializer() { + current_serializer_delegate_ = &throwing_serializer_; + } + + void EnableDefaultDeserializer() { + current_deserializer_delegate_ = &default_deserializer_; + } + protected: static void SetUpTestCase() { g_saved_flag = i::FLAG_expose_wasm; @@ -2608,32 +2652,243 @@ class ValueSerializerTestWithWasm : public ValueSerializerTest { g_saved_flag = false; } + class ThrowingSerializer : public ValueSerializer::Delegate { + public: + Maybe GetWasmModuleTransferId( + Isolate* isolate, Local module) override { + isolate->ThrowException(Exception::Error( + String::NewFromOneByte( + isolate, + reinterpret_cast(kUnsupportedSerialization), + NewStringType::kNormal) + .ToLocalChecked())); + return Nothing(); + } + + void ThrowDataCloneError(Local message) override { UNREACHABLE(); } + }; + + class SerializeToTransfer : public ValueSerializer::Delegate { + public: + SerializeToTransfer( + std::vector* modules) + : modules_(modules) {} + Maybe GetWasmModuleTransferId( + Isolate* isolate, Local module) override { + modules_->push_back(module->GetTransferrableModule()); + return Just(static_cast(modules_->size()) - 1); + } + + void ThrowDataCloneError(Local message) override { UNREACHABLE(); } + + private: + std::vector* modules_; + }; + + class DeserializeFromTransfer : public ValueDeserializer::Delegate { + public: + DeserializeFromTransfer( + std::vector* modules) + : modules_(modules) {} + + MaybeLocal GetWasmModuleFromId(Isolate* isolate, + uint32_t id) override { + return WasmCompiledModule::FromTransferrableModule(isolate, + modules_->at(id)); + } + + private: + std::vector* modules_; + }; + + ValueSerializer::Delegate* GetSerializerDelegate() override { + return current_serializer_delegate_; + } + + ValueDeserializer::Delegate* GetDeserializerDelegate() override { + return current_deserializer_delegate_; + } + + Local MakeWasm() { + return WasmCompiledModule::DeserializeOrCompile( + isolate(), {nullptr, 0}, + {kIncrementerWasm, sizeof(kIncrementerWasm)}) + .ToLocalChecked(); + } + + void ExpectPass() { + RoundTripTest( + [this]() { return MakeWasm(); }, + [this](Local value) { + ASSERT_TRUE(value->IsWebAssemblyCompiledModule()); + EXPECT_TRUE(EvaluateScriptForResultBool( + "new WebAssembly.Instance(result).exports.increment(8) === 9")); + }); + } + + void ExpectFail() { + EncodeTest( + [this]() { return MakeWasm(); }, + [this](const std::vector& data) { InvalidDecodeTest(data); }); + } + + Local GetComplexObjectWithDuplicate() { + Local wasm_module = MakeWasm(); + serialization_context() + ->Global() + ->CreateDataProperty(serialization_context(), + StringFromUtf8("wasm_module"), wasm_module) + .FromMaybe(false); + Local