272e5a8766
The wasm serialization format only contains TurboFan code. All other functions are only represented by placeholders. With this CL serialization fails if the serialized module does not contain any TurboFan functions and would therefore consist only of placeholders. This is a defense in depth approach, because ideally serialization only gets triggered when TurboFan code is available. However, in some scenarios like debugging it can happen that modules without TurboFan code get serialized. Bug: v8:12281 Change-Id: Ib05430ff89eb2317da80fc0d086ce1d7ab0e919d Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3212510 Commit-Queue: Andreas Haas <ahaas@chromium.org> Reviewed-by: Jakob Kummerow <jkummerow@chromium.org> Cr-Commit-Position: refs/heads/main@{#77715}
367 lines
13 KiB
C++
367 lines
13 KiB
C++
// Copyright 2015 the V8 project authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "include/v8-wasm.h"
|
|
#include "src/api/api-inl.h"
|
|
#include "src/objects/objects-inl.h"
|
|
#include "src/snapshot/code-serializer.h"
|
|
#include "src/utils/version.h"
|
|
#include "src/wasm/module-decoder.h"
|
|
#include "src/wasm/wasm-engine.h"
|
|
#include "src/wasm/wasm-module-builder.h"
|
|
#include "src/wasm/wasm-module.h"
|
|
#include "src/wasm/wasm-objects-inl.h"
|
|
#include "src/wasm/wasm-opcodes.h"
|
|
#include "src/wasm/wasm-serialization.h"
|
|
#include "test/cctest/cctest.h"
|
|
#include "test/common/wasm/flag-utils.h"
|
|
#include "test/common/wasm/test-signatures.h"
|
|
#include "test/common/wasm/wasm-macro-gen.h"
|
|
#include "test/common/wasm/wasm-module-runner.h"
|
|
|
|
namespace v8 {
|
|
namespace internal {
|
|
namespace wasm {
|
|
namespace test_wasm_serialization {
|
|
|
|
// Approximate gtest TEST_F style, in case we adopt gtest.
|
|
class WasmSerializationTest {
|
|
public:
|
|
WasmSerializationTest() : zone_(&allocator_, ZONE_NAME) {
|
|
// Don't call here if we move to gtest.
|
|
SetUp();
|
|
}
|
|
|
|
static void BuildWireBytes(Zone* zone, ZoneBuffer* buffer) {
|
|
WasmModuleBuilder* builder = zone->New<WasmModuleBuilder>(zone);
|
|
TestSignatures sigs;
|
|
|
|
WasmFunctionBuilder* f = builder->AddFunction(sigs.i_i());
|
|
byte code[] = {WASM_LOCAL_GET(0), kExprI32Const, 1, kExprI32Add, kExprEnd};
|
|
f->EmitCode(code, sizeof(code));
|
|
builder->AddExport(base::CStrVector(kFunctionName), f);
|
|
|
|
builder->WriteTo(buffer);
|
|
}
|
|
|
|
void ClearSerializedData() { serialized_bytes_ = {nullptr, 0}; }
|
|
|
|
void InvalidateVersion() {
|
|
uint32_t* slot = reinterpret_cast<uint32_t*>(
|
|
const_cast<uint8_t*>(serialized_bytes_.data()) +
|
|
WasmSerializer::kVersionHashOffset);
|
|
*slot = Version::Hash() + 1;
|
|
}
|
|
|
|
void InvalidateWireBytes() {
|
|
memset(const_cast<uint8_t*>(wire_bytes_.data()), 0, wire_bytes_.size() / 2);
|
|
}
|
|
|
|
MaybeHandle<WasmModuleObject> Deserialize(
|
|
base::Vector<const char> source_url = {}) {
|
|
return DeserializeNativeModule(CcTest::i_isolate(),
|
|
base::VectorOf(serialized_bytes_),
|
|
base::VectorOf(wire_bytes_), source_url);
|
|
}
|
|
|
|
void DeserializeAndRun() {
|
|
ErrorThrower thrower(CcTest::i_isolate(), "");
|
|
Handle<WasmModuleObject> module_object;
|
|
CHECK(Deserialize().ToHandle(&module_object));
|
|
{
|
|
DisallowGarbageCollection assume_no_gc;
|
|
base::Vector<const byte> deserialized_module_wire_bytes =
|
|
module_object->native_module()->wire_bytes();
|
|
CHECK_EQ(deserialized_module_wire_bytes.size(), wire_bytes_.size());
|
|
CHECK_EQ(memcmp(deserialized_module_wire_bytes.begin(),
|
|
wire_bytes_.data(), wire_bytes_.size()),
|
|
0);
|
|
}
|
|
Handle<WasmInstanceObject> instance =
|
|
GetWasmEngine()
|
|
->SyncInstantiate(CcTest::i_isolate(), &thrower, module_object,
|
|
Handle<JSReceiver>::null(),
|
|
MaybeHandle<JSArrayBuffer>())
|
|
.ToHandleChecked();
|
|
Handle<Object> params[1] = {
|
|
Handle<Object>(Smi::FromInt(41), CcTest::i_isolate())};
|
|
int32_t result = testing::CallWasmFunctionForTesting(
|
|
CcTest::i_isolate(), instance, kFunctionName, 1, params);
|
|
CHECK_EQ(42, result);
|
|
}
|
|
|
|
void CollectGarbage() {
|
|
// Try hard to collect all garbage and will therefore also invoke all weak
|
|
// callbacks of actually unreachable persistent handles.
|
|
CcTest::i_isolate()->heap()->CollectAllAvailableGarbage(
|
|
GarbageCollectionReason::kTesting);
|
|
}
|
|
|
|
private:
|
|
static const char* kFunctionName;
|
|
|
|
Zone* zone() { return &zone_; }
|
|
|
|
void SetUp() {
|
|
CcTest::InitIsolateOnce();
|
|
ZoneBuffer buffer(&zone_);
|
|
WasmSerializationTest::BuildWireBytes(zone(), &buffer);
|
|
|
|
v8::Isolate::CreateParams create_params;
|
|
create_params.array_buffer_allocator =
|
|
CcTest::i_isolate()->array_buffer_allocator();
|
|
|
|
v8::Isolate* serialization_v8_isolate = v8::Isolate::New(create_params);
|
|
Isolate* serialization_isolate =
|
|
reinterpret_cast<Isolate*>(serialization_v8_isolate);
|
|
ErrorThrower thrower(serialization_isolate, "");
|
|
// Keep a weak pointer so we can check that the native module dies after
|
|
// serialization (when the isolate is disposed).
|
|
std::weak_ptr<NativeModule> weak_native_module;
|
|
{
|
|
HandleScope scope(serialization_isolate);
|
|
v8::Local<v8::Context> serialization_context =
|
|
v8::Context::New(serialization_v8_isolate);
|
|
serialization_context->Enter();
|
|
|
|
auto enabled_features = WasmFeatures::FromIsolate(serialization_isolate);
|
|
MaybeHandle<WasmModuleObject> maybe_module_object =
|
|
GetWasmEngine()->SyncCompile(
|
|
serialization_isolate, enabled_features, &thrower,
|
|
ModuleWireBytes(buffer.begin(), buffer.end()));
|
|
Handle<WasmModuleObject> module_object =
|
|
maybe_module_object.ToHandleChecked();
|
|
weak_native_module = module_object->shared_native_module();
|
|
// Check that the native module exists at this point.
|
|
CHECK(weak_native_module.lock());
|
|
|
|
auto* native_module = module_object->native_module();
|
|
native_module->compilation_state()->WaitForTopTierFinished();
|
|
DCHECK(!native_module->compilation_state()->failed());
|
|
|
|
v8::Local<v8::Object> v8_module_obj =
|
|
v8::Utils::ToLocal(Handle<JSObject>::cast(module_object));
|
|
CHECK(v8_module_obj->IsWasmModuleObject());
|
|
|
|
v8::Local<v8::WasmModuleObject> v8_module_object =
|
|
v8_module_obj.As<v8::WasmModuleObject>();
|
|
v8::CompiledWasmModule compiled_module =
|
|
v8_module_object->GetCompiledModule();
|
|
v8::MemorySpan<const uint8_t> uncompiled_bytes =
|
|
compiled_module.GetWireBytesRef();
|
|
uint8_t* bytes_copy = zone()->NewArray<uint8_t>(uncompiled_bytes.size());
|
|
memcpy(bytes_copy, uncompiled_bytes.data(), uncompiled_bytes.size());
|
|
wire_bytes_ = {bytes_copy, uncompiled_bytes.size()};
|
|
// keep alive data_ until the end
|
|
data_ = compiled_module.Serialize();
|
|
CHECK_LT(0, data_.size);
|
|
}
|
|
// Dispose of serialization isolate to destroy the reference to the
|
|
// NativeModule, which removes it from the module cache in the wasm engine
|
|
// and forces de-serialization in the new isolate.
|
|
serialization_v8_isolate->Dispose();
|
|
|
|
// Busy-wait for the NativeModule to really die. Background threads might
|
|
// temporarily keep it alive (happens very rarely, see
|
|
// https://crbug.com/v8/10148).
|
|
while (weak_native_module.lock()) {
|
|
}
|
|
|
|
serialized_bytes_ = {data_.buffer.get(), data_.size};
|
|
|
|
v8::HandleScope new_scope(CcTest::isolate());
|
|
v8::Local<v8::Context> deserialization_context =
|
|
v8::Context::New(CcTest::isolate());
|
|
deserialization_context->Enter();
|
|
}
|
|
|
|
v8::internal::AccountingAllocator allocator_;
|
|
Zone zone_;
|
|
v8::OwnedBuffer data_;
|
|
v8::MemorySpan<const uint8_t> wire_bytes_ = {nullptr, 0};
|
|
v8::MemorySpan<const uint8_t> serialized_bytes_ = {nullptr, 0};
|
|
};
|
|
|
|
const char* WasmSerializationTest::kFunctionName = "increment";
|
|
|
|
TEST(DeserializeValidModule) {
|
|
WasmSerializationTest test;
|
|
{
|
|
HandleScope scope(CcTest::i_isolate());
|
|
test.DeserializeAndRun();
|
|
}
|
|
test.CollectGarbage();
|
|
}
|
|
|
|
TEST(DeserializeWithSourceUrl) {
|
|
WasmSerializationTest test;
|
|
{
|
|
HandleScope scope(CcTest::i_isolate());
|
|
const std::string url = "http://example.com/example.wasm";
|
|
Handle<WasmModuleObject> module_object;
|
|
CHECK(test.Deserialize(base::VectorOf(url)).ToHandle(&module_object));
|
|
String url_str = String::cast(module_object->script().name());
|
|
CHECK_EQ(url, url_str.ToCString().get());
|
|
}
|
|
test.CollectGarbage();
|
|
}
|
|
|
|
TEST(DeserializeMismatchingVersion) {
|
|
WasmSerializationTest test;
|
|
{
|
|
HandleScope scope(CcTest::i_isolate());
|
|
test.InvalidateVersion();
|
|
CHECK(test.Deserialize().is_null());
|
|
}
|
|
test.CollectGarbage();
|
|
}
|
|
|
|
TEST(DeserializeNoSerializedData) {
|
|
WasmSerializationTest test;
|
|
{
|
|
HandleScope scope(CcTest::i_isolate());
|
|
test.ClearSerializedData();
|
|
CHECK(test.Deserialize().is_null());
|
|
}
|
|
test.CollectGarbage();
|
|
}
|
|
|
|
TEST(DeserializeWireBytesAndSerializedDataInvalid) {
|
|
WasmSerializationTest test;
|
|
{
|
|
HandleScope scope(CcTest::i_isolate());
|
|
test.InvalidateVersion();
|
|
test.InvalidateWireBytes();
|
|
CHECK(test.Deserialize().is_null());
|
|
}
|
|
test.CollectGarbage();
|
|
}
|
|
|
|
bool False(v8::Local<v8::Context> context, v8::Local<v8::String> source) {
|
|
return false;
|
|
}
|
|
|
|
TEST(BlockWasmCodeGenAtDeserialization) {
|
|
WasmSerializationTest test;
|
|
{
|
|
HandleScope scope(CcTest::i_isolate());
|
|
CcTest::isolate()->SetAllowWasmCodeGenerationCallback(False);
|
|
CHECK(test.Deserialize().is_null());
|
|
}
|
|
test.CollectGarbage();
|
|
}
|
|
|
|
UNINITIALIZED_TEST(CompiledWasmModulesTransfer) {
|
|
v8::internal::AccountingAllocator allocator;
|
|
Zone zone(&allocator, ZONE_NAME);
|
|
|
|
ZoneBuffer buffer(&zone);
|
|
WasmSerializationTest::BuildWireBytes(&zone, &buffer);
|
|
|
|
v8::Isolate::CreateParams create_params;
|
|
create_params.array_buffer_allocator = CcTest::array_buffer_allocator();
|
|
v8::Isolate* from_isolate = v8::Isolate::New(create_params);
|
|
std::vector<v8::CompiledWasmModule> store;
|
|
std::shared_ptr<NativeModule> original_native_module;
|
|
{
|
|
v8::HandleScope scope(from_isolate);
|
|
LocalContext env(from_isolate);
|
|
|
|
Isolate* from_i_isolate = reinterpret_cast<Isolate*>(from_isolate);
|
|
testing::SetupIsolateForWasmModule(from_i_isolate);
|
|
ErrorThrower thrower(from_i_isolate, "TestCompiledWasmModulesTransfer");
|
|
auto enabled_features = WasmFeatures::FromIsolate(from_i_isolate);
|
|
MaybeHandle<WasmModuleObject> maybe_module_object =
|
|
GetWasmEngine()->SyncCompile(
|
|
from_i_isolate, enabled_features, &thrower,
|
|
ModuleWireBytes(buffer.begin(), buffer.end()));
|
|
Handle<WasmModuleObject> module_object =
|
|
maybe_module_object.ToHandleChecked();
|
|
v8::Local<v8::WasmModuleObject> v8_module =
|
|
v8::Local<v8::WasmModuleObject>::Cast(
|
|
v8::Utils::ToLocal(Handle<JSObject>::cast(module_object)));
|
|
store.push_back(v8_module->GetCompiledModule());
|
|
original_native_module = module_object->shared_native_module();
|
|
}
|
|
|
|
{
|
|
v8::Isolate* to_isolate = v8::Isolate::New(create_params);
|
|
{
|
|
v8::HandleScope scope(to_isolate);
|
|
LocalContext env(to_isolate);
|
|
|
|
v8::MaybeLocal<v8::WasmModuleObject> transferred_module =
|
|
v8::WasmModuleObject::FromCompiledModule(to_isolate, store[0]);
|
|
CHECK(!transferred_module.IsEmpty());
|
|
Handle<WasmModuleObject> module_object = Handle<WasmModuleObject>::cast(
|
|
v8::Utils::OpenHandle(*transferred_module.ToLocalChecked()));
|
|
std::shared_ptr<NativeModule> transferred_native_module =
|
|
module_object->shared_native_module();
|
|
CHECK_EQ(original_native_module, transferred_native_module);
|
|
}
|
|
to_isolate->Dispose();
|
|
}
|
|
original_native_module.reset();
|
|
from_isolate->Dispose();
|
|
}
|
|
|
|
TEST(TierDownAfterDeserialization) {
|
|
WasmSerializationTest test;
|
|
|
|
Isolate* isolate = CcTest::i_isolate();
|
|
HandleScope scope(isolate);
|
|
Handle<WasmModuleObject> module_object;
|
|
CHECK(test.Deserialize().ToHandle(&module_object));
|
|
|
|
auto* native_module = module_object->native_module();
|
|
CHECK_EQ(1, native_module->module()->functions.size());
|
|
WasmCodeRefScope code_ref_scope;
|
|
auto* turbofan_code = native_module->GetCode(0);
|
|
CHECK_NOT_NULL(turbofan_code);
|
|
CHECK_EQ(ExecutionTier::kTurbofan, turbofan_code->tier());
|
|
|
|
GetWasmEngine()->TierDownAllModulesPerIsolate(isolate);
|
|
|
|
auto* liftoff_code = native_module->GetCode(0);
|
|
CHECK_EQ(ExecutionTier::kLiftoff, liftoff_code->tier());
|
|
}
|
|
|
|
TEST(SerializeLiftoffModuleFails) {
|
|
// Make sure that no function is tiered up to TurboFan.
|
|
if (!FLAG_liftoff) return;
|
|
FlagScope<bool> no_tier_up(&FLAG_wasm_tier_up, false);
|
|
v8::internal::AccountingAllocator allocator;
|
|
Zone zone(&allocator, "test_zone");
|
|
|
|
CcTest::InitIsolateOnce();
|
|
Isolate* isolate = CcTest::i_isolate();
|
|
HandleScope scope(isolate);
|
|
ZoneBuffer wire_bytes_buffer(&zone);
|
|
WasmSerializationTest::BuildWireBytes(&zone, &wire_bytes_buffer);
|
|
|
|
ErrorThrower thrower(isolate, "Test");
|
|
MaybeHandle<WasmModuleObject> maybe_module_object =
|
|
GetWasmEngine()->SyncCompile(
|
|
isolate, WasmFeatures::All(), &thrower,
|
|
ModuleWireBytes(wire_bytes_buffer.begin(), wire_bytes_buffer.end()));
|
|
Handle<WasmModuleObject> module_object =
|
|
maybe_module_object.ToHandleChecked();
|
|
|
|
NativeModule* native_module = module_object->native_module();
|
|
WasmSerializer wasm_serializer(native_module);
|
|
size_t buffer_size = wasm_serializer.GetSerializedNativeModuleSize();
|
|
std::unique_ptr<uint8_t[]> buffer(new uint8_t[buffer_size]);
|
|
// Serialization is expected to fail if there is no TurboFan function to
|
|
// serialize.
|
|
CHECK(!wasm_serializer.SerializeNativeModule({buffer.get(), buffer_size}));
|
|
}
|
|
|
|
} // namespace test_wasm_serialization
|
|
} // namespace wasm
|
|
} // namespace internal
|
|
} // namespace v8
|