Files
Kmake/deps/v8/test/cctest/wasm/test-wasm-serialization.cc
2026-05-26 23:36:42 -07:00

805 lines
31 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/cctest/heap/heap-utils.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::internal::wasm {
// 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 constexpr const char* kFunctionName = "increment";
static void BuildWireBytes(Zone* zone, ZoneBuffer* buffer) {
WasmModuleBuilder* builder = zone->New<WasmModuleBuilder>(zone);
TestSignatures sigs;
// Generate 3 functions, and export the last one with the name "increment".
WasmFunctionBuilder* f;
for (int i = 0; i < 3; ++i) {
f = builder->AddFunction(sigs.i_i());
f->EmitCode({WASM_LOCAL_GET(0), kExprI32Const, 1, kExprI32Add, kExprEnd});
}
builder->AddExport(base::CStrVector(kFunctionName), f);
builder->WriteTo(buffer);
}
void ClearSerializedData() { serialized_bytes_ = {}; }
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);
}
void PartlyDropTieringBudget() {
serialized_bytes_ = {serialized_bytes_.data(),
serialized_bytes_.size() - 1};
}
MaybeDirectHandle<WasmModuleObject> Deserialize(
base::Vector<const char> source_url = {}) {
return DeserializeNativeModule(
CcTest::i_isolate(), base::VectorOf(serialized_bytes_),
base::VectorOf(wire_bytes_), compile_imports_, source_url);
}
void DeserializeAndRun() {
ErrorThrower thrower(CcTest::i_isolate(), "");
DirectHandle<WasmModuleObject> module_object;
CHECK(Deserialize().ToHandle(&module_object));
{
DisallowGarbageCollection assume_no_gc;
base::Vector<const uint8_t> 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);
}
DirectHandle<WasmInstanceObject> instance =
GetWasmEngine()
->SyncInstantiate(CcTest::i_isolate(), &thrower, module_object,
DirectHandle<JSReceiver>::null(),
MaybeDirectHandle<JSArrayBuffer>())
.ToHandleChecked();
DirectHandle<Object> params[] = {
direct_handle(Smi::FromInt(41), CcTest::i_isolate())};
int32_t result = testing::CallWasmFunctionForTesting(
CcTest::i_isolate(), instance, kFunctionName,
base::ArrayVector(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.
heap::InvokeMemoryReducingMajorGCs(CcTest::heap());
}
v8::MemorySpan<const uint8_t> wire_bytes() const { return wire_bytes_; }
CompileTimeImports MakeCompileTimeImports() { return CompileTimeImports{}; }
private:
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;
{
v8::Isolate::Scope isolate_scope(serialization_v8_isolate);
HandleScope scope(serialization_isolate);
v8::Local<v8::Context> serialization_context =
v8::Context::New(serialization_v8_isolate);
serialization_context->Enter();
auto enabled_features =
WasmEnabledFeatures::FromIsolate(serialization_isolate);
MaybeDirectHandle<WasmModuleObject> maybe_module_object =
GetWasmEngine()->SyncCompile(serialization_isolate, enabled_features,
MakeCompileTimeImports(), &thrower,
base::OwnedCopyOf(buffer));
DirectHandle<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());
v8::Local<v8::Object> v8_module_obj =
v8::Utils::ToLocal(Cast<JSObject>(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()->AllocateArray<uint8_t>(uncompiled_bytes.size());
memcpy(bytes_copy, uncompiled_bytes.data(), uncompiled_bytes.size());
wire_bytes_ = {bytes_copy, uncompiled_bytes.size()};
// Run the code until tier-up (of the single function) was observed.
DirectHandle<WasmInstanceObject> instance =
GetWasmEngine()
->SyncInstantiate(serialization_isolate, &thrower, module_object,
{}, {})
.ToHandleChecked();
CHECK_EQ(0, data_.size);
while (data_.size == 0) {
testing::CallWasmFunctionForTesting(serialization_isolate, instance,
kFunctionName, {});
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_;
// TODO(14179): Add tests for de/serializing modules with compile-time
// imports.
CompileTimeImports compile_imports_;
v8::OwnedBuffer data_;
v8::MemorySpan<const uint8_t> wire_bytes_ = {nullptr, 0};
v8::MemorySpan<const uint8_t> serialized_bytes_ = {nullptr, 0};
FlagScope<int> tier_up_quickly_{&v8_flags.wasm_tiering_budget, 1000};
};
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";
DirectHandle<WasmModuleObject> module_object;
CHECK(test.Deserialize(base::VectorOf(url)).ToHandle(&module_object));
Tagged<String> url_str = Cast<String>(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::Isolate::Scope isolate_scope(from_isolate);
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 = WasmEnabledFeatures::FromIsolate(from_i_isolate);
MaybeDirectHandle<WasmModuleObject> maybe_module_object =
GetWasmEngine()->SyncCompile(from_i_isolate, enabled_features,
CompileTimeImports{}, &thrower,
base::OwnedCopyOf(buffer));
DirectHandle<WasmModuleObject> module_object =
maybe_module_object.ToHandleChecked();
v8::Local<v8::WasmModuleObject> v8_module =
v8::Local<v8::WasmModuleObject>::Cast(
v8::Utils::ToLocal(Cast<JSObject>(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::Isolate::Scope isolate_scope(to_isolate);
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());
DirectHandle<WasmModuleObject> module_object = Cast<WasmModuleObject>(
v8::Utils::OpenDirectHandle(*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);
DirectHandle<WasmModuleObject> module_object;
CHECK(test.Deserialize().ToHandle(&module_object));
auto* native_module = module_object->native_module();
CHECK_EQ(3, native_module->module()->functions.size());
WasmCodeRefScope code_ref_scope;
// The deserialized code must be TurboFan (we wait for tier-up before
// serializing).
auto* turbofan_code = native_module->GetCode(2);
CHECK_NOT_NULL(turbofan_code);
CHECK_EQ(ExecutionTier::kTurbofan, turbofan_code->tier());
GetWasmEngine()->EnterDebuggingForIsolate(isolate);
// Entering debugging should delete all code, so that debug code gets compiled
// lazily.
CHECK_NULL(native_module->GetCode(0));
}
TEST(SerializeLiftoffModuleFails) {
// Make sure that no function is tiered up to TurboFan.
if (!v8_flags.liftoff) return;
FlagScope<bool> no_tier_up(&v8_flags.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");
MaybeDirectHandle<WasmModuleObject> maybe_module_object =
GetWasmEngine()->SyncCompile(isolate, WasmEnabledFeatures::All(),
CompileTimeImports{}, &thrower,
base::OwnedCopyOf(wire_bytes_buffer));
DirectHandle<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}));
}
TEST(SerializeTieringBudget) {
WasmSerializationTest test;
Isolate* isolate = CcTest::i_isolate();
v8::OwnedBuffer serialized_bytes;
uint32_t mock_budget[3]{1, 2, 3};
{
HandleScope scope(isolate);
DirectHandle<WasmModuleObject> module_object;
CHECK(test.Deserialize().ToHandle(&module_object));
auto* native_module = module_object->native_module();
memcpy(native_module->tiering_budget_array(), mock_budget,
arraysize(mock_budget) * sizeof(uint32_t));
v8::Local<v8::Object> v8_module_obj =
v8::Utils::ToLocal(Cast<JSObject>(module_object));
CHECK(v8_module_obj->IsWasmModuleObject());
v8::Local<v8::WasmModuleObject> v8_module_object =
v8_module_obj.As<v8::WasmModuleObject>();
serialized_bytes = v8_module_object->GetCompiledModule().Serialize();
// Change one entry in the tiering budget after serialization to make sure
// the module gets deserialized and not just loaded from the module cache.
native_module->tiering_budget_array()[0]++;
}
// We need to invoke GC without stack, otherwise some objects may survive.
DisableConservativeStackScanningScopeForTesting no_stack_scanning(
isolate->heap());
test.CollectGarbage();
HandleScope scope(isolate);
DirectHandle<WasmModuleObject> module_object;
CompileTimeImports compile_imports = test.MakeCompileTimeImports();
CHECK(
DeserializeNativeModule(
isolate,
base::VectorOf(serialized_bytes.buffer.get(), serialized_bytes.size),
base::VectorOf(test.wire_bytes()), compile_imports, {})
.ToHandle(&module_object));
auto* native_module = module_object->native_module();
for (size_t i = 0; i < arraysize(mock_budget); ++i) {
CHECK_EQ(mock_budget[i], native_module->tiering_budget_array()[i]);
}
}
TEST(DeserializeTieringBudgetPartlyMissing) {
WasmSerializationTest test;
{
HandleScope scope(CcTest::i_isolate());
test.PartlyDropTieringBudget();
CHECK(test.Deserialize().is_null());
}
test.CollectGarbage();
}
TEST(SerializationFailsOnChangedFlags) {
WasmSerializationTest test;
{
HandleScope scope(CcTest::i_isolate());
FlagScope<bool> no_bounds_checks(&v8_flags.wasm_bounds_checks, false);
CHECK(test.Deserialize().is_null());
FlagScope<bool> bounds_checks(&v8_flags.wasm_bounds_checks, true);
CHECK(!test.Deserialize().is_null());
}
}
TEST(SerializationFailsOnChangedFeatures) {
WasmSerializationTest test;
{
HandleScope scope(CcTest::i_isolate());
CcTest::isolate()->SetWasmImportedStringsEnabledCallback(
[](auto) { return true; });
CHECK(test.Deserialize().is_null());
CcTest::isolate()->SetWasmImportedStringsEnabledCallback(
[](auto) { return false; });
CHECK(!test.Deserialize().is_null());
}
}
TEST(DeserializeIndirectCallWithDifferentCanonicalId) {
// This test compiles and serializes a module with an indirect call, then
// resets the type canonicalizer, compiles another module, and then
// deserializes the original module. This ensures that a different canonical
// signature ID is used for the indirect call.
// We then call the deserialized module to check that the right canonical
// signature ID is being used.
// Compile with Turbofan right away.
FlagScope<bool> no_liftoff{&v8_flags.liftoff, false};
FlagScope<bool> no_lazy_compilation{&v8_flags.wasm_lazy_compilation, false};
FlagScope<bool> expose_gc{&v8_flags.expose_gc, true};
i::Isolate* i_isolate = CcTest::InitIsolateOnce();
v8::Isolate* v8_isolate = CcTest::isolate();
v8::internal::AccountingAllocator allocator;
Zone zone(&allocator, ZONE_NAME);
HandleScope handle_scope(i_isolate);
// Build a small module with an indirect call.
ZoneBuffer zone_buffer(&zone);
{
WasmModuleBuilder builder{&zone};
TestSignatures sigs;
// Add the "call_indirect" function which calls table0[0].
ModuleTypeIndex sig_id = builder.AddSignature(sigs.i_i(), true);
WasmFunctionBuilder* f = builder.AddFunction(sig_id);
f->EmitCode({// (i) => i != 0 ? f(i-1) : 42
WASM_IF_ELSE_I(
// cond:
WASM_LOCAL_GET(0),
// if_true:
WASM_CALL_INDIRECT(
SIG_INDEX(sig_id.index),
WASM_I32_SUB(WASM_LOCAL_GET(0), WASM_ONE), WASM_ZERO),
// if_false:
WASM_I32V_1(42)),
WASM_END});
builder.AddExport(base::CStrVector("call_indirect"), f);
// Add a function table.
uint32_t table_id = builder.AddTable(kWasmFuncRef, 1);
builder.SetIndirectFunction(
table_id, 0, f->func_index(),
WasmModuleBuilder::WasmElemSegment::kRelativeToImports);
// Write the final module into {buffer}.
builder.WriteTo(&zone_buffer);
}
// Compile the module and serialize it.
// Keep a weak pointer so we can check that the original native module died.
auto enabled_features = WasmEnabledFeatures::FromIsolate(i_isolate);
std::weak_ptr<NativeModule> weak_native_module;
v8::OwnedBuffer serialized_module;
CanonicalTypeIndex canonical_sig_id_before_serialization;
{
ErrorThrower thrower(i_isolate, "");
{
v8::Isolate::Scope isolate_scope(v8_isolate);
HandleScope scope(i_isolate);
v8::Local<v8::Context> serialization_context =
v8::Context::New(v8_isolate);
serialization_context->Enter();
DirectHandle<WasmModuleObject> module_object =
GetWasmEngine()
->SyncCompile(i_isolate, enabled_features, CompileTimeImports{},
&thrower, base::OwnedCopyOf(zone_buffer))
.ToHandleChecked();
weak_native_module = module_object->shared_native_module();
// Retrieve the canonicalized signature ID.
const std::vector<CanonicalTypeIndex>& canonical_type_ids =
module_object->native_module()
->module()
->isorecursive_canonical_type_ids;
CHECK_EQ(1, canonical_type_ids.size());
canonical_sig_id_before_serialization = canonical_type_ids[0];
// Check that the embedded constant in the code is right.
WasmCodeRefScope code_ref_scope;
WasmCode* code = module_object->native_module()->GetCode(0);
RelocIterator reloc_it{
code->instructions(), code->reloc_info(), code->constant_pool(),
RelocInfo::ModeMask(RelocInfo::WASM_CANONICAL_SIG_ID)};
CHECK(!reloc_it.done());
CHECK_EQ(canonical_sig_id_before_serialization.index,
reloc_it.rinfo()->wasm_canonical_sig_id());
reloc_it.next();
CHECK(reloc_it.done());
// Convert to API objects and serialize.
v8::Local<v8::WasmModuleObject> v8_module_object =
v8::Utils::ToLocal(module_object);
serialized_module = v8_module_object->GetCompiledModule().Serialize();
}
CHECK_LT(0, serialized_module.size);
// Run GC until the NativeModule died. Add a manual timeout of 60 seconds to
// get a better error message than just a test timeout if this fails.
const auto start_time = std::chrono::steady_clock::now();
const auto end_time = start_time + std::chrono::seconds(60);
while (weak_native_module.lock()) {
// We need to invoke GC without stack, otherwise the native module may
// survive.
DisableConservativeStackScanningScopeForTesting no_stack_scanning(
i_isolate->heap());
v8_isolate->RequestGarbageCollectionForTesting(
v8::Isolate::kFullGarbageCollection);
if (std::chrono::steady_clock::now() > end_time) {
FATAL("NativeModule did not die within 60 seconds");
}
}
}
// Clear canonicalized types, then compile another module which adds a
// canonical type at the same index we used in the previous module.
GetTypeCanonicalizer()->EmptyStorageForTesting();
{
ZoneBuffer buffer(&zone);
WasmModuleBuilder builder{&zone};
TestSignatures sigs;
ModuleTypeIndex sig_id = builder.AddSignature(sigs.v_v(), true);
WasmFunctionBuilder* f = builder.AddFunction(sig_id);
f->EmitByte(kExprEnd);
builder.WriteTo(&buffer);
ErrorThrower thrower(i_isolate, "");
GetWasmEngine()
->SyncCompile(i_isolate, enabled_features, CompileTimeImports{},
&thrower, base::OwnedCopyOf(buffer))
.ToHandleChecked();
}
// Now deserialize the previous module.
CanonicalTypeIndex canonical_sig_id_after_deserialization{
canonical_sig_id_before_serialization.index + 1};
{
v8::Local<v8::Context> deserialization_context =
v8::Context::New(CcTest::isolate());
deserialization_context->Enter();
ErrorThrower thrower(CcTest::i_isolate(), "");
base::Vector<const char> kNoSourceUrl;
DirectHandle<WasmModuleObject> module_object =
DeserializeNativeModule(CcTest::i_isolate(),
base::VectorOf(serialized_module.buffer.get(),
serialized_module.size),
base::VectorOf(zone_buffer),
CompileTimeImports{}, kNoSourceUrl)
.ToHandleChecked();
// Check that the signature ID got canonicalized to index 1.
const std::vector<CanonicalTypeIndex>& canonical_type_ids =
module_object->native_module()
->module()
->isorecursive_canonical_type_ids;
CHECK_EQ(1, canonical_type_ids.size());
CHECK_EQ(canonical_sig_id_after_deserialization, canonical_type_ids[0]);
// Check that the embedded constant in the code is right.
WasmCodeRefScope code_ref_scope;
WasmCode* code = module_object->native_module()->GetCode(0);
RelocIterator reloc_it{
code->instructions(), code->reloc_info(), code->constant_pool(),
RelocInfo::ModeMask(RelocInfo::WASM_CANONICAL_SIG_ID)};
CHECK(!reloc_it.done());
CHECK_EQ(canonical_sig_id_after_deserialization.index,
reloc_it.rinfo()->wasm_canonical_sig_id());
reloc_it.next();
CHECK(reloc_it.done());
// Now call the function.
DirectHandle<WasmInstanceObject> instance =
GetWasmEngine()
->SyncInstantiate(CcTest::i_isolate(), &thrower, module_object,
DirectHandle<JSReceiver>::null(),
MaybeDirectHandle<JSArrayBuffer>())
.ToHandleChecked();
DirectHandle<Object> params[] = {direct_handle(Smi::FromInt(1), i_isolate)};
int32_t result = testing::CallWasmFunctionForTesting(
i_isolate, instance, "call_indirect", base::ArrayVector(params));
CHECK_EQ(42, result);
}
}
// Regression test for https://crbug.com/372840600 /
// https://crbug.com/369793713 / https://crbug.com/369869947.
TEST(SerializeDetectedFeatures) {
// This test compiles and serializes a module which uses a use-counter-tracked
// feature (tail calls). We check that the set of detected features is
// preserved across serialization and deserialization. Otherwise we would
// fail a DCHECK in lazy compilation later.
FlagScope<int> tier_up_quickly{&v8_flags.wasm_tiering_budget, 10};
FlagScope<bool> expose_gc{&v8_flags.expose_gc, true};
i::Isolate* i_isolate = CcTest::InitIsolateOnce();
v8::Isolate* v8_isolate = CcTest::isolate();
v8::internal::AccountingAllocator allocator;
Zone zone(&allocator, ZONE_NAME);
HandleScope handle_scope(i_isolate);
// Build a small module with a tail call.
ZoneBuffer buffer(&zone);
{
WasmModuleBuilder builder{&zone};
// Add a function which is tail-called by another one.
ModuleTypeIndex sig_i_v = builder.AddSignature(TestSignatures::i_v(), true);
WasmFunctionBuilder* a = builder.AddFunction(sig_i_v);
a->EmitCode({WASM_I32V_1(11), WASM_END});
builder.AddExport(base::CStrVector("a"), a);
// Add the function which tail-calls the first one.
WasmFunctionBuilder* b = builder.AddFunction(sig_i_v);
b->EmitCode({WASM_RETURN_CALL_FUNCTION0(a->func_index()), WASM_END});
builder.AddExport(base::CStrVector("b"), b);
// Write the final module into {buffer}.
builder.WriteTo(&buffer);
}
// Compile and initialize the module and serialize it.
// Keep a weak pointer so we can check that the original native module died.
auto enabled_features = WasmEnabledFeatures::FromIsolate(i_isolate);
std::weak_ptr<NativeModule> weak_native_module;
v8::OwnedBuffer serialized_module;
{
ErrorThrower thrower(i_isolate, "");
{
v8::Isolate::Scope isolate_scope(v8_isolate);
HandleScope scope(i_isolate);
v8::Local<v8::Context> serialization_context =
v8::Context::New(v8_isolate);
serialization_context->Enter();
DirectHandle<WasmModuleObject> module_object =
GetWasmEngine()
->SyncCompile(i_isolate, enabled_features, CompileTimeImports{},
&thrower, base::OwnedCopyOf(buffer))
.ToHandleChecked();
// Check that "return_call" is in the set of detected features.
CHECK_EQ(WasmDetectedFeatures{{WasmDetectedFeature::return_call}},
module_object->native_module()
->compilation_state()
->detected_features());
weak_native_module = module_object->shared_native_module();
// Now call the tail-calling function "b". This triggers lazy compilation,
// which should not DCHECK because of a new detected feature.
DirectHandle<WasmInstanceObject> instance =
GetWasmEngine()
->SyncInstantiate(CcTest::i_isolate(), &thrower, module_object,
DirectHandle<JSReceiver>::null(),
MaybeDirectHandle<JSArrayBuffer>())
.ToHandleChecked();
v8::Local<v8::WasmModuleObject> v8_module_object =
v8::Utils::ToLocal(module_object);
// Call function "a" until serialization succeeds (once we have TF code).
const auto start_time = std::chrono::steady_clock::now();
const auto end_time = start_time + std::chrono::seconds(60);
while (true) {
int32_t result =
testing::CallWasmFunctionForTesting(i_isolate, instance, "a", {});
CHECK_EQ(11, result);
serialized_module = v8_module_object->GetCompiledModule().Serialize();
if (serialized_module.size != 0) break;
v8_isolate->RequestGarbageCollectionForTesting(
v8::Isolate::kFullGarbageCollection);
if (std::chrono::steady_clock::now() > end_time) {
FATAL("Tier-up didn't complete within 60 seconds");
}
}
}
CHECK_LT(0, serialized_module.size);
// Run GC until the NativeModule died. Add a manual timeout of 60 seconds to
// get a better error message than just a test timeout if this fails.
const auto start_time = std::chrono::steady_clock::now();
const auto end_time = start_time + std::chrono::seconds(60);
while (weak_native_module.lock()) {
// We need to invoke GC without stack, otherwise the native module may
// survive.
DisableConservativeStackScanningScopeForTesting no_stack_scanning(
i_isolate->heap());
v8_isolate->RequestGarbageCollectionForTesting(
v8::Isolate::kFullGarbageCollection);
if (std::chrono::steady_clock::now() > end_time) {
FATAL("NativeModule did not die within 60 seconds");
}
}
}
// Now deserialize the module and check the detected features again.
{
v8::Local<v8::Context> deserialization_context =
v8::Context::New(CcTest::isolate());
deserialization_context->Enter();
ErrorThrower thrower(CcTest::i_isolate(), "");
base::Vector<const char> kNoSourceUrl;
DirectHandle<WasmModuleObject> module_object =
DeserializeNativeModule(CcTest::i_isolate(),
base::VectorOf(serialized_module.buffer.get(),
serialized_module.size),
base::VectorOf(buffer), CompileTimeImports{},
kNoSourceUrl)
.ToHandleChecked();
CHECK_EQ(WasmDetectedFeatures{{WasmDetectedFeature::return_call}},
module_object->native_module()
->compilation_state()
->detected_features());
// Now call the tail-calling function "b". This triggers lazy compilation,
// which should not DCHECK because of a new detected feature.
DirectHandle<WasmInstanceObject> instance =
GetWasmEngine()
->SyncInstantiate(CcTest::i_isolate(), &thrower, module_object,
DirectHandle<JSReceiver>::null(),
MaybeDirectHandle<JSArrayBuffer>())
.ToHandleChecked();
int32_t result =
testing::CallWasmFunctionForTesting(i_isolate, instance, "b", {});
CHECK_EQ(11, result);
}
}
} // namespace v8::internal::wasm