/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
deps/v8/src/wasm/module-decoder-impl.h
3 086 строк
117 KB
Michaël Zasso
deps: update V8 to 14.6.202.33
24 апр 2026, 19:01
Не верифицирован
24 апр 2026, 19:01
f1e0b83
Код
Авторство
О чём код?
// Copyright 2022 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. #ifndef V8_WASM_MODULE_DECODER_IMPL_H_ #define V8_WASM_MODULE_DECODER_IMPL_H_ #if !V8_ENABLE_WEBASSEMBLY #error This header should only be included if WebAssembly is enabled. #endif // !V8_ENABLE_WEBASSEMBLY #include "src/base/platform/wrappers.h" #include "src/strings/unicode.h" #include "src/utils/ostreams.h" #include "src/wasm/canonical-types.h" #include "src/wasm/constant-expression-interface.h" #include "src/wasm/function-body-decoder-impl.h" #include "src/wasm/module-decoder.h" #include "src/wasm/wasm-module.h" #include "src/wasm/wasm-subtyping.h" #include "src/wasm/well-known-imports.h" namespace v8::internal::wasm { #define TRACE(...) \ do { \ if (v8_flags.trace_wasm_decoder) PrintF(__VA_ARGS__); \ } while (false) inline const char* ExternalKindName(ImportExportKindCode kind) { switch (kind) { case kExternalFunction: return "function"; case kExternalTable: return "table"; case kExternalMemory: return "memory"; case kExternalGlobal: return "global"; case kExternalTag: return "tag"; case kExternalExactFunction: return "exact function"; } return "unknown"; } inline bool validate_utf8(Decoder* decoder, WireBytesRef string) { return unibrow::Utf8::ValidateEncoding( decoder->start() + decoder->GetBufferRelativeOffset(string.offset()), string.length()); } // Reads a length-prefixed string, checking that it is within bounds. Returns // the offset of the string, and the length as an out parameter. inline WireBytesRef consume_string(Decoder* decoder, unibrow::Utf8Variant grammar, const char* name, ITracer* tracer) { if (tracer) { tracer->Description(name); tracer->Description(" "); } uint32_t length = decoder->consume_u32v("length", tracer); if (tracer) { tracer->Description(": "); tracer->Description(length); tracer->NextLine(); } uint32_t offset = decoder->pc_offset(); const uint8_t* string_start = decoder->pc(); // Consume bytes before validation to guarantee that the string is not oob. if (length > 0) { if (tracer) { tracer->Bytes(decoder->pc(), length); tracer->Description(name); tracer->Description(": "); tracer->Description(reinterpret_cast<const char*>(decoder->pc()), length); tracer->NextLine(); } decoder->consume_bytes(length, name); if (decoder->ok()) { switch (grammar) { case unibrow::Utf8Variant::kLossyUtf8: break; case unibrow::Utf8Variant::kUtf8: if (!unibrow::Utf8::ValidateEncoding(string_start, length)) { decoder->errorf(string_start, "%s: no valid UTF-8 string", name); } break; case unibrow::Utf8Variant::kWtf8: if (!unibrow::Wtf8::ValidateEncoding(string_start, length)) { decoder->errorf(string_start, "%s: no valid WTF-8 string", name); } break; case unibrow::Utf8Variant::kUtf8NoTrap: UNREACHABLE(); } } } return {offset, decoder->failed() ? 0 : length}; } inline WireBytesRef consume_string(Decoder* decoder, unibrow::Utf8Variant grammar, const char* name) { return consume_string(decoder, grammar, name, ITracer::NoTrace); } inline WireBytesRef consume_utf8_string(Decoder* decoder, const char* name, ITracer* tracer) { return consume_string(decoder, unibrow::Utf8Variant::kUtf8, name, tracer); } inline SectionCode IdentifyUnknownSectionInternal(Decoder* decoder, ITracer* tracer) { WireBytesRef string = consume_utf8_string(decoder, "section name", tracer); if (decoder->failed()) { return kUnknownSectionCode; } const uint8_t* section_name_start = decoder->start() + decoder->GetBufferRelativeOffset(string.offset()); TRACE(" +%d section name : \"%.*s\"\n", static_cast<int>(section_name_start - decoder->start()), string.length() < 20 ? string.length() : 20, section_name_start); using SpecialSectionPair = std::pair<base::Vector<const char>, SectionCode>; static constexpr SpecialSectionPair kSpecialSections[]{ {base::StaticCharVector(kNameString), kNameSectionCode}, {base::StaticCharVector(kSourceMappingURLString), kSourceMappingURLSectionCode}, {base::StaticCharVector(kInstTraceString), kInstTraceSectionCode}, {base::StaticCharVector(kBranchHintsString), kBranchHintsSectionCode}, {base::StaticCharVector(kCompilationPriorityString), kCompilationPrioritySectionCode}, {base::StaticCharVector(kInstructionFrequenciesString), kInstFrequenciesSectionCode}, {base::StaticCharVector(kCallTargetsString), kCallTargetsSectionCode}, {base::StaticCharVector(kDebugInfoString), kDebugInfoSectionCode}, {base::StaticCharVector(kExternalDebugInfoString), kExternalDebugInfoSectionCode}, {base::StaticCharVector(kBuildIdString), kBuildIdSectionCode}, {base::StaticCharVector(kDescriptorsString), kDescriptorsSectionCode}}; auto name_vec = base::Vector<const char>::cast( base::VectorOf(section_name_start, string.length())); for (auto& special_section : kSpecialSections) { if (name_vec == special_section.first) return special_section.second; } return kUnknownSectionCode; } // An iterator over the sections in a wasm binary module. // Automatically skips all unknown sections. class WasmSectionIterator { public: explicit WasmSectionIterator(Decoder* decoder, ITracer* tracer) : decoder_(decoder), tracer_(tracer), section_code_(kUnknownSectionCode), section_start_(decoder->pc()), section_end_(decoder->pc()) { next(); } bool more() const { return decoder_->ok() && decoder_->more(); } SectionCode section_code() const { return section_code_; } const uint8_t* section_start() const { return section_start_; } uint32_t section_length() const { return static_cast<uint32_t>(section_end_ - section_start_); } base::Vector<const uint8_t> payload() const { return {payload_start_, payload_length()}; } const uint8_t* payload_start() const { return payload_start_; } uint32_t payload_length() const { return static_cast<uint32_t>(section_end_ - payload_start_); } const uint8_t* section_end() const { return section_end_; } // Advances to the next section, checking that decoding the current section // stopped at {section_end_}. void advance(bool move_to_section_end = false) { if (move_to_section_end && decoder_->pc() < section_end_) { decoder_->consume_bytes( static_cast<uint32_t>(section_end_ - decoder_->pc())); } if (decoder_->pc() != section_end_) { const char* msg = decoder_->pc() < section_end_ ? "shorter" : "longer"; decoder_->errorf(decoder_->pc(), "section was %s than expected size " "(%u bytes expected, %zu decoded)", msg, section_length(), static_cast<size_t>(decoder_->pc() - section_start_)); } next(); } private: Decoder* decoder_; ITracer* tracer_; SectionCode section_code_; const uint8_t* section_start_; const uint8_t* payload_start_; const uint8_t* section_end_; // Reads the section code/name at the current position and sets up // the embedder fields. void next() { if (!decoder_->more()) { section_code_ = kUnknownSectionCode; return; } section_start_ = decoder_->pc(); // Empty line before next section. if (tracer_) tracer_->NextLine(); uint8_t section_code = decoder_->consume_u8("section kind", tracer_); if (tracer_) { tracer_->Description(": "); tracer_->Description(SectionName(static_cast<SectionCode>(section_code))); tracer_->NextLine(); } // Read and check the section size. uint32_t section_length = decoder_->consume_u32v("section length", tracer_); if (tracer_) { tracer_->Description(section_length); tracer_->NextLine(); } payload_start_ = decoder_->pc(); section_end_ = payload_start_ + section_length; if (section_length > decoder_->available_bytes()) { decoder_->errorf( section_start_, "section (code %u, \"%s\") extends past end of the module " "(length %u, remaining bytes %u)", section_code, SectionName(static_cast<SectionCode>(section_code)), section_length, decoder_->available_bytes()); section_end_ = payload_start_; } if (section_code == kUnknownSectionCode) { // Check for known custom sections. // To identify the unknown section we set the end of the decoder bytes to // the end of the custom section, so that we do not read the section name // beyond the end of the section. const uint8_t* module_end = decoder_->end(); decoder_->set_end(section_end_); section_code = IdentifyUnknownSectionInternal(decoder_, tracer_); if (decoder_->ok()) decoder_->set_end(module_end); // As a side effect, the above function will forward the decoder to after // the identifier string. payload_start_ = decoder_->pc(); } else if (!IsValidSectionCode(section_code)) { decoder_->errorf(decoder_->pc(), "unknown section code #0x%02x", section_code); } section_code_ = decoder_->failed() ? kUnknownSectionCode : static_cast<SectionCode>(section_code); if (section_code_ == kUnknownSectionCode && section_end_ > decoder_->pc()) { // Skip to the end of the unknown section. uint32_t remaining = static_cast<uint32_t>(section_end_ - decoder_->pc()); decoder_->consume_bytes(remaining, "section payload", tracer_); } } }; inline void DumpModule(const base::Vector<const uint8_t> module_bytes, bool ok) { std::string path; if (v8_flags.dump_wasm_module_path) { path = v8_flags.dump_wasm_module_path; if (path.size() && !base::OS::isDirectorySeparator(path[path.size() - 1])) { path += base::OS::DirectorySeparator(); } } // File are named `<hash>.{ok,failed}.wasm`. // Limit the hash to 8 characters (32 bits). uint32_t hash = static_cast<uint32_t>(GetWireBytesHash(module_bytes)); base::EmbeddedVector<char, 32> buf; SNPrintF(buf, "%08x.%s.wasm", hash, ok ? "ok" : "failed"); path += buf.begin(); size_t rv = 0; if (FILE* file = base::OS::FOpen(path.c_str(), "wb")) { rv = fwrite(module_bytes.begin(), module_bytes.size(), 1, file); base::Fclose(file); } if (rv != 1) { OFStream os(stderr); os << "Error while dumping wasm file to " << path << std::endl; } } // The main logic for decoding the bytes of a module. class ModuleDecoderImpl : public Decoder { public: ModuleDecoderImpl(WasmEnabledFeatures enabled_features, base::Vector<const uint8_t> wire_bytes, ModuleOrigin origin, WasmDetectedFeatures* detected_features, ITracer* tracer = ITracer::NoTrace) : Decoder(wire_bytes), enabled_features_(enabled_features), detected_features_(detected_features), module_(std::make_shared<WasmModule>(origin)), module_start_(wire_bytes.begin()), module_end_(wire_bytes.end()), tracer_(tracer) {} void onFirstError() override { pc_ = end_; // On error, terminate section decoding loop. } void DecodeModuleHeader(base::Vector<const uint8_t> bytes) { if (failed()) return; Reset(bytes); const uint8_t* pos = pc_; uint32_t magic_word = consume_u32("wasm magic", tracer_); if (tracer_) tracer_->NextLine(); #define BYTES(x) (x & 0xFF), (x >> 8) & 0xFF, (x >> 16) & 0xFF, (x >> 24) & 0xFF if (magic_word != kWasmMagic) { errorf(pos, "expected magic word %02x %02x %02x %02x, " "found %02x %02x %02x %02x", BYTES(kWasmMagic), BYTES(magic_word)); } pos = pc_; { uint32_t magic_version = consume_u32("wasm version", tracer_); if (tracer_) tracer_->NextLine(); if (magic_version != kWasmVersion) { errorf(pos, "expected version %02x %02x %02x %02x, " "found %02x %02x %02x %02x", BYTES(kWasmVersion), BYTES(magic_version)); } } #undef BYTES } bool CheckSectionOrder(SectionCode section_code) { // Check the order of ordered sections. if (section_code >= kFirstSectionInModule && section_code < kFirstUnorderedSection) { if (section_code < next_ordered_section_) { errorf(pc(), "unexpected section <%s>", SectionName(section_code)); return false; } next_ordered_section_ = section_code + 1; return true; } // Ignore ordering problems in unknown / custom sections. Even allow them to // appear multiple times. As optional sections we use them on a "best // effort" basis. if (section_code == kUnknownSectionCode) return true; if (section_code > kLastKnownModuleSection) return true; // The rest is standardized unordered sections; they are checked more // thoroughly.. DCHECK_LE(kFirstUnorderedSection, section_code); DCHECK_GE(kLastKnownModuleSection, section_code); // Check that unordered sections don't appear multiple times. if (has_seen_unordered_section(section_code)) { errorf(pc(), "Multiple %s sections not allowed", SectionName(section_code)); return false; } set_seen_unordered_section(section_code); // Define a helper to ensure that sections <= {before} appear before the // current unordered section, and everything >= {after} appears after it. auto check_order = [this, section_code](SectionCode before, SectionCode after) -> bool { DCHECK_LT(before, after); if (next_ordered_section_ > after) { errorf(pc(), "The %s section must appear before the %s section", SectionName(section_code), SectionName(after)); return false; } if (next_ordered_section_ <= before) next_ordered_section_ = before + 1; return true; }; // Now check the ordering constraints of specific unordered sections. switch (section_code) { case kDataCountSectionCode: return check_order(kElementSectionCode, kCodeSectionCode); case kTagSectionCode: return check_order(kMemorySectionCode, kGlobalSectionCode); case kStringRefSectionCode: // TODO(12868): If there's a tag section, assert that we're after the // tag section. return check_order(kMemorySectionCode, kGlobalSectionCode); case kInstTraceSectionCode: // Custom section following code.metadata tool convention containing // offsets specifying where trace marks should be emitted. // Be lenient with placement of instruction trace section. All except // first occurrence after function section and before code section are // ignored. return true; default: return true; } } void DecodeSection(SectionCode section_code, base::Vector<const uint8_t> bytes, uint32_t offset) { if (failed()) return; Reset(bytes, offset); TRACE("Section: %s\n", SectionName(section_code)); TRACE("Decode Section %p - %p\n", bytes.begin(), bytes.end()); if (!CheckSectionOrder(section_code)) return; switch (section_code) { case kUnknownSectionCode: break; case kTypeSectionCode: DecodeTypeSection(); break; case kImportSectionCode: DecodeImportSection(); break; case kFunctionSectionCode: DecodeFunctionSection(); break; case kTableSectionCode: DecodeTableSection(); break; case kMemorySectionCode: DecodeMemorySection(); break; case kGlobalSectionCode: DecodeGlobalSection(); break; case kExportSectionCode: DecodeExportSection(); break; case kStartSectionCode: DecodeStartSection(); break; case kCodeSectionCode: DecodeCodeSection(); break; case kElementSectionCode: DecodeElementSection(); break; case kDataSectionCode: DecodeDataSection(); break; case kNameSectionCode: DecodeNameSection(); break; case kSourceMappingURLSectionCode: DecodeSourceMappingURLSection(); break; case kDebugInfoSectionCode: module_->debug_symbols[WasmDebugSymbols::Type::EmbeddedDWARF] = { WasmDebugSymbols::Type::EmbeddedDWARF, {}}; consume_bytes(static_cast<uint32_t>(end_ - start_), ".debug_info"); break; case kExternalDebugInfoSectionCode: DecodeExternalDebugInfoSection(); break; case kBuildIdSectionCode: DecodeBuildIdSection(); break; case kInstTraceSectionCode: if (enabled_features_.has_instruction_tracing()) { DecodeInstTraceSection(); } else { // Ignore this section when feature is disabled. It is an optional // custom section anyways. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } break; case kBranchHintsSectionCode: if (enabled_features_.has_branch_hinting()) { DecodeBranchHintsSection(); } else { // Ignore this section when feature was disabled. It is an optional // custom section anyways. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } break; case kCompilationPrioritySectionCode: if (enabled_features_.has_compilation_hints()) { DecodeCompilationPrioritySection(); } else { // Ignore this section when feature was disabled. It is an optional // custom section anyways. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } break; case kInstFrequenciesSectionCode: if (enabled_features_.has_compilation_hints()) { DecodeInstructionFrequenciesSection(); } else { // Ignore this section when feature was disabled. It is an optional // custom section anyways. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } break; case kCallTargetsSectionCode: if (enabled_features_.has_compilation_hints()) { DecodeCallTargetsSection(); } else { // Ignore this section when feature was disabled. It is an optional // custom section anyways. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } break; case kDescriptorsSectionCode: DecodeDescriptorsSection(); break; case kDataCountSectionCode: DecodeDataCountSection(); break; case kTagSectionCode: DecodeTagSection(); break; case kStringRefSectionCode: if (enabled_features_.has_stringref()) { DecodeStringRefSection(); } else { errorf(pc(), "unexpected section <%s> (enable with " "--experimental-wasm-stringref)", SectionName(section_code)); } break; default: errorf(pc(), "unexpected section <%s>", SectionName(section_code)); return; } if (pc() != bytes.end()) { const char* msg = pc() < bytes.end() ? "shorter" : "longer"; errorf(pc(), "section was %s than expected size " "(%zu bytes expected, %zu decoded)", msg, bytes.size(), static_cast<size_t>(pc() - bytes.begin())); } } static constexpr const char* TypeKindName(uint8_t kind) { switch (kind) { // clang-format off case kWasmFunctionTypeCode: return "func"; case kWasmStructTypeCode: return "struct"; case kWasmArrayTypeCode: return "array"; case kWasmContTypeCode: return "cont"; default: return "unknown"; // clang-format on } } TypeDefinition consume_base_type_definition(bool is_descriptor, bool is_shared) { const bool is_final = true; uint8_t kind = consume_u8(" kind", tracer_); if (tracer_) { tracer_->Description(": "); tracer_->Description(TypeKindName(kind)); } switch (kind) { case kWasmFunctionTypeCode: { const FunctionSig* sig = consume_sig(); if (sig == nullptr) { CHECK(!ok()); return {}; } return {sig, kNoSuperType, is_final, is_shared}; } case kWasmStructTypeCode: { module_->is_wasm_gc = true; const StructType* type = consume_struct(is_descriptor, is_shared); if (type == nullptr) { CHECK(!ok()); return {}; } return {type, kNoSuperType, is_final, is_shared}; } case kWasmArrayTypeCode: { module_->is_wasm_gc = true; const ArrayType* type = consume_array(); if (type == nullptr) { CHECK(!ok()); return {}; } return {type, kNoSuperType, is_final, is_shared}; } case kWasmContTypeCode: { if (!enabled_features_.has_wasmfx()) { error(pc() - 1, "core stack switching not enabled (enable with " "--experimental-wasm-wasmfx)"); return {}; } const uint8_t* pos = pc(); HeapType hp = consume_heap_type(); if (!hp.has_index()) { error(pos, "cont type must refer to a signature index"); return {}; } ContType* type = module_->signature_storage.New<ContType>(hp.ref_index()); return {type, kNoSuperType, is_final, is_shared}; } default: if (tracer_) tracer_->NextLine(); errorf(pc() - 1, "unknown type form: %d", kind); return {}; } } TypeDefinition consume_described_type(bool is_descriptor, bool is_shared) { uint8_t kind = read_u8<Decoder::FullValidationTag>(pc(), "type kind"); if (kind == kWasmDescriptorCode) { if (!enabled_features_.has_custom_descriptors()) { error(pc(), "descriptor types need --experimental-wasm-custom-descriptors"); return {}; } detected_features_->add_custom_descriptors(); consume_bytes(1, "", tracer_); const uint8_t* pos = pc(); uint32_t descriptor = consume_u32v(" descriptor", tracer_); if (descriptor >= module_->types.size()) { errorf(pos, "descriptor type index %u is out of bounds", descriptor); return {}; } if (tracer_) tracer_->Description(descriptor); if (tracer_) tracer_->NextLine(); TypeDefinition type = consume_base_type_definition(is_descriptor, is_shared); if (type.kind != TypeDefinition::kStruct) { error(pos - 1, "'descriptor' may only be used with structs"); return {}; } type.descriptor = ModuleTypeIndex{descriptor}; return type; } else { return consume_base_type_definition(is_descriptor, is_shared); } } TypeDefinition consume_describing_type(size_t current_type_index, bool is_shared) { uint8_t kind = read_u8<Decoder::FullValidationTag>(pc(), "type kind"); if (kind == kWasmDescribesCode) { if (!enabled_features_.has_custom_descriptors()) { error(pc(), "descriptor types need --experimental-wasm-custom-descriptors"); return {}; } detected_features_->add_custom_descriptors(); consume_bytes(1, "", tracer_); const uint8_t* pos = pc(); uint32_t describes = consume_u32v(" describes", tracer_); if (describes >= current_type_index) { error(pos, "types can only describe previously-declared types"); return {}; } if (tracer_) tracer_->Description(describes); if (tracer_) tracer_->NextLine(); TypeDefinition type = consume_described_type(true, is_shared); if (type.kind != TypeDefinition::kStruct) { error(pos - 1, "'describes' may only be used with structs"); return {}; } type.describes = ModuleTypeIndex{describes}; return type; } else { return consume_described_type(false, is_shared); } } TypeDefinition consume_shared_type(size_t current_type_index) { uint8_t kind = read_u8<Decoder::FullValidationTag>(pc(), "type kind"); if (kind == kSharedFlagCode) { if (!enabled_features_.has_shared()) { errorf(pc() - 1, "unknown type form: %d, enable with --experimental-wasm-shared", kind); return {}; } module_->has_shared_part = true; consume_bytes(1, " shared", tracer_); TypeDefinition type = consume_describing_type(current_type_index, true); DCHECK(type.is_shared); if (type.kind == TypeDefinition::kFunction || type.kind == TypeDefinition::kCont) { // TODO(42204563): Support shared functions/continuations. error(pc() - 1, "shared functions/continuations are not supported yet"); return {}; } return type; } else { return consume_describing_type(current_type_index, false); } } // {current_type_index} is the index of the type that's being decoded. // Any supertype must have a lower index. TypeDefinition consume_subtype_definition(size_t current_type_index) { uint8_t kind = read_u8<Decoder::FullValidationTag>(pc(), "type kind"); if (kind == kWasmSubtypeCode || kind == kWasmSubtypeFinalCode) { module_->is_wasm_gc = true; bool is_final = kind == kWasmSubtypeFinalCode; consume_bytes(1, is_final ? " subtype final, " : " subtype extensible, ", tracer_); constexpr uint32_t kMaximumSupertypes = 1; uint32_t supertype_count = consume_count("supertype count", kMaximumSupertypes); uint32_t supertype = kNoSuperType.index; if (supertype_count == 1) { supertype = consume_u32v("supertype", tracer_); if (supertype >= current_type_index) { errorf("type %u: invalid supertype %u", current_type_index, supertype); return {}; } if (tracer_) { tracer_->Description(supertype); tracer_->NextLine(); } } TypeDefinition type = consume_shared_type(current_type_index); type.supertype = ModuleTypeIndex{supertype}; type.is_final = is_final; return type; } else { return consume_shared_type(current_type_index); } } void DecodeTypeSection() { TypeCanonicalizer* type_canon = GetTypeCanonicalizer(); uint32_t types_count = consume_count("types count", kV8MaxWasmTypes); for (uint32_t i = 0; ok() && i < types_count; ++i) { TRACE("DecodeType[%d] module+%d\n", i, static_cast<int>(pc_ - start_)); uint8_t kind = read_u8<Decoder::FullValidationTag>(pc(), "type kind"); size_t initial_size = module_->types.size(); uint32_t group_size = 1; if (kind == kWasmRecursiveTypeGroupCode) { module_->is_wasm_gc = true; uint32_t rec_group_offset = pc_offset(); consume_bytes(1, "rec. group definition", tracer_); if (tracer_) tracer_->NextLine(); group_size = consume_count("recursive group size", kV8MaxWasmTypes); if (tracer_) tracer_->RecGroupOffset(rec_group_offset, group_size); } if (initial_size + group_size > kV8MaxWasmTypes) { errorf(pc(), "Type definition count exceeds maximum %zu", kV8MaxWasmTypes); return; } // We need to resize types before decoding the type definitions in this // group, so that the correct type size is visible to type definitions. module_->types.resize(initial_size + group_size); for (uint32_t j = 0; j < group_size; j++) { if (tracer_) tracer_->TypeOffset(pc_offset()); TypeDefinition type = consume_subtype_definition(initial_size + j); if (failed()) return; module_->types[initial_size + j] = type; } FinalizeRecgroup(group_size, type_canon); if (kind == kWasmRecursiveTypeGroupCode && tracer_) { tracer_->Description("end of rec. group"); tracer_->NextLine(); } } } void FinalizeRecgroup(uint32_t group_size, TypeCanonicalizer* type_canon) { // Now that we have decoded the entire recgroup, check its validity, // initialize additional data, and canonicalize it: // - check supertype validity // - propagate subtyping depths // - validate is_shared bits and set up RefTypeKind fields WasmModule* module = module_.get(); uint32_t end_index = static_cast<uint32_t>(module->types.size()); uint32_t start_index = end_index - group_size; for (uint32_t i = start_index; ok() && i < end_index; ++i) { TypeDefinition& type_def = module_->types[i]; bool is_shared = type_def.is_shared; switch (type_def.kind) { case TypeDefinition::kFunction: { base::Vector<const ValueType> all = type_def.function_sig->all(); size_t count = all.size(); ValueType* storage = const_cast<ValueType*>(all.begin()); for (uint32_t j = 0; j < count; j++) { value_type_reader::Populate(&storage[j], module); ValueType type = storage[j]; if (is_shared && !type.is_shared()) { DCHECK(enabled_features_.has_shared()); uint32_t retcount = static_cast<uint32_t>(type_def.function_sig->return_count()); const char* param_or_return = j < retcount ? "return" : "parameter"; uint32_t index = j < retcount ? j : j - retcount; // {pc_} isn't very accurate, it's pointing at the end of the // recgroup. So to ease debugging, we print the type's index. errorf(pc_, "Type %u: shared signature must have shared %s types, " "actual type for %s %d is %s", i, param_or_return, param_or_return, index, type.name().c_str()); return; } } break; } case TypeDefinition::kStruct: { size_t count = type_def.struct_type->field_count(); ValueType* storage = const_cast<ValueType*>(type_def.struct_type->fields().begin()); for (uint32_t j = 0; j < count; j++) { value_type_reader::Populate(&storage[j], module); ValueType type = storage[j]; if (is_shared && !type.is_shared()) { errorf(pc_, "Type %u: shared struct must have shared field types, " "actual type for field %d is %s", i, j, type.name().c_str()); return; } } if (type_def.descriptor.valid()) { const TypeDefinition& descriptor = module->type(type_def.descriptor); if (descriptor.describes.index != i) { uint32_t d = type_def.descriptor.index; errorf(pc_, "Type %u has descriptor %u but %u doesn't describe %u", i, d, d, i); return; } if (descriptor.is_shared != type_def.is_shared) { errorf(pc_, "Type %u and its descriptor %u must have same sharedness", i, type_def.descriptor.index); return; } } if (type_def.describes.valid()) { if (module->type(type_def.describes).descriptor.index != i) { uint32_t d = type_def.describes.index; errorf(pc_, "Type %u describes %u but %u isn't a descriptor for %u", i, d, d, i); return; } } break; } case TypeDefinition::kArray: { value_type_reader::Populate( const_cast<ArrayType*>(type_def.array_type) ->element_type_writable_ptr(), module); ValueType type = type_def.array_type->element_type(); if (is_shared && !type.is_shared()) { errorf(pc_, "Type %u: shared array must have shared element type, " "actual element type is %s", i, type.name().c_str()); return; } break; } case TypeDefinition::kCont: { ModuleTypeIndex contfun_typeid = type_def.cont_type->contfun_typeindex(); const TypeDefinition contfun_type = module_->types[contfun_typeid.index]; if (contfun_type.kind != TypeDefinition::kFunction) { errorf(pc_, "Type %u: cont type must refer to a signature index, " "actual type is %s", i, module_->heap_type(contfun_typeid).name().c_str()); return; } if (is_shared && !contfun_type.is_shared) { errorf(pc_, "Type %u: shared cont type must refer to a shared signature," " actual type is %s", module_->heap_type(contfun_typeid).name().c_str()); return; } break; } } } module->isorecursive_canonical_type_ids.resize(end_index); type_canon->AddRecursiveGroup(module, group_size); for (uint32_t i = start_index; ok() && i < end_index; ++i) { TypeDefinition& type_def = module_->types[i]; ModuleTypeIndex explicit_super = type_def.supertype; if (!explicit_super.valid()) continue; // No supertype. DCHECK_LT(explicit_super.index, i); // Checked during decoding. uint32_t depth = module->type(explicit_super).subtyping_depth + 1; type_def.subtyping_depth = depth; DCHECK_GE(depth, 0); if (depth > kV8MaxRttSubtypingDepth) { errorf("type %u: subtyping depth is greater than allowed", i); return; } // This check is technically redundant; we include for the improved error // message. if (module->type(explicit_super).is_final) { errorf("type %u extends final type %u", i, explicit_super.index); return; } if (!ValidSubtypeDefinition(ModuleTypeIndex{i}, explicit_super, module)) { errorf("type %u has invalid explicit supertype %u", i, explicit_super.index); return; } } } void DecodeImportSection() { uint32_t import_table_count = consume_count("imports count", kV8MaxWasmImports); module_->import_table.reserve(import_table_count); for (uint32_t i = 0; ok() && i < import_table_count; ++i) { TRACE("DecodeImportTable[%d] module+%d\n", i, static_cast<int>(pc_ - start_)); if (tracer_) tracer_->ImportOffset(pc_offset()); const uint8_t* pos = pc_; WireBytesRef module_name = consume_utf8_string(this, "module name", tracer_); WireBytesRef field_name = consume_utf8_string(this, "field name", tracer_); ImportExportKindCode kind = static_cast<ImportExportKindCode>(consume_u8("kind", tracer_)); if (tracer_) { tracer_->Description(": "); tracer_->Description(ExternalKindName(kind)); } module_->import_table.push_back(WasmImport{ .module_name = module_name, .field_name = field_name, .kind = kind}); WasmImport* import = &module_->import_table.back(); switch (kind) { case kExternalFunction: case kExternalExactFunction: { // ===== Imported function =========================================== import->index = static_cast<uint32_t>(module_->functions.size()); module_->num_imported_functions++; module_->functions.push_back(WasmFunction{ .func_index = import->index, .imported = true, }); WasmFunction* function = &module_->functions.back(); if (kind == kExternalExactFunction) { if (!enabled_features_.has_custom_descriptors()) { // We haven't decoded any bytes since {consume_u8("kind")}. errorf(pc_ - 1, "Invalid import kind %d, enable with " "--experimental-wasm-custom-descriptors", kind); break; } function->exact = true; detected_features_->add_custom_descriptors(); } function->sig_index = consume_sig_index(module_.get(), &function->sig); break; } case kExternalTable: { // ===== Imported table ============================================== import->index = static_cast<uint32_t>(module_->tables.size()); const uint8_t* type_position = pc(); ValueType type = consume_value_type(module_.get()); if (!type.is_ref()) { errorf(type_position, "Invalid table type %s", type.name().c_str()); break; } module_->num_imported_tables++; module_->tables.push_back(WasmTable{ .type = type, .imported = true, }); WasmTable* table = &module_->tables.back(); consume_table_flags(table); DCHECK_IMPLIES(table->shared, enabled_features_.has_shared() || !ok()); if (table->shared && enabled_features_.has_shared()) { module_->has_shared_part = true; if (!type.is_shared()) { errorf(type_position, "Shared table %i must have shared element type, actual " "type %s", i, type.name().c_str()); break; } } // Note that we should not throw an error if the declared maximum size // is oob. We will instead fail when growing at runtime. uint64_t kNoMaximum = kMaxUInt64; consume_resizable_limits( "table", "elements", wasm::max_table_size(), &table->initial_size, table->has_maximum_size, kNoMaximum, &table->maximum_size, table->is_table64() ? k64BitLimits : k32BitLimits); break; } case kExternalMemory: { // ===== Imported memory ============================================= static_assert(kV8MaxWasmMemories <= kMaxUInt32); if (module_->memories.size() >= kV8MaxWasmMemories - 1) { errorf("At most %u imported memories are supported", kV8MaxWasmMemories); break; } uint32_t mem_index = static_cast<uint32_t>(module_->memories.size()); import->index = mem_index; module_->memories.emplace_back(); WasmMemory* external_memory = &module_->memories.back(); external_memory->imported = true; external_memory->index = mem_index; consume_memory_flags(external_memory); uint32_t max_pages = external_memory->is_memory64() ? kSpecMaxMemory64Pages : kSpecMaxMemory32Pages; consume_resizable_limits( "memory", "pages", max_pages, &external_memory->initial_pages, external_memory->has_maximum_pages, max_pages, &external_memory->maximum_pages, external_memory->is_memory64() ? k64BitLimits : k32BitLimits); break; } case kExternalGlobal: { // ===== Imported global ============================================= import->index = static_cast<uint32_t>(module_->globals.size()); ValueType type = consume_value_type(module_.get()); auto [mutability, shared] = consume_global_flags(); if (V8_UNLIKELY(failed())) break; if (V8_UNLIKELY(shared && !type.is_shared())) { error("shared imported global must have shared type"); break; } module_->globals.push_back( WasmGlobal{.type = type, .mutability = mutability, .index = 0, // set later in CalculateGlobalOffsets .shared = shared, .imported = true}); module_->num_imported_globals++; DCHECK_EQ(module_->globals.size(), module_->num_imported_globals); if (shared) module_->has_shared_part = true; if (mutability) module_->num_imported_mutable_globals++; if (tracer_) tracer_->NextLine(); break; } case kExternalTag: { // ===== Imported tag ================================================ import->index = static_cast<uint32_t>(module_->tags.size()); module_->num_imported_tags++; const WasmTagSig* tag_sig = nullptr; consume_exception_attribute(); // Attribute ignored for now. ModuleTypeIndex sig_index = consume_tag_sig_index(module_.get(), &tag_sig); module_->tags.emplace_back(tag_sig, sig_index); break; } default: errorf(pos, "unknown import kind 0x%02x", kind); break; } } if (module_->memories.size() > 1) { detected_features_->add_multi_memory(); if (v8_flags.wasm_jitless) { error("Multiple memories not supported in Wasm jitless mode"); } } if (module_->num_imported_mutable_globals > 0) { detected_features_->add_mutable_globals(); } UpdateComputedMemoryInformation(); module_->type_feedback.well_known_imports.Initialize( module_->num_imported_functions); if (tracer_) tracer_->ImportsDone(module_.get()); } void DecodeFunctionSection() { uint32_t functions_count = consume_count("functions count", v8_flags.max_wasm_functions); DCHECK_EQ(module_->functions.size(), module_->num_imported_functions); uint32_t total_function_count = module_->num_imported_functions + functions_count; module_->functions.resize(total_function_count); module_->num_declared_functions = functions_count; // Also initialize the {validated_functions} bitset here, now that we know // the number of declared functions. DCHECK_NULL(module_->validated_functions); module_->validated_functions = std::make_unique<std::atomic<uint8_t>[]>((functions_count + 7) / 8); if (is_asmjs_module(module_.get())) { // Mark all asm.js functions as valid by design (it's faster to do this // here than to check this in {WasmModule::function_was_validated}). std::fill_n(module_->validated_functions.get(), (functions_count + 7) / 8, 0xff); } for (uint32_t func_index = module_->num_imported_functions; func_index < total_function_count; ++func_index) { WasmFunction* function = &module_->functions[func_index]; function->func_index = func_index; if (tracer_) tracer_->FunctionName(func_index); function->sig_index = consume_sig_index(module_.get(), &function->sig); if (!ok()) return; } } void DecodeTableSection() { static_assert(kV8MaxWasmTables <= kMaxUInt32); uint32_t table_count = consume_count("table count", kV8MaxWasmTables); for (uint32_t i = 0; ok() && i < table_count; i++) { if (tracer_) tracer_->TableOffset(pc_offset()); module_->tables.emplace_back(); WasmTable* table = &module_->tables.back(); const uint8_t* type_position = pc(); bool has_initializer = false; if (read_u8<Decoder::FullValidationTag>( pc(), "table-with-initializer byte") == 0x40) { consume_bytes(1, "with-initializer ", tracer_); has_initializer = true; type_position++; uint8_t reserved = consume_u8("reserved-byte", tracer_); if (reserved != 0) { error(type_position, "Reserved byte must be 0x00"); break; } type_position++; } ValueType table_type = consume_value_type(module_.get()); if (!table_type.is_ref()) { error(type_position, "Only reference types can be used as table types"); break; } if (!has_initializer && !table_type.is_defaultable()) { errorf(type_position, "Table of non-defaultable table %s needs initial value", table_type.name().c_str()); break; } table->type = table_type; consume_table_flags(table); DCHECK_IMPLIES(table->shared, enabled_features_.has_shared() || !ok()); if (table->shared && enabled_features_.has_shared()) { module_->has_shared_part = true; if (!table_type.is_shared()) { errorf( type_position, "Shared table %i must have shared element type, actual type %s", i + module_->num_imported_tables, table_type.name().c_str()); break; } } // Note that we should not throw an error if the declared maximum size is // oob. We will instead fail when growing at runtime. uint64_t kNoMaximum = kMaxUInt64; consume_resizable_limits( "table", "elements", wasm::max_table_size(), &table->initial_size, table->has_maximum_size, kNoMaximum, &table->maximum_size, table->is_table64() ? k64BitLimits : k32BitLimits); if (has_initializer) { table->initial_value = consume_init_expr(module_.get(), table_type, table->shared); } } } void DecodeMemorySection() { const uint8_t* mem_count_pc = pc(); static_assert(kV8MaxWasmMemories <= kMaxUInt32); // Use {kV8MaxWasmMemories} here, but only allow for >1 memory if // multi-memory is enabled (checked below). This allows for better error // messages. uint32_t memory_count = consume_count("memory count", kV8MaxWasmMemories); size_t imported_memories = module_->memories.size(); DCHECK_GE(kV8MaxWasmMemories, imported_memories); if (memory_count > kV8MaxWasmMemories - imported_memories) { errorf(mem_count_pc, "Exceeding maximum number of memories (%u; declared %u, " "imported %zu)", kV8MaxWasmMemories, memory_count, imported_memories); } module_->memories.resize(imported_memories + memory_count); for (uint32_t i = 0; ok() && i < memory_count; i++) { WasmMemory* memory = module_->memories.data() + imported_memories + i; memory->index = static_cast<uint32_t>(imported_memories + i); if (tracer_) tracer_->MemoryOffset(pc_offset()); consume_memory_flags(memory); uint32_t max_pages = memory->is_memory64() ? kSpecMaxMemory64Pages : kSpecMaxMemory32Pages; consume_resizable_limits( "memory", "pages", max_pages, &memory->initial_pages, memory->has_maximum_pages, max_pages, &memory->maximum_pages, memory->is_memory64() ? k64BitLimits : k32BitLimits); } if (module_->memories.size() > 1) { detected_features_->add_multi_memory(); if (v8_flags.wasm_jitless) { error("Multiple memories not supported in Wasm jitless mode"); } } UpdateComputedMemoryInformation(); } void UpdateComputedMemoryInformation() { for (WasmMemory& memory : module_->memories) { UpdateComputedInformation(&memory, module_->origin); } } void DecodeGlobalSection() { uint32_t globals_count = consume_count("globals count", kV8MaxWasmGlobals); uint32_t imported_globals = static_cast<uint32_t>(module_->globals.size()); // It is important to not resize the globals vector from the beginning, // because we use its current size when decoding the initializer. module_->globals.reserve(imported_globals + globals_count); for (uint32_t i = 0; ok() && i < globals_count; ++i) { TRACE("DecodeGlobal[%d] module+%d\n", i, static_cast<int>(pc_ - start_)); if (tracer_) tracer_->GlobalOffset(pc_offset()); const uint8_t* pos = pc_; ValueType type = consume_value_type(module_.get()); auto [mutability, shared] = consume_global_flags(); if (failed()) return; if (shared && !type.is_shared()) { CHECK(enabled_features_.has_shared()); errorf(pos, "Shared global %i must have shared type, actual type %s", i + imported_globals, type.name().c_str()); return; } // Validation that {type} and {shared} are compatible will happen in // {consume_init_expr}. bool ends_with_struct_new = false; ConstantExpression init = consume_init_expr(module_.get(), type, shared, &ends_with_struct_new); module_->globals.push_back( WasmGlobal{.type = type, .mutability = mutability, .init = init, .index = 0, // set later in CalculateGlobalOffsets .shared = shared, .initializer_ends_with_struct_new = ends_with_struct_new}); if (shared) module_->has_shared_part = true; } } void DecodeExportSection() { uint32_t export_table_count = consume_count("exports count", kV8MaxWasmExports); module_->export_table.reserve(export_table_count); for (uint32_t i = 0; ok() && i < export_table_count; ++i) { TRACE("DecodeExportTable[%d] module+%d\n", i, static_cast<int>(pc_ - start_)); if (tracer_) { tracer_->Description("export #"); tracer_->Description(i); tracer_->NextLine(); } WireBytesRef name = consume_utf8_string(this, "field name", tracer_); const uint8_t* kind_pos = pc(); ImportExportKindCode kind = static_cast<ImportExportKindCode>(consume_u8("kind", tracer_)); module_->export_table.push_back(WasmExport{.name = name, .kind = kind}); WasmExport* exp = &module_->export_table.back(); if (tracer_) { tracer_->Description(": "); tracer_->Description(ExternalKindName(exp->kind)); tracer_->Description(" "); } switch (kind) { case kExternalFunction: { WasmFunction* func = nullptr; exp->index = consume_func_index(module_.get(), &func); if (failed()) break; DCHECK_NOT_NULL(func); module_->num_exported_functions++; func->exported = true; // Exported functions are considered "declared". func->declared = true; break; } case kExternalTable: { WasmTable* table = nullptr; exp->index = consume_table_index(module_.get(), &table); if (table) table->exported = true; break; } case kExternalMemory: { const uint8_t* index_pos = pc(); exp->index = consume_u32v("memory index", tracer_); size_t num_memories = module_->memories.size(); if (exp->index >= module_->memories.size()) { errorf(index_pos, "invalid exported memory index %u (having %zu memor%s)", exp->index, num_memories, num_memories == 1 ? "y" : "ies"); break; } module_->memories[exp->index].exported = true; break; } case kExternalGlobal: { WasmGlobal* global = nullptr; exp->index = consume_global_index(module_.get(), &global); if (global) { global->exported = true; } break; } case kExternalTag: { WasmTag* tag = nullptr; exp->index = consume_tag_index(module_.get(), &tag); break; } case kExternalExactFunction: default: errorf(kind_pos, "invalid export kind 0x%02x", exp->kind); break; } if (tracer_) tracer_->NextLine(); } // Check for duplicate exports (except for asm.js). if (ok() && module_->origin == kWasmOrigin && module_->export_table.size() > 1) { std::vector<WasmExport> sorted_exports(module_->export_table); auto cmp_less = [this](const WasmExport& a, const WasmExport& b) { // Return true if a < b. if (a.name.length() != b.name.length()) { return a.name.length() < b.name.length(); } const uint8_t* left = start() + GetBufferRelativeOffset(a.name.offset()); const uint8_t* right = start() + GetBufferRelativeOffset(b.name.offset()); return memcmp(left, right, a.name.length()) < 0; }; std::stable_sort(sorted_exports.begin(), sorted_exports.end(), cmp_less); auto it = sorted_exports.begin(); WasmExport* last = &*it++; for (auto end = sorted_exports.end(); it != end; last = &*it++) { DCHECK(!cmp_less(*it, *last)); // Vector must be sorted. if (!cmp_less(*last, *it)) { const uint8_t* pc = start() + GetBufferRelativeOffset(it->name.offset()); TruncatedUserString<> name(pc, it->name.length()); errorf(pc, "Duplicate export name '%.*s' for %s %d and %s %d", name.length(), name.start(), ExternalKindName(last->kind), last->index, ExternalKindName(it->kind), it->index); break; } } } } void DecodeStartSection() { if (tracer_) tracer_->StartOffset(pc_offset()); WasmFunction* func; const uint8_t* pos = pc_; module_->start_function_index = consume_func_index(module_.get(), &func); if (tracer_) tracer_->NextLine(); if (func && (func->sig->parameter_count() > 0 || func->sig->return_count() > 0)) { error(pos, "invalid start function: non-zero parameter or return count"); } } void DecodeElementSection() { uint32_t segment_count = consume_count("segment count", wasm::max_table_size()); for (uint32_t i = 0; i < segment_count; ++i) { if (tracer_) tracer_->ElementOffset(pc_offset()); WasmElemSegment segment = consume_element_segment_header(); if (tracer_) tracer_->NextLineIfNonEmpty(); if (failed()) return; DCHECK_NE(segment.type, kWasmBottom); for (uint32_t j = 0; j < segment.element_count; j++) { // Just run validation on elements; do not store them anywhere. We will // decode them again from wire bytes as needed. consume_element_segment_entry(module_.get(), segment); if (failed()) return; } module_->elem_segments.push_back(std::move(segment)); } } void DecodeCodeSection() { // Make sure global offset were calculated before they get accessed during // function compilation. CalculateGlobalOffsets(module_.get()); uint32_t code_section_start = pc_offset(); uint32_t functions_count = consume_u32v("functions count", tracer_); if (tracer_) { tracer_->Description(functions_count); tracer_->NextLine(); } CheckFunctionsCount(functions_count, code_section_start); auto inst_traces_it = this->inst_traces_.begin(); std::vector<std::pair<uint32_t, uint32_t>> inst_traces; for (uint32_t i = 0; ok() && i < functions_count; ++i) { int function_index = module_->num_imported_functions + i; if (tracer_) { tracer_->Description("function #"); tracer_->FunctionName(function_index); tracer_->NextLine(); } const uint8_t* pos = pc(); uint32_t size = consume_u32v("body size", tracer_); if (tracer_) { tracer_->Description(size); tracer_->NextLine(); } if (size > kV8MaxWasmFunctionSize) { errorf(pos, "size %u > maximum function size %zu", size, kV8MaxWasmFunctionSize); return; } uint32_t offset = pc_offset(); consume_bytes(size, "function body"); if (failed()) break; DecodeFunctionBody(function_index, size, offset); // Now that the function has been decoded, we can compute module offsets. for (; inst_traces_it != this->inst_traces_.end() && std::get<0>(*inst_traces_it) == i; ++inst_traces_it) { uint32_t trace_offset = offset + std::get<1>(*inst_traces_it); uint32_t mark_id = std::get<2>(*inst_traces_it); std::pair<uint32_t, uint32_t> trace_mark = {trace_offset, mark_id}; inst_traces.push_back(trace_mark); } } // If we have actually decoded traces and they were all decoded without // error, then we can move them to the module. If any errors are found, it // is safe to throw away all traces. if (V8_UNLIKELY(!inst_traces.empty() && inst_traces_it == this->inst_traces_.end())) { // This adds an invalid entry at the end of the traces. An invalid entry // is defined as having an module offset of 0 and a markid of 0. inst_traces.push_back({0, 0}); this->module_->inst_traces = std::move(inst_traces); } DCHECK_GE(pc_offset(), code_section_start); module_->code = {code_section_start, pc_offset() - code_section_start}; } void StartCodeSection(WireBytesRef section_bytes) { CheckSectionOrder(kCodeSectionCode); // Make sure global offset were calculated before they get accessed during // function compilation. CalculateGlobalOffsets(module_.get()); module_->code = section_bytes; } bool CheckFunctionsCount(uint32_t functions_count, uint32_t error_offset) { if (functions_count != module_->num_declared_functions) { errorf(error_offset, "function body count %u mismatch (%u expected)", functions_count, module_->num_declared_functions); return false; } return true; } void DecodeFunctionBody(uint32_t func_index, uint32_t length, uint32_t offset) { WasmFunction* function = &module_->functions[func_index]; function->code = {offset, length}; constexpr uint32_t kSmallFunctionThreshold = 50; if (length < kSmallFunctionThreshold) { ++module_->num_small_functions; } if (tracer_) { tracer_->FunctionBody(function, pc_ - (pc_offset() - offset)); } } bool CheckDataSegmentsCount(uint32_t data_segments_count) { if (has_seen_unordered_section(kDataCountSectionCode) && data_segments_count != module_->num_declared_data_segments) { errorf(pc(), "data segments count %u mismatch (%u expected)", data_segments_count, module_->num_declared_data_segments); return false; } return true; } struct DataSegmentHeader { bool is_active; bool is_shared; uint32_t memory_index; ConstantExpression dest_addr; }; void DecodeDataSection() { uint32_t data_segments_count = consume_count("data segments count", kV8MaxWasmDataSegments); if (!CheckDataSegmentsCount(data_segments_count)) return; module_->data_segments.reserve(data_segments_count); for (uint32_t i = 0; i < data_segments_count; ++i) { TRACE("DecodeDataSegment[%d] module+%d\n", i, static_cast<int>(pc_ - start_)); if (tracer_) tracer_->DataOffset(pc_offset()); DataSegmentHeader header = consume_data_segment_header(); uint32_t source_length = consume_u32v("source size", tracer_); if (tracer_) { tracer_->Description(source_length); tracer_->NextLine(); } uint32_t source_offset = pc_offset(); if (tracer_) { tracer_->Bytes(pc_, source_length); tracer_->Description("segment data"); tracer_->NextLine(); } consume_bytes(source_length, "segment data"); if (failed()) break; module_->data_segments.emplace_back( header.is_active, header.is_shared, header.memory_index, header.dest_addr, WireBytesRef{source_offset, source_length}); } } void DecodeNameSection() { if (tracer_) { tracer_->NameSection( pc_, end_, buffer_offset_ + static_cast<uint32_t>(pc_ - start_)); } // TODO(titzer): find a way to report name errors as warnings. // Ignore all but the first occurrence of name section. if (!has_seen_unordered_section(kNameSectionCode)) { set_seen_unordered_section(kNameSectionCode); module_->name_section = {buffer_offset_, static_cast<uint32_t>(end_ - start_)}; // Use an inner decoder so that errors don't fail the outer decoder. Decoder inner(start_, pc_, end_, buffer_offset_); // Decode all name subsections. // Be lenient with their order. while (inner.ok() && inner.more()) { uint8_t name_type = inner.consume_u8("name type"); if (name_type & 0x80) inner.error("name type if not varuint7"); uint32_t name_payload_len = inner.consume_u32v("name payload length"); if (!inner.checkAvailable(name_payload_len)) break; // Decode module name, ignore the rest. // Function and local names will be decoded when needed. if (name_type == NameSectionKindCode::kModuleCode) { WireBytesRef name = consume_string(&inner, unibrow::Utf8Variant::kLossyUtf8, "module name", ITracer::NoTrace); if (inner.ok() && validate_utf8(&inner, name)) { module_->name = name; } } else { inner.consume_bytes(name_payload_len, "name subsection payload"); } } } // Skip the whole names section in the outer decoder. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } void DecodeDescriptorsSection() { if (enabled_features_.has_custom_descriptors() && !has_seen_unordered_section(kDescriptorsSectionCode)) { set_seen_unordered_section(kDescriptorsSectionCode); module_->descriptors_section = {buffer_offset_, static_cast<uint32_t>(end_ - start_)}; } consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } void DecodeSourceMappingURLSection() { Decoder inner(start_, pc_, end_, buffer_offset_); WireBytesRef url = wasm::consume_utf8_string(&inner, "module name", tracer_); if (inner.ok() && module_->debug_symbols[WasmDebugSymbols::Type::SourceMap].type == WasmDebugSymbols::None) { module_->debug_symbols[WasmDebugSymbols::Type::SourceMap] = { WasmDebugSymbols::Type::SourceMap, url}; } set_seen_unordered_section(kSourceMappingURLSectionCode); consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } void DecodeExternalDebugInfoSection() { Decoder inner(start_, pc_, end_, buffer_offset_); WireBytesRef url = wasm::consume_utf8_string(&inner, "external symbol file", tracer_); if (inner.ok()) { module_->debug_symbols[WasmDebugSymbols::Type::ExternalDWARF] = { WasmDebugSymbols::Type::ExternalDWARF, url}; set_seen_unordered_section(kExternalDebugInfoSectionCode); } consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } void DecodeBuildIdSection() { Decoder inner(start_, pc_, end_, buffer_offset_); WireBytesRef build_id = consume_string(&inner, unibrow::Utf8Variant::kLossyUtf8, "build_id"); if (inner.ok()) { module_->build_id = build_id; set_seen_unordered_section(kBuildIdSectionCode); } consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } void DecodeInstTraceSection() { TRACE("DecodeInstTrace module+%d\n", static_cast<int>(pc_ - start_)); if (!has_seen_unordered_section(kInstTraceSectionCode)) { set_seen_unordered_section(kInstTraceSectionCode); // Use an inner decoder so that errors don't fail the outer decoder. Decoder inner(start_, pc_, end_, buffer_offset_); std::vector<std::tuple<uint32_t, uint32_t, uint32_t>> inst_traces; uint32_t func_count = inner.consume_u32v("number of functions"); // Keep track of the previous function index to validate the ordering. int64_t last_func_idx = -1; for (uint32_t i = 0; i < func_count; i++) { uint32_t func_idx = inner.consume_u32v("function index"); if (int64_t{func_idx} <= last_func_idx) { inner.errorf("Invalid function index: %d", func_idx); break; } last_func_idx = func_idx; uint32_t num_traces = inner.consume_u32v("number of trace marks"); TRACE("DecodeInstTrace[%d] module+%d\n", func_idx, static_cast<int>(inner.pc() - inner.start())); // Keep track of the previous offset to validate the ordering. int64_t last_func_off = -1; for (uint32_t j = 0; j < num_traces; ++j) { uint32_t func_off = inner.consume_u32v("function offset"); uint32_t mark_size = inner.consume_u32v("mark size"); uint32_t trace_mark_id = 0; // Build the mark id from the individual bytes. for (uint32_t k = 0; k < mark_size; k++) { trace_mark_id |= inner.consume_u8("trace mark id") << k * 8; } if (int64_t{func_off} <= last_func_off) { inner.errorf("Invalid branch offset: %d", func_off); break; } last_func_off = func_off; TRACE("DecodeInstTrace[%d][%d] module+%d\n", func_idx, func_off, static_cast<int>(inner.pc() - inner.start())); // Store the function index, function offset, and mark id into a // temporary 3-tuple. This will later be translated to a module // offset and mark id. std::tuple<uint32_t, uint32_t, uint32_t> mark_tuple = { func_idx, func_off, trace_mark_id}; inst_traces.push_back(mark_tuple); } } // Extra unexpected bytes are an error. if (inner.more()) { inner.errorf("Unexpected extra bytes: %d\n", static_cast<int>(inner.pc() - inner.start())); } // If everything went well, accept the traces for the module. if (inner.ok()) { this->inst_traces_ = std::move(inst_traces); } } // Skip the whole instruction trace section in the outer decoder. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } void DecodeBranchHintsSection() { TRACE("DecodeBranchHints module+%d\n", static_cast<int>(pc_ - start_)); detected_features_->add_branch_hinting(); if (!has_seen_unordered_section(kBranchHintsSectionCode)) { set_seen_unordered_section(kBranchHintsSectionCode); // Use an inner decoder so that errors don't fail the outer decoder. Decoder inner(start_, pc_, end_, buffer_offset_); BranchHintInfo branch_hints; uint32_t func_count = inner.consume_u32v("number of functions"); // Keep track of the previous function index to validate the ordering int64_t last_func_idx = -1; for (uint32_t i = 0; i < func_count; i++) { uint32_t func_idx = inner.consume_u32v("function index"); if (int64_t{func_idx} <= last_func_idx) { inner.errorf("Invalid function index: %d", func_idx); break; } last_func_idx = func_idx; uint32_t num_hints = inner.consume_u32v("number of hints"); BranchHintMap func_branch_hints; TRACE("DecodeBranchHints[%d] module+%d\n", func_idx, static_cast<int>(inner.pc() - inner.start())); // Keep track of the previous branch offset to validate the ordering int64_t last_br_off = -1; for (uint32_t j = 0; j < num_hints; ++j) { uint32_t br_off = inner.consume_u32v("branch instruction offset"); if (int64_t{br_off} <= last_br_off) { inner.errorf("Invalid branch offset: %d", br_off); break; } last_br_off = br_off; uint32_t data_size = inner.consume_u32v("data size"); if (data_size != 1) { inner.errorf("Invalid data size: %#x. Expected 1.", data_size); break; } uint32_t br_dir = inner.consume_u8("branch direction"); TRACE("DecodeBranchHints[%d][%d] module+%d\n", func_idx, br_off, static_cast<int>(inner.pc() - inner.start())); BranchHint hint; switch (br_dir) { case 0: hint = BranchHint::kFalse; break; case 1: hint = BranchHint::kTrue; break; default: hint = BranchHint::kNone; inner.errorf(inner.pc(), "Invalid branch hint %#x", br_dir); break; } if (!inner.ok()) { break; } func_branch_hints.insert(br_off, hint); } if (!inner.ok()) { break; } branch_hints.emplace(func_idx, std::move(func_branch_hints)); } // Extra unexpected bytes are an error. if (inner.more()) { inner.errorf("Unexpected extra bytes: %d\n", static_cast<int>(inner.pc() - inner.start())); } // If everything went well, accept the hints for the module. if (inner.ok()) { module_->branch_hints = std::move(branch_hints); } else { TRACE("DecodeBranchHints error: %s", inner.error().message().c_str()); } } // Skip the whole branch hints section in the outer decoder. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } void DecodeCompilationPrioritySection() { TRACE("DecodeCompilationPriority module+%d\n", static_cast<int>(pc_ - start_)); detected_features_->add_compilation_hints(); if (!has_seen_unordered_section(kCompilationPrioritySectionCode)) { set_seen_unordered_section(kCompilationPrioritySectionCode); // Use an inner decoder so that errors don't fail the outer decoder. Decoder inner(start_, pc_, end_, buffer_offset_); CompilationPriorities compilation_priorities; uint32_t func_count = inner.consume_u32v("number of functions"); int64_t last_func_index = -1; for (uint32_t i = 0; i < func_count; i++) { uint32_t func_index = inner.consume_u32v("function index"); if (static_cast<int64_t>(func_index) <= last_func_index) { inner.error("out of order functions"); break; } last_func_index = func_index; uint32_t byte_offset = inner.consume_u32v("byte offset"); if (byte_offset != 0) { inner.error("byte offset has to be 0 (function level only)"); break; } uint32_t hint_length = inner.consume_u32v("hint length"); const uint8_t* pc_after_hint_length = inner.pc(); uint32_t compilation_priority = inner.consume_u32v("compilation priority"); if (static_cast<int64_t>(hint_length) < inner.pc() - pc_after_hint_length) { inner.error("Compilation priority longer than declared hint length"); break; } int optimization_priority = kOptimizationPriorityNotSpecifiedSentinel; // If we exhausted the declared hint length, do not parse optimization // priority. if (static_cast<int64_t>(hint_length) > inner.pc() - pc_after_hint_length) { uint32_t parsed_priority = inner.consume_u32v("optimization priority"); if (static_cast<int>(parsed_priority) > kOptimizationPriorityExecutedOnceSentinel) { inner.error("Optimization priority too large"); break; } optimization_priority = static_cast<int>(parsed_priority); if (static_cast<int64_t>(hint_length) < inner.pc() - pc_after_hint_length) { inner.error("Optimization priority overflows declared hint length"); break; } } // Ignore remaining hint bytes. inner.consume_bytes( hint_length - static_cast<uint32_t>(inner.pc() - pc_after_hint_length)); if (!inner.ok()) break; compilation_priorities.emplace( func_index, CompilationPriority{compilation_priority, optimization_priority}); } // Extra unexpected bytes are an error. if (inner.more()) { inner.errorf("Unexpected extra bytes: %d\n", static_cast<int>(inner.pc() - inner.start())); } // If everything went well, accept the compilation priority hints for the // module. if (inner.ok()) { module_->compilation_priorities = std::move(compilation_priorities); } else if (v8_flags.trace_wasm_decoder || v8_flags.trace_wasm_compilation_hints) { PrintF("DecodeCompilationPriority error, offset %d: %s\n", inner.error().offset(), inner.error().message().c_str()); } } // Skip the whole compilation priority section in the outer decoder. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } void DecodeInstructionFrequenciesSection() { TRACE("DecodeInstructionFrequencies module+%d\n", static_cast<int>(pc_ - start_)); detected_features_->add_compilation_hints(); if (!has_seen_unordered_section(kInstFrequenciesSectionCode)) { set_seen_unordered_section(kInstFrequenciesSectionCode); // Use an inner decoder so that errors don't fail the outer decoder. Decoder inner(start_, pc_, end_, buffer_offset_); InstructionFrequencies frequencies; uint32_t func_count = inner.consume_u32v("number of functions"); int64_t last_func_index = -1; for (uint32_t i = 0; i < func_count; i++) { uint32_t func_index = inner.consume_u32v("function index"); if (static_cast<int64_t>(func_index) <= last_func_index) { inner.error("out of order functions"); break; } last_func_index = func_index; int64_t last_byte_offset = -1; uint32_t hints_count = inner.consume_u32v("hints count"); for (uint32_t hint = 0; hint < hints_count; hint++) { uint32_t byte_offset = inner.consume_u32v("byte offset"); if (static_cast<int64_t>(byte_offset) <= last_byte_offset) { inner.error("out of order hints"); break; } last_byte_offset = byte_offset; uint32_t hint_length = inner.consume_u32v("hint length"); if (hint_length == 0) { inner.error("hint length must be larger than 0"); break; } uint8_t frequency = inner.consume_u8("frequency"); if (!(frequency <= 64 || frequency == 127)) { inner.error("invalid frequency"); break; } // Skip remaining hint bytes. if (hint_length > 1) { inner.consume_bytes(hint_length - 1); } frequencies[func_index].emplace_back(byte_offset, frequency); } } // Extra unexpected bytes are an error. if (inner.more()) { inner.errorf("Unexpected extra bytes: %d\n", static_cast<int>(inner.pc() - inner.start())); } // If everything went well, accept the instruction-frequency hints for the // module. if (inner.ok()) { module_->instruction_frequencies = std::move(frequencies); } else if (v8_flags.trace_wasm_decoder || v8_flags.trace_wasm_compilation_hints) { PrintF("DecodeInstructionFrequencies error, offset %d: %s\n", inner.error().offset(), inner.error().message().c_str()); } } // Skip the whole instruction frequencies section in the outer decoder. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } void DecodeCallTargetsSection() { TRACE("DecodeCallTargets module+%d\n", static_cast<int>(pc_ - start_)); detected_features_->add_compilation_hints(); if (!has_seen_unordered_section(kCallTargetsSectionCode)) { set_seen_unordered_section(kCallTargetsSectionCode); // Use an inner decoder so that errors don't fail the outer decoder. Decoder inner(start_, pc_, end_, buffer_offset_); CallTargets call_targets; uint32_t func_count = inner.consume_u32v("number of functions"); int64_t last_func_index = -1; for (uint32_t i = 0; i < func_count; i++) { uint32_t func_index = inner.consume_u32v("function index"); if (static_cast<int64_t>(func_index) <= last_func_index) { inner.error("out of order functions"); break; } last_func_index = func_index; int64_t last_byte_offset = -1; uint32_t hints_count = inner.consume_u32v("hints count"); for (uint32_t hint = 0; hint < hints_count; hint++) { uint32_t byte_offset = inner.consume_u32v("byte offset"); if (static_cast<int64_t>(byte_offset) <= last_byte_offset) { inner.error("out of order hints"); break; } last_byte_offset = byte_offset; uint32_t hint_length = inner.consume_u32v("hint length"); if (hint_length == 0) { inner.error("hint length must be greater than 0"); break; } CallTargetVector call_targets_for_offset; while (hint_length > 0) { auto [function_index, function_index_length] = inner.read_u32v<FullValidationTag>(inner.pc(), "function index"); if (inner.failed()) break; if (function_index_length > hint_length) { inner.error("function length overflows declared hint length"); break; } hint_length -= function_index_length; inner.consume_bytes(function_index_length); auto [call_frequency, call_frequency_length] = inner.read_u32v<FullValidationTag>(inner.pc(), "call frequency"); if (inner.failed()) break; if (call_frequency_length > hint_length) { inner.error("call frequency overflows declared hint length"); break; } if (call_frequency > 100U) { inner.error("invalid call frequency percentage"); break; } hint_length -= call_frequency_length; inner.consume_bytes(call_frequency_length); call_targets_for_offset.emplace_back(function_index, call_frequency); } if (inner.failed()) break; DCHECK_EQ(hint_length, 0); uint32_t sum_of_percentages = 0; for (CallTarget& call_target : call_targets_for_offset) { sum_of_percentages += call_target.call_frequency_percent; } if (sum_of_percentages > 100U) { inner.error("percentages must sum to at most 100"); break; } call_targets[func_index].emplace_back(byte_offset, call_targets_for_offset); } if (inner.failed()) break; } // Extra unexpected bytes are an error. if (inner.more()) { inner.errorf("Unexpected extra bytes: %d\n", static_cast<int>(inner.pc() - inner.start())); } // If everything went well, accept the call-target hints for the module. if (inner.ok()) { module_->call_targets = std::move(call_targets); } else if (v8_flags.trace_wasm_decoder || v8_flags.trace_wasm_compilation_hints) { PrintF("DecodeCallTargets error, offset %d: %s\n", inner.error().offset(), inner.error().message().c_str()); } } // Skip the whole call-targets section in the outer decoder. consume_bytes(static_cast<uint32_t>(end_ - start_), nullptr); } void DecodeDataCountSection() { module_->num_declared_data_segments = consume_count("data segments count", kV8MaxWasmDataSegments); if (tracer_) tracer_->NextLineIfNonEmpty(); } void DecodeTagSection() { uint32_t tag_count = consume_count("tag count", kV8MaxWasmTags); for (uint32_t i = 0; ok() && i < tag_count; ++i) { TRACE("DecodeTag[%d] module+%d\n", i, static_cast<int>(pc_ - start_)); if (tracer_) tracer_->TagOffset(pc_offset()); const WasmTagSig* tag_sig = nullptr; consume_exception_attribute(); // Attribute ignored for now. ModuleTypeIndex sig_index = consume_tag_sig_index(module_.get(), &tag_sig); module_->tags.emplace_back(tag_sig, sig_index); } } void DecodeStringRefSection() { uint32_t deferred = consume_count("deferred string literal count", kV8MaxWasmStringLiterals); if (deferred) { errorf(pc(), "Invalid deferred string literal count %u (expected 0)", deferred); } uint32_t immediate = consume_count("string literal count", kV8MaxWasmStringLiterals - deferred); for (uint32_t i = 0; ok() && i < immediate; ++i) { TRACE("DecodeStringLiteral[%d] module+%d\n", i, static_cast<int>(pc_ - start_)); if (tracer_) tracer_->StringOffset(pc_offset()); // TODO(12868): Throw if the string's utf-16 length > String::kMaxLength. WireBytesRef pos = wasm::consume_string(this, unibrow::Utf8Variant::kWtf8, "string literal", tracer_); module_->stringref_literals.emplace_back(pos); } } bool CheckMismatchedCounts() { // The declared vs. defined function count is normally checked when // decoding the code section, but we have to check it here too in case the // code section is absent. if (module_->num_declared_functions != 0) { DCHECK_LT(module_->num_imported_functions, module_->functions.size()); // We know that the code section has been decoded if the first // non-imported function has its code set. if (!module_->functions[module_->num_imported_functions].code.is_set()) { errorf(pc(), "function count is %u, but code section is absent", module_->num_declared_functions); return false; } } // Perform a similar check for the DataCount and Data sections, where data // segments are declared but the Data section is absent. if (!CheckDataSegmentsCount( static_cast<uint32_t>(module_->data_segments.size()))) { return false; } return true; } ModuleResult FinishDecoding() { if (ok() && CheckMismatchedCounts()) { // We calculate the global offsets here, because there may not be a // global section and code section that would have triggered the // calculation before. Even without the globals section the calculation // is needed because globals can also be defined in the import section. CalculateGlobalOffsets(module_.get()); } if (module_->has_shared_part) detected_features_->add_shared(); return toResult(std::move(module_)); } // Decodes an entire module. ModuleResult DecodeModule(bool validate_functions) { // Keep a reference to the wire bytes, in case this decoder gets reset on // error. base::Vector<const uint8_t> wire_bytes(start_, end_ - start_); size_t max_size = max_module_size(); if (wire_bytes.size() > max_size) { return ModuleResult{WasmError{0, "size > maximum module size (%zu): %zu", max_size, wire_bytes.size()}}; } DecodeModuleHeader(wire_bytes); if (failed()) return toResult(nullptr); static constexpr uint32_t kWasmHeaderSize = 8; Decoder section_iterator_decoder(start_ + kWasmHeaderSize, end_, kWasmHeaderSize); WasmSectionIterator section_iter(§ion_iterator_decoder, tracer_); while (ok()) { if (section_iter.section_code() != SectionCode::kUnknownSectionCode) { uint32_t offset = static_cast<uint32_t>(section_iter.payload().begin() - wire_bytes.begin()); DecodeSection(section_iter.section_code(), section_iter.payload(), offset); if (!ok()) break; } if (!section_iter.more()) break; section_iter.advance(true); } // Check for module structure errors before validating function bodies, to // produce consistent error message independent of whether validation // happens here or later. if (section_iterator_decoder.failed()) { return section_iterator_decoder.toResult(nullptr); } ModuleResult result = FinishDecoding(); if (!result.failed() && validate_functions) { std::function<bool(int)> kNoFilter; if (WasmError validation_error = ValidateFunctions(module_.get(), enabled_features_, wire_bytes, kNoFilter, detected_features_)) { result = ModuleResult{validation_error}; } } if (v8_flags.dump_wasm_module) DumpModule(wire_bytes, result.ok()); return result; } // Decodes a single function signature at {start}. const FunctionSig* DecodeFunctionSignatureForTesting(const uint8_t* start) { pc_ = start; if (!expect_u8("type form", kWasmFunctionTypeCode)) return nullptr; const FunctionSig* result = consume_sig(); return ok() ? result : nullptr; } // Takes a module as parameter so that wasm-disassembler.cc can pass its own // module. ConstantExpression consume_element_segment_entry( WasmModule* module, const WasmElemSegment& segment) { if (segment.element_type == WasmElemSegment::kExpressionElements) { return consume_init_expr(module, segment.type, segment.shared); } else { return ConstantExpression::RefFunc( consume_element_func_index(module, segment.type)); } } const std::shared_ptr<WasmModule>& shared_module() const { return module_; } private: bool has_seen_unordered_section(SectionCode section_code) { return seen_unordered_sections_ & (1 << section_code); } void set_seen_unordered_section(SectionCode section_code) { seen_unordered_sections_ |= 1 << section_code; } uint32_t off(const uint8_t* ptr) { return static_cast<uint32_t>(ptr - start_) + buffer_offset_; } // Calculate individual global offsets and total size of globals table. This // function should be called after all globals have been defined, which is // after the import section and the global section, but before the global // offsets are accessed, e.g. by the function compilers. The moment when this // function should be called is not well-defined, as the global section may // not exist. Therefore this function is called multiple times. void CalculateGlobalOffsets(WasmModule* module) { if (module->globals.empty() || module->untagged_globals_buffer_size != 0 || module->tagged_globals_buffer_size != 0) { // This function has already been executed before, so we don't have to // execute it again. return; } uint32_t untagged_offset = 0; uint32_t tagged_offset = 0; uint32_t num_imported_mutable_globals = 0; for (WasmGlobal& global : module->globals) { if (global.mutability && global.imported) { global.index = num_imported_mutable_globals++; } else if (global.type.is_ref()) { global.offset = tagged_offset; // All entries in the tagged_globals_buffer have size 1. tagged_offset++; } else { int size = global.type.value_kind_size(); untagged_offset = (untagged_offset + size - 1) & ~(size - 1); // align global.offset = untagged_offset; untagged_offset += size; } } module->untagged_globals_buffer_size = untagged_offset; module->tagged_globals_buffer_size = tagged_offset; } ModuleTypeIndex consume_sig_index(WasmModule* module, const FunctionSig** sig) { const uint8_t* pos = pc_; ModuleTypeIndex sig_index{consume_u32v("signature index")}; if (tracer_) tracer_->Bytes(pos, static_cast<uint32_t>(pc_ - pos)); if (!module->has_signature(sig_index)) { errorf(pos, "no signature at index %u (%d types)", sig_index.index, static_cast<int>(module->types.size())); *sig = nullptr; return {}; } *sig = module->signature(sig_index); if (tracer_) { tracer_->Description(*sig); tracer_->NextLine(); } return sig_index; } ModuleTypeIndex consume_tag_sig_index(WasmModule* module, const FunctionSig** sig) { const uint8_t* pos = pc_; ModuleTypeIndex sig_index = consume_sig_index(module, sig); if (!enabled_features_.has_wasmfx() && *sig && (*sig)->return_count() != 0) { errorf(pos, "tag signature %u has non-void return", sig_index); *sig = nullptr; return {}; } return sig_index; } uint32_t consume_count(const char* name, size_t maximum) { const uint8_t* p = pc_; uint32_t count = consume_u32v(name, tracer_); if (tracer_) { tracer_->Description(count); if (count == 1) { tracer_->Description(": "); } else { tracer_->NextLine(); } } if (count > maximum) { errorf(p, "%s of %u exceeds internal limit of %zu", name, count, maximum); return 0; } return count; } uint32_t consume_func_index(WasmModule* module, WasmFunction** func) { return consume_index("function", &module->functions, func); } uint32_t consume_global_index(WasmModule* module, WasmGlobal** global) { return consume_index("global", &module->globals, global); } uint32_t consume_table_index(WasmModule* module, WasmTable** table) { return consume_index("table", &module->tables, table); } uint32_t consume_tag_index(WasmModule* module, WasmTag** tag) { return consume_index("tag", &module->tags, tag); } template <typename T> uint32_t consume_index(const char* name, std::vector<T>* vector, T** ptr) { const uint8_t* pos = pc_; uint32_t index = consume_u32v("index", tracer_); if (tracer_) { tracer_->Description(": "); tracer_->Description(index); } if (index >= vector->size()) { errorf(pos, "%s index %u out of bounds (%d entr%s)", name, index, static_cast<int>(vector->size()), vector->size() == 1 ? "y" : "ies"); *ptr = nullptr; return 0; } *ptr = &(*vector)[index]; return index; } // The limits byte structure is used for memories and tables. struct LimitsByte { uint8_t flags; // Flags 0..7 are valid (3 bits). bool is_valid() const { return (flags & ~0x7) == 0; } bool has_maximum() const { return flags & 0x1; } bool is_shared() const { return flags & 0x2; } bool is_64bit() const { return flags & 0x4; } AddressType address_type() const { return is_64bit() ? AddressType::kI64 : AddressType::kI32; } }; enum LimitsByteType { kMemory, kTable }; template <LimitsByteType limits_type> LimitsByte consume_limits_byte() { if (tracer_) tracer_->Bytes(pc_, 1); LimitsByte limits{consume_u8( limits_type == kMemory ? "memory limits flags" : "table limits flags")}; if (!limits.is_valid()) { errorf(pc() - 1, "invalid %s limits flags 0x%x", limits_type == kMemory ? "memory" : "table", limits.flags); } if (limits.is_shared()) { if constexpr (limits_type == kMemory) { // V8 does not support shared memory without a maximum. if (!limits.has_maximum()) { error(pc() - 1, "shared memory must have a maximum defined"); } // TODO(42204563): Implement proper handling of shared memories when // Shared Everything is enabled. } else if (!enabled_features_.has_shared()) { // table error(pc() - 1, "invalid table limits flags"); } else { // TODO(42204563): Support shared tables. error(pc() - 1, "shared tables are not supported yet"); } } if (tracer_) { if (limits.is_shared()) tracer_->Description(" shared"); if (limits.is_64bit()) { tracer_->Description(limits_type == kMemory ? " mem64" : " table64"); } tracer_->Description(limits.has_maximum() ? " with maximum" : " no maximum"); tracer_->NextLine(); } return limits; } void consume_table_flags(WasmTable* table) { LimitsByte limits = consume_limits_byte<kTable>(); table->has_maximum_size = limits.has_maximum(); table->shared = limits.is_shared(); table->address_type = limits.address_type(); if (table->is_table64()) detected_features_->add_memory64(); } void consume_memory_flags(WasmMemory* memory) { LimitsByte limits = consume_limits_byte<kMemory>(); memory->has_maximum_pages = limits.has_maximum(); memory->is_shared = limits.is_shared(); memory->address_type = limits.address_type(); if (memory->is_shared) detected_features_->add_shared_memory(); if (memory->is_memory64()) detected_features_->add_memory64(); } std::pair<bool, bool> consume_global_flags() { uint8_t flags = consume_u8("global flags"); if (flags & ~0b11) { errorf(pc() - 1, "invalid global flags 0x%x", flags); return {false, false}; } bool mutability = flags & 0b1; bool shared = flags & 0b10; if (tracer_) { tracer_->Bytes(pc_ - 1, 1); // The flags byte. if (shared) tracer_->Description(" shared"); tracer_->Description(mutability ? " mutable" : " immutable"); } if (shared && !enabled_features_.has_shared()) { errorf(pc() - 1, "invalid global flags 0x%x", flags); return {false, false}; } if (shared) { // TODO(42204563): Support shared globals. error(pc() - 1, "shared globals are not supported yet"); return {false, false}; } return {mutability, shared}; } enum ResizableLimitsType : bool { k32BitLimits, k64BitLimits }; void consume_resizable_limits( const char* name, const char* units, // Not: both memories and tables have a 32-bit limit on the initial size. uint32_t max_initial, uint32_t* initial, bool has_maximum, uint64_t max_maximum, uint64_t* maximum, ResizableLimitsType type) { const uint8_t* pos = pc(); // Note that even if we read the values as 64-bit value, all V8 limits are // still within uint32_t range. uint64_t initial_64 = type == k64BitLimits ? consume_u64v("initial size", tracer_) : consume_u32v("initial size", tracer_); if (initial_64 > max_initial) { errorf(pos, "initial %s size (%" PRIu64 " %s) is larger than implementation limit (%u %s)", name, initial_64, units, max_initial, units); } *initial = static_cast<uint32_t>(initial_64); if (tracer_) { tracer_->Description(*initial); tracer_->NextLine(); } if (has_maximum) { pos = pc(); uint64_t maximum_64 = type == k64BitLimits ? consume_u64v("maximum size", tracer_) : consume_u32v("maximum size", tracer_); if (maximum_64 > max_maximum) { errorf(pos, "maximum %s size (%" PRIu64 " %s) is larger than implementation limit (%" PRIu64 " %s)", name, maximum_64, units, max_maximum, units); } if (maximum_64 < *initial) { errorf(pos, "maximum %s size (%" PRIu64 " %s) is less than initial (%u %s)", name, maximum_64, units, *initial, units); } *maximum = maximum_64; if (tracer_) { tracer_->Description(*maximum); tracer_->NextLine(); } } else { *maximum = max_initial; } } // Consumes a byte, and emits an error if it does not equal {expected}. bool expect_u8(const char* name, uint8_t expected) { const uint8_t* pos = pc(); uint8_t value = consume_u8(name); if (value != expected) { errorf(pos, "expected %s 0x%02x, got 0x%02x", name, expected, value); return false; } return true; } ConstantExpression consume_init_expr(WasmModule* module, ValueType expected, bool is_shared, bool* ends_with_struct_new = nullptr) { // The error message mimics the one generated by the {WasmFullDecoder}. #define TYPE_CHECK(found) \ if (V8_UNLIKELY(!IsSubtypeOf(found, expected, module))) { \ errorf(pc() + 1, \ "type error in constant expression[0] (expected %s, got %s)", \ expected.name().c_str(), found.name().c_str()); \ return {}; \ } if (tracer_) tracer_->NextLineIfNonEmpty(); // To avoid initializing a {WasmFullDecoder} for the most common // expressions, we replicate their decoding and validation here. The // manually handled cases correspond to {ConstantExpression}'s kinds. // We need to make sure to check that the expression ends in {kExprEnd}; // otherwise, it is just the first operand of a composite expression, and we // fall back to the default case. if (!more()) { error("Beyond end of code"); return {}; } switch (static_cast<WasmOpcode>(*pc())) { case kExprI32Const: { auto [value, length] = read_i32v<FullValidationTag>(pc() + 1, "i32.const"); if (V8_UNLIKELY(failed())) return {}; if (V8_LIKELY(lookahead(1 + length, kExprEnd))) { TYPE_CHECK(kWasmI32) if (tracer_) { tracer_->InitializerExpression(pc_, pc_ + length + 2, kWasmI32); } consume_bytes(length + 2); return ConstantExpression::I32Const(value); } break; } case kExprRefFunc: { auto [index, length] = read_u32v<FullValidationTag>(pc() + 1, "ref.func"); if (V8_UNLIKELY(failed())) return {}; if (V8_LIKELY(lookahead(1 + length, kExprEnd))) { if (V8_UNLIKELY(index >= module->functions.size())) { errorf(pc() + 1, "function index %u out of bounds", index); return {}; } ModuleTypeIndex functype{module->functions[index].sig_index}; bool functype_is_shared = module->type(functype).is_shared; ValueType type = ValueType::Ref(functype, functype_is_shared, RefTypeKind::kFunction); if (enabled_features_.has_custom_descriptors() && (index >= module->num_imported_functions || module->functions[index].exact)) { type = type.AsExact(); } TYPE_CHECK(type) if (V8_UNLIKELY(is_shared && !type.is_shared())) { error(pc(), "ref.func does not have a shared type"); return {}; } module->functions[index].declared = true; if (tracer_) { tracer_->InitializerExpression(pc_, pc_ + length + 2, type); } consume_bytes(length + 2); return ConstantExpression::RefFunc(index); } break; } case kExprRefNull: { auto [type, length] = value_type_reader::read_heap_type<FullValidationTag>( this, pc() + 1, enabled_features_, detected_features_); value_type_reader::ValidateHeapType<FullValidationTag>(this, pc_, module, type); if (V8_UNLIKELY(failed())) return {}; value_type_reader::Populate(&type, module); if (V8_LIKELY(lookahead(1 + length, kExprEnd))) { TYPE_CHECK(ValueType::RefNull(type)) if (V8_UNLIKELY(is_shared && !type.is_shared())) { error(pc(), "ref.null does not have a shared type"); return {}; } if (tracer_) { tracer_->InitializerExpression(pc_, pc_ + length + 2, ValueType::RefNull(type)); } consume_bytes(length + 2); return ConstantExpression::RefNull(type); } break; } default: break; } #undef TYPE_CHECK auto sig = FixedSizeSignature<ValueType>::Returns(expected); FunctionBody body(&sig, this->pc_offset(), pc_, end_, is_shared); WasmDetectedFeatures detected; ConstantExpression result; { // We need a scope for the decoder because its destructor resets some Zone // elements, which has to be done before we reset the Zone afterwards. WasmFullDecoder<Decoder::FullValidationTag, ConstantExpressionInterface, kConstantExpression> decoder(&init_expr_zone_, module, enabled_features_, &detected, body, module); uint32_t offset = this->pc_offset(); decoder.DecodeFunctionBody(); if (tracer_) { // In case of error, decoder.end() is set to the position right before // the byte(s) that caused the error. For debugging purposes, we should // print these bytes, but we don't know how many of them there are, so // for now we have to guess. For more accurate behavior, we'd have to // pass {num_invalid_bytes} to every {decoder->DecodeError()} call. static constexpr size_t kInvalidBytesGuess = 4; const uint8_t* end = decoder.ok() ? decoder.end() : std::min(decoder.end() + kInvalidBytesGuess, end_); tracer_->InitializerExpression(pc_, end, expected); } this->pc_ = decoder.end(); if (decoder.failed()) { error(decoder.error().offset(), decoder.error().message().c_str()); return {}; } if (!decoder.interface().end_found()) { error("constant expression is missing 'end'"); return {}; } result = ConstantExpression::WireBytes( offset, static_cast<uint32_t>(decoder.end() - decoder.start())); if (ends_with_struct_new) { *ends_with_struct_new = decoder.interface().ends_with_struct_new(); } } // We reset the zone here; its memory is not used anymore, and we do not // want memory from all constant expressions to add up. init_expr_zone_.Reset(); return result; } // Read a mutability flag bool consume_mutability() { if (tracer_) tracer_->Bytes(pc_, 1); uint8_t val = consume_u8("mutability"); if (tracer_) { tracer_->Description(val == 0 ? " immutable" : val == 1 ? " mutable" : " invalid"); } if (val > 1) error(pc_ - 1, "invalid mutability"); return val != 0; } // Pass a {module} to get a pre-{Populate()}d ValueType. That's safe // when the module's type section has already been fully decoded. ValueType consume_value_type(const WasmModule* module = nullptr) { auto [result, length] = value_type_reader::read_value_type<FullValidationTag>( this, pc_, module_->origin == kWasmOrigin ? enabled_features_ : WasmEnabledFeatures::None(), detected_features_); value_type_reader::ValidateValueType<FullValidationTag>( this, pc_, module_.get(), result); if (ok() && module) value_type_reader::Populate(&result, module); if (tracer_) { tracer_->Bytes(pc_, length); tracer_->Description(result); } consume_bytes(length, "value type"); return result; } HeapType consume_heap_type() { auto [heap_type, length] = value_type_reader::read_heap_type<FullValidationTag>( this, pc_, module_->origin == kWasmOrigin ? enabled_features_ : WasmEnabledFeatures::None(), detected_features_); value_type_reader::ValidateHeapType<FullValidationTag>( this, pc_, module_.get(), heap_type); if (tracer_) { tracer_->Bytes(pc_, length); tracer_->Description(heap_type); } consume_bytes(length, "heap type"); return heap_type; } ValueType consume_storage_type() { uint8_t opcode = read_u8<FullValidationTag>(this->pc()); switch (opcode) { case kI8Code: consume_bytes(1, " i8", tracer_); return kWasmI8; case kI16Code: consume_bytes(1, " i16", tracer_); return kWasmI16; default: // It is not a packed type, so it has to be a value type. return consume_value_type(); } } const FunctionSig* consume_sig() { if (tracer_) tracer_->NextLine(); // Parse parameter types. uint32_t param_count = consume_count("param count", kV8MaxWasmFunctionParams); // We don't know the return count yet, so decode the parameters into a // temporary SmallVector. This needs to be copied over into the permanent // storage later. base::SmallVector<ValueType, 8> params{param_count}; for (uint32_t i = 0; i < param_count; ++i) { params[i] = consume_value_type(); if (tracer_) tracer_->NextLineIfFull(); } if (tracer_) tracer_->NextLineIfNonEmpty(); // Parse return types. uint32_t return_count = consume_count("return count", kV8MaxWasmFunctionReturns); if (return_count > 1) { detected_features_->add_multi_value(); } // Now that we know the param count and the return count, we can allocate // the permanent storage. FunctionSig::Builder sig_builder{&module_->signature_storage, return_count, param_count}; for (ValueType t : params) sig_builder.AddParam(t); for (uint32_t i = 0; i < return_count; ++i) { sig_builder.AddReturn(consume_value_type()); if (tracer_) tracer_->NextLineIfFull(); } if (tracer_) tracer_->NextLineIfNonEmpty(); return sig_builder.Get(); } const StructType* consume_struct(bool is_descriptor, bool is_shared) { uint32_t field_count = consume_count(", field count", kV8MaxWasmStructFields); if (failed()) return nullptr; ValueType* fields = module_->signature_storage.AllocateArray<ValueType>(field_count); bool* mutabilities = module_->signature_storage.AllocateArray<bool>(field_count); for (uint32_t i = 0; ok() && i < field_count; ++i) { fields[i] = consume_storage_type(); mutabilities[i] = consume_mutability(); if (tracer_) tracer_->NextLine(); } if (failed()) return nullptr; uint32_t* offsets = module_->signature_storage.AllocateArray<uint32_t>(field_count); StructType* result = module_->signature_storage.New<StructType>( field_count, offsets, fields, mutabilities, is_descriptor, is_shared); result->InitializeOffsets(); return result; } const ArrayType* consume_array() { ValueType element_type = consume_storage_type(); bool mutability = consume_mutability(); if (tracer_) tracer_->NextLine(); if (failed()) return nullptr; return module_->signature_storage.New<ArrayType>(element_type, mutability); } // Consume the attribute field of an exception. uint32_t consume_exception_attribute() { const uint8_t* pos = pc_; uint32_t attribute = consume_u32v("exception attribute"); if (tracer_) tracer_->Bytes(pos, static_cast<uint32_t>(pc_ - pos)); if (attribute != kExceptionAttribute) { errorf(pos, "exception attribute %u not supported", attribute); return 0; } return attribute; } WasmElemSegment consume_element_segment_header() { const uint8_t* pos = pc(); // The mask for the bit in the flag which indicates if the segment is // active or not (0 is active). constexpr uint8_t kNonActiveMask = 1 << 0; // The mask for the bit in the flag which indicates: // - for active tables, if the segment has an explicit table index field. // - for non-active tables, whether the table is declarative (vs. passive). constexpr uint8_t kHasTableIndexOrIsDeclarativeMask = 1 << 1; // The mask for the bit in the flag which indicates if the functions of this // segment are defined as function indices (0) or constant expressions (1). constexpr uint8_t kExpressionsAsElementsMask = 1 << 2; // The mask for the bit which denotes whether this segment is shared. constexpr uint8_t kSharedFlag = 1 << 3; constexpr uint8_t kFullMask = kNonActiveMask | kHasTableIndexOrIsDeclarativeMask | kExpressionsAsElementsMask | kSharedFlag; uint32_t flag = consume_u32v("flag", tracer_); if ((flag & kFullMask) != flag) { errorf(pos, "illegal flag value %u", flag); return {}; } bool is_shared = flag & kSharedFlag; if (is_shared && !enabled_features_.has_shared()) { errorf(pos, "illegal flag value %u", flag); return {}; } if (is_shared) { // TODO(42204563): Support shared element segments. error(pos, "shared element segments are not supported yet."); return {}; } if (is_shared) module_->has_shared_part = true; const WasmElemSegment::Status status = (flag & kNonActiveMask) ? (flag & kHasTableIndexOrIsDeclarativeMask) ? WasmElemSegment::kStatusDeclarative : WasmElemSegment::kStatusPassive : WasmElemSegment::kStatusActive; const bool is_active = status == WasmElemSegment::kStatusActive; if (tracer_) { tracer_->Description(": "); tracer_->Description(status == WasmElemSegment::kStatusActive ? "active" : status == WasmElemSegment::kStatusPassive ? "passive," : "declarative,"); } WasmElemSegment::ElementType element_type = flag & kExpressionsAsElementsMask ? WasmElemSegment::kExpressionElements : WasmElemSegment::kFunctionIndexElements; const bool has_table_index = is_active && (flag & kHasTableIndexOrIsDeclarativeMask); uint32_t table_index = 0; if (has_table_index) { table_index = consume_u32v(", table index", tracer_); if (tracer_) tracer_->Description(table_index); } if (V8_UNLIKELY(is_active && table_index >= module_->tables.size())) { // If `has_table_index`, we have an explicit table index. Otherwise, we // always have the implicit table index 0. errorf(pos, "out of bounds%s table index %u", has_table_index ? "" : " implicit", table_index); return {}; } ValueType table_type = is_active ? module_->tables[table_index].type : kWasmBottom; ConstantExpression offset; if (is_active) { if (tracer_) { tracer_->Description(", offset:"); tracer_->NextLine(); } offset = consume_init_expr( module_.get(), module_->tables[table_index].is_table64() ? kWasmI64 : kWasmI32, is_shared); // Failed to parse offset initializer, return early. if (failed()) return {}; } // Denotes an active segment without table index, type, or element kind. const bool backwards_compatible_mode = is_active && !(flag & kHasTableIndexOrIsDeclarativeMask); ValueType type; if (element_type == WasmElemSegment::kExpressionElements) { if (backwards_compatible_mode) { type = kWasmFuncRef; } else { if (tracer_) tracer_->Description(" element type:"); type = consume_value_type(module_.get()); if (failed()) return {}; } } else { if (!backwards_compatible_mode) { // We have to check that there is an element kind of type Function. All // other element kinds are not valid yet. if (tracer_) tracer_->Description(" "); uint8_t val = consume_u8("element type: function", tracer_); if (V8_UNLIKELY(static_cast<ImportExportKindCode>(val) != kExternalFunction)) { errorf(pos, "illegal element kind 0x%x. Must be 0x%x", val, kExternalFunction); return {}; } } type = kWasmFuncRef.AsNonNull(); } if (V8_UNLIKELY(is_active && !IsSubtypeOf(type, table_type, this->module_.get()))) { errorf(pos, "Element segment of type %s is not a subtype of referenced " "table %u (of type %s)", type.name().c_str(), table_index, table_type.name().c_str()); return {}; } // TODO(14616): Is this too restrictive? if (V8_UNLIKELY(is_active && (is_shared != module_->tables[table_index].shared))) { error(pos, "Shared (resp. non-shared) element segments must refer to shared " "(resp. non-shared) tables"); return {}; } uint32_t num_elem = consume_count(" number of elements", max_table_init_entries()); if (is_active) { return {is_shared, type, table_index, std::move(offset), element_type, num_elem, pc_offset()}; } else { return {status, is_shared, type, element_type, num_elem, pc_offset()}; } } DataSegmentHeader consume_data_segment_header() { const uint8_t* pos = pc(); uint32_t flag = consume_u32v("flag", tracer_); if (flag & ~0b1011) { errorf(pos, "illegal flag value %u", flag); return {}; } uint32_t status_flag = flag & 0b11; if (tracer_) { tracer_->Description(": "); tracer_->Description( status_flag == SegmentFlags::kActiveNoIndex ? "active no index" : status_flag == SegmentFlags::kPassive ? "passive" : status_flag == SegmentFlags::kActiveWithIndex ? "active with index" : "unknown"); } if (status_flag != SegmentFlags::kActiveNoIndex && status_flag != SegmentFlags::kPassive && status_flag != SegmentFlags::kActiveWithIndex) { errorf(pos, "illegal flag value %u", flag); return {}; } bool is_shared = flag & 0b1000; if (V8_UNLIKELY(is_shared && !enabled_features_.has_shared())) { errorf(pos, "illegal flag value %u.", flag); return {}; } if (V8_UNLIKELY(is_shared)) { // TODO(42204563): Support shared data segments. error(pos, "shared data segments are not supported yet."); return {}; } if (is_shared) module_->has_shared_part = true; if (tracer_) { if (is_shared) tracer_->Description(" shared"); tracer_->NextLine(); } bool is_active = status_flag == SegmentFlags::kActiveNoIndex || status_flag == SegmentFlags::kActiveWithIndex; uint32_t mem_index = status_flag == SegmentFlags::kActiveWithIndex ? consume_u32v("memory index", tracer_) : 0; ConstantExpression offset; if (is_active) { size_t num_memories = module_->memories.size(); if (mem_index >= num_memories) { errorf(pos, "invalid memory index %u for data section (having %zu memor%s)", mem_index, num_memories, num_memories == 1 ? "y" : "ies"); return {}; } ValueType expected_type = module_->memories[mem_index].is_memory64() ? kWasmI64 : kWasmI32; offset = consume_init_expr(module_.get(), expected_type, is_shared); } return {is_active, is_shared, mem_index, offset}; } uint32_t consume_element_func_index(WasmModule* module, ValueType expected) { WasmFunction* func = nullptr; const uint8_t* initial_pc = pc(); uint32_t index = consume_func_index(module, &func); if (tracer_) tracer_->NextLine(); if (failed()) return index; DCHECK_NOT_NULL(func); DCHECK_EQ(index, func->func_index); ValueType entry_type = ValueType::Ref(func->sig_index, module->type(func->sig_index).is_shared, RefTypeKind::kFunction); if (V8_LIKELY(expected == kWasmFuncRef && !enabled_features_.has_shared())) { DCHECK(module->type(func->sig_index).kind == TypeDefinition::kFunction); DCHECK(IsSubtypeOf(entry_type, expected, module)); } else if (V8_UNLIKELY(!IsSubtypeOf(entry_type, expected, module))) { errorf(initial_pc, "Invalid type in element entry: expected %s, got %s instead.", expected.name().c_str(), entry_type.name().c_str()); return index; } func->declared = true; return index; } const WasmEnabledFeatures enabled_features_; WasmDetectedFeatures* const detected_features_; const std::shared_ptr<WasmModule> module_; const uint8_t* module_start_ = nullptr; const uint8_t* module_end_ = nullptr; ITracer* tracer_; // The type section is the first section in a module. uint8_t next_ordered_section_ = kFirstSectionInModule; // We store next_ordered_section_ as uint8_t instead of SectionCode so that // we can increment it. This static_assert should make sure that SectionCode // does not get bigger than uint8_t accidentally. static_assert(sizeof(ModuleDecoderImpl::next_ordered_section_) == sizeof(SectionCode), "type mismatch"); uint32_t seen_unordered_sections_ = 0; static_assert(kBitsPerByte * sizeof(ModuleDecoderImpl::seen_unordered_sections_) > kLastKnownModuleSection, "not enough bits"); AccountingAllocator allocator_; // We pass this {Zone} to the temporary {WasmFullDecoder} we allocate during // each call to {consume_init_expr}, and reset it after each such call. This // has been found to improve performance a bit over allocating a new {Zone} // each time. Zone init_expr_zone_{&allocator_, "constant expr. zone"}; // Instruction traces are decoded in DecodeInstTraceSection as a 3-tuple // of the function index, function offset, and mark_id. In DecodeCodeSection, // after the functions have been decoded this is translated to pairs of module // offsets and mark ids. std::vector<std::tuple<uint32_t, uint32_t, uint32_t>> inst_traces_; }; } // namespace v8::internal::wasm #undef TRACE #endif // V8_WASM_MODULE_DECODER_IMPL_H_