diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 714a855be4a..cdb9d9bfa02 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -394,6 +394,9 @@ def get_tests(test_dir, extensions=[], recursive=False): # Test invalid 'elem.wast', + + # Requires module splitting to work with module instances + 'instance.wast', ] SPEC_TESTSUITE_PROPOSALS_TO_SKIP = [ 'custom-page-sizes', @@ -423,8 +426,8 @@ def get_tests(test_dir, extensions=[], recursive=False): 'proposals/threads/memory.wast', # Missing memory type validation on instantiation 'annotations.wast', # String annotations IDs should be allowed 'id.wast', # Empty IDs should be disallowed - # Requires correct handling of tag imports from different instances of the same module - # and splitting for module instances + # Requires support for table default elements and + # requires module splitting to work with module instances 'instance.wast', 'table64.wast', # Requires validations for table size 'tag.wast', # Non-empty tag results allowed by stack switching diff --git a/src/ir/import-utils.h b/src/ir/import-utils.h index 481d62af7f1..9d6b4cacbeb 100644 --- a/src/ir/import-utils.h +++ b/src/ir/import-utils.h @@ -137,6 +137,8 @@ class ImportResolver { // as long as the ImportResolver instance. virtual RuntimeTable* getTableOrNull(ImportNames name, const Table& type) const = 0; + + virtual Tag* getTagOrNull(ImportNames name, const Signature& type) const = 0; }; // Looks up imports from the given `linkedInstances`. @@ -168,6 +170,16 @@ class LinkedInstancesImportResolver : public ImportResolver { return instance->getExportedTableOrNull(name.name); } + Tag* getTagOrNull(ImportNames name, const Signature& type) const override { + auto it = linkedInstances.find(name.module); + if (it == linkedInstances.end()) { + return nullptr; + } + + ModuleRunnerType* instance = it->second.get(); + return instance->getExportedTagOrNull(name.name); + } + private: const std::map> linkedInstances; }; diff --git a/src/shell-interface.h b/src/shell-interface.h index d35bc9bb65e..615000a7fba 100644 --- a/src/shell-interface.h +++ b/src/shell-interface.h @@ -151,10 +151,6 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { return Literal::makeFunc(import->name, import->type); } - Tag* getImportedTag(Tag* tag) override { - WASM_UNREACHABLE("missing imported tag"); - } - int8_t load8s(Address addr, Name memoryName) override { auto it = memories.find(memoryName); assert(it != memories.end()); diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index bec95777bae..7df3e3939da 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -23,8 +23,37 @@ namespace wasm { +namespace { + using Loggings = std::vector; +Tag& getWasmTag() { + static Tag tag = []() { + Tag tag; + tag.module = "fuzzing-support"; + tag.base = "wasmtag"; + tag.name = "imported-wasm-tag"; + tag.type = Signature(Type::i32, Type::none); + + return tag; + }(); + return tag; +} + +Tag& getJsTag() { + static Tag tag = []() { + Tag tag; + tag.module = "fuzzing-support"; + tag.base = "jstag"; + tag.name = "imported-js-tag"; + tag.type = Signature(Type(HeapType::ext, Nullable), Type::none); + return tag; + }(); + return tag; +} + +} // namespace + // Logs every relevant import call parameter. struct LoggingExternalInterface : public ShellExternalInterface { private: @@ -43,10 +72,10 @@ struct LoggingExternalInterface : public ShellExternalInterface { Module& wasm; // The imported fuzzing tag for wasm. - Tag wasmTag; + const Tag& wasmTag; // The imported tag for js exceptions. - Tag jsTag; + const Tag& jsTag; // The ModuleRunner and this ExternalInterface end up needing links both ways, // so we cannot init this in the constructor. @@ -57,34 +86,14 @@ struct LoggingExternalInterface : public ShellExternalInterface { Loggings& loggings, Module& wasm, std::map> linkedInstances_ = {}) - : ShellExternalInterface(linkedInstances_), loggings(loggings), wasm(wasm) { + : ShellExternalInterface(linkedInstances_), loggings(loggings), wasm(wasm), + wasmTag(getWasmTag()), jsTag(getJsTag()) { for (auto& exp : wasm.exports) { if (exp->kind == ExternalKind::Table && exp->name == "table") { exportedTable = *exp->getInternalName(); break; } } - - // Set up tags. (Setting these values is useful for debugging - making the - // Tag objects valid - and also appears in fuzz-exec logging.) - wasmTag.module = "fuzzing-support"; - wasmTag.base = "wasmtag"; - wasmTag.name = "imported-wasm-tag"; - wasmTag.type = Signature(Type::i32, Type::none); - - jsTag.module = "fuzzing-support"; - jsTag.base = "jstag"; - jsTag.name = "imported-js-tag"; - jsTag.type = Signature(Type(HeapType::ext, Nullable), Type::none); - } - - Tag* getImportedTag(Tag* tag) override { - for (auto* imported : {&wasmTag, &jsTag}) { - if (imported->module == tag->module && imported->base == tag->base) { - return imported; - } - } - Fatal() << "missing host tag " << tag->module << '.' << tag->base; } Literal getImportedFunction(Function* import) override { @@ -299,6 +308,27 @@ struct LoggingExternalInterface : public ShellExternalInterface { void setModuleRunner(ModuleRunner* instance_) { instance = instance_; } }; +class FuzzerImportResolver + : public LinkedInstancesImportResolver { + using LinkedInstancesImportResolver::LinkedInstancesImportResolver; + Tag* getTagOrNull(ImportNames name, const Signature& type) const override { + if (name.module == "fuzzing-support") { + if (name.name == "wasmtag") { + return &wasmTag; + } + if (name.name == "jstag") { + return &jsTag; + } + } + + return LinkedInstancesImportResolver::getTagOrNull(name, type); + } + +private: + Tag& wasmTag = getWasmTag(); + Tag& jsTag = getJsTag(); +}; + // gets execution results from a wasm module. this is useful for fuzzing // // we can only get results when there are no imports. we then call each method @@ -319,12 +349,19 @@ struct ExecutionResults { try { // Instantiate the first module. LoggingExternalInterface interface(loggings, wasm); - auto instance = std::make_shared(wasm, &interface); + + // `linkedInstances` is empty at this point and the below constructors + // make copies. + std::map> linkedInstances; + auto instance = std::make_shared( + wasm, + &interface, + linkedInstances, + std::make_shared(linkedInstances)); instantiate(*instance, interface); // Instantiate the second, if there is one (we instantiate both before // running anything, so that we match the behavior of fuzz_shell.js). - std::map> linkedInstances; std::unique_ptr secondInterface; std::shared_ptr secondInstance; if (second) { diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index fb57865999d..ac8045e547b 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -84,6 +84,12 @@ class EvallingImportResolver : public ImportResolver { throw FailToEvalException{"Imported table access."}; } + Tag* getTagOrNull(ImportNames name, + const Signature& signature) const override { + Fatal() << "getTagOrNull not implemented in ctor-eval."; + return nullptr; + } + private: mutable Literals stubLiteral; }; @@ -393,10 +399,6 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { import->type); } - Tag* getImportedTag(Tag* tag) override { - WASM_UNREACHABLE("missing imported tag"); - } - int8_t load8s(Address addr, Name memoryName) override { return doLoad(addr, memoryName); } diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index cf51b3d8f72..9e44153afa3 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -170,12 +170,12 @@ struct ExnData { // The tag of this exn data. // TODO: Add self, like in FuncData, to handle the case of a module that is // instantiated multiple times. - Tag* tag; + const Tag* tag; // The payload of this exn data. Literals payload; - ExnData(Tag* tag, Literals payload) : tag(tag), payload(payload) {} + ExnData(const Tag* tag, Literals payload) : tag(tag), payload(payload) {} }; // Suspend/resume support. @@ -2977,9 +2977,6 @@ class ModuleRunnerBase : public ExpressionRunner { virtual void trap(std::string_view why) = 0; virtual void hostLimit(std::string_view why) = 0; virtual void throwException(const WasmException& exn) = 0; - // Get the Tag instance for a tag implemented in the host, that is, not - // among the linked ModuleRunner instances, but imported from the host. - virtual Tag* getImportedTag(Tag* tag) = 0; // the default impls for load and store switch on the sizes. you can either // customize load/store, or the sub-functions which they call @@ -3173,6 +3170,8 @@ class ModuleRunnerBase : public ExpressionRunner { // Like `allGlobals`. Keyed by internal name. All tables including imports. std::unordered_map allTables; + std::unordered_map allTags; + using CreateTableFunc = std::unique_ptr(Literal, Table); ModuleRunnerBase( @@ -3223,6 +3222,7 @@ class ModuleRunnerBase : public ExpressionRunner { initializeGlobals(); initializeTables(); + initializeTags(); initializeMemoryContents(); @@ -3296,16 +3296,28 @@ class ModuleRunnerBase : public ExpressionRunner { return *global; } - Tag* getExportedTag(Name name) { + Tag* getExportedTagOrNull(Name name) { Export* export_ = wasm.getExportOrNull(name); if (!export_ || export_->kind != ExternalKind::Tag) { - externalInterface->trap("exported tag not found"); + return nullptr; } - auto* tag = wasm.getTag(*export_->getInternalName()); - if (tag->imported()) { - tag = externalInterface->getImportedTag(tag); + Name internalName = *export_->getInternalName(); + auto it = allTags.find(internalName); + if (it == allTags.end()) { + return nullptr; } - return tag; + return it->second; + } + + Tag& getExportedTagOrTrap(Name name) { + auto* tag = getExportedTagOrNull(name); + if (!tag) { + externalInterface->trap((std::stringstream() << "getExportedTag: export " + << name << " not found.") + .str()); + } + + return *tag; } std::string printFunctionStack() { @@ -3323,6 +3335,7 @@ class ModuleRunnerBase : public ExpressionRunner { // internal name. std::vector definedGlobals; std::vector> definedTables; + std::vector definedTags; // Keep a record of call depth, to guard against excessive recursion. size_t callDepth = 0; @@ -3441,24 +3454,58 @@ class ModuleRunnerBase : public ExpressionRunner { << " not found.") .str()); } - auto [_, inserted] = + // Unused in noassert builds + [[maybe_unused]] auto [_, inserted] = allGlobals.try_emplace(global->name, importedGlobal); - (void)inserted; // for noassert builds // parsing/validation checked this already. assert(inserted && "Unexpected repeated global name"); } else { Literals init = self()->visit(global->init).values; auto& definedGlobal = definedGlobals.emplace_back(std::move(init)); - auto [_, inserted] = + // Unused in noassert builds + [[maybe_unused]] auto [_, inserted] = allGlobals.try_emplace(global->name, &definedGlobal); - (void)inserted; // for noassert builds // parsing/validation checked this already. assert(inserted && "Unexpected repeated global name"); } } } + void initializeTags() { + int definedTagCount = 0; + ModuleUtils::iterDefinedTags( + wasm, [&definedTagCount](auto&& _) { ++definedTagCount; }); + definedTags.reserve(definedTagCount); + + for (auto& tag : wasm.tags) { + if (tag->imported()) { + auto importNames = tag->importNames(); + auto importedTag = + importResolver->getTagOrNull(importNames, tag->type.getSignature()); + if (!importedTag) { + externalInterface->trap((std::stringstream() + << "Imported tag " << importNames + << " not found.") + .str()); + } + // Unused in noassert builds + [[maybe_unused]] auto [_, inserted] = + allTags.try_emplace(tag->name, importedTag); + // parsing/validation checked this already. + assert(inserted && "Unexpected repeated tag name"); + } else { + auto& definedTag = definedTags.emplace_back(*tag); + + // Unused in noassert builds + [[maybe_unused]] auto [_, inserted] = + allTags.try_emplace(tag->name, &definedTag); + // parsing/validation checked this already. + assert(inserted && "Unexpected repeated tag name"); + } + } + } + void initializeTables() { int definedTableCount = 0; ModuleUtils::iterDefinedTables( @@ -3476,8 +3523,9 @@ class ModuleRunnerBase : public ExpressionRunner { << " not found.") .str()); } - auto [_, inserted] = allTables.try_emplace(table->name, importedTable); - (void)inserted; // for noassert builds + // Unused in noassert builds + [[maybe_unused]] auto [_, inserted] = + allTables.try_emplace(table->name, importedTable); // parsing/validation checked this already. assert(inserted && "Unexpected repeated table name"); } else { @@ -3487,9 +3535,9 @@ class ModuleRunnerBase : public ExpressionRunner { auto null = Literal::makeNull(table->type.getHeapType()); auto& runtimeTable = definedTables.emplace_back(createTable(null, *table)); - auto [_, inserted] = + // Unused in noassert builds + [[maybe_unused]] auto [_, inserted] = allTables.try_emplace(table->name, runtimeTable.get()); - (void)inserted; // for noassert builds assert(inserted && "Unexpected repeated table name"); } } @@ -3702,24 +3750,6 @@ class ModuleRunnerBase : public ExpressionRunner { return inst->getExportedFunction(func->base); } - // Get a tag object while looking through imports, i.e., this uses the name as - // the name of the tag in the current module, and finds the actual canonical - // Tag* object for it: the Tag in this module, if not imported, and if - // imported, the Tag in the originating module. - Tag* getCanonicalTag(Name name) { - auto* inst = self(); - auto* tag = inst->wasm.getTag(name); - if (!tag->imported()) { - return tag; - } - auto iter = inst->linkedInstances.find(tag->module); - if (iter == inst->linkedInstances.end()) { - return externalInterface->getImportedTag(tag); - } - inst = iter->second.get(); - return inst->getExportedTag(tag->base); - } - public: Flow visitCall(Call* curr) { Name target = curr->target; @@ -4608,7 +4638,7 @@ class ModuleRunnerBase : public ExpressionRunner { auto exnData = e.exn.getExnData(); for (size_t i = 0; i < curr->catchTags.size(); i++) { - auto* tag = self()->getCanonicalTag(curr->catchTags[i]); + auto* tag = allTags[curr->catchTags[i]]; if (tag == exnData->tag) { multiValues.push_back(exnData->payload); return processCatchBody(curr->catchBodies[i]); @@ -4631,8 +4661,13 @@ class ModuleRunnerBase : public ExpressionRunner { auto exnData = e.exn.getExnData(); for (size_t i = 0; i < curr->catchTags.size(); i++) { auto catchTag = curr->catchTags[i]; - if (!catchTag.is() || - self()->getCanonicalTag(catchTag) == exnData->tag) { + + // note: allTags[catchTag] will be null if it's a tag that we don't know + // about, i.e. an unimported tag. + // Pointer comparison. + if (auto tag = allTags.find(catchTag); + !catchTag.is() || + ((tag != allTags.end()) && tag->second == exnData->tag)) { Flow ret; ret.breakTo = curr->catchDests[i]; if (catchTag.is()) { @@ -4653,8 +4688,8 @@ class ModuleRunnerBase : public ExpressionRunner { Flow visitThrow(Throw* curr) { Literals arguments; VISIT_ARGUMENTS(flow, curr->operands, arguments); - throwException(WasmException{ - self()->makeExnData(self()->getCanonicalTag(curr->tag), arguments)}); + throwException( + WasmException{self()->makeExnData(allTags[curr->tag], arguments)}); WASM_UNREACHABLE("throw"); } Flow visitRethrow(Rethrow* curr) { @@ -4749,7 +4784,7 @@ class ModuleRunnerBase : public ExpressionRunner { // old one may exist, in which case we still emit a continuation, but it is // meaningless (it will error when it reaches the host). auto old = self()->getCurrContinuationOrNull(); - auto* tag = self()->getCanonicalTag(curr->tag); + auto* tag = allTags[curr->tag]; if (!old) { return Flow(SUSPEND_FLOW, tag, std::move(arguments)); } @@ -4804,8 +4839,7 @@ class ModuleRunnerBase : public ExpressionRunner { if (auto* resumeThrow = curr->template dynCast()) { if (resumeThrow->tag) { // resume_throw - contData->exceptionTag = - self()->getModule()->getTag(resumeThrow->tag); + contData->exceptionTag = allTags[resumeThrow->tag]; } else { // resume_throw_ref contData->exception = arguments[0]; @@ -4835,7 +4869,7 @@ class ModuleRunnerBase : public ExpressionRunner { } else { // We are suspending. See if a suspension arrived that we support. for (size_t i = 0; i < curr->handlerTags.size(); i++) { - auto* handlerTag = self()->getCanonicalTag(curr->handlerTags[i]); + auto* handlerTag = allTags[curr->handlerTags[i]]; if (handlerTag == ret.suspendTag) { // Switch the flow from suspending to branching. ret.suspendTag = nullptr; @@ -5202,12 +5236,15 @@ class ModuleRunner : public ModuleRunnerBase { ModuleRunner( Module& wasm, ExternalInterface* externalInterface, - std::map> linkedInstances = {}) + std::map> linkedInstances = {}, + std::shared_ptr importResolver = nullptr) : ModuleRunnerBase( wasm, externalInterface, - std::make_shared>( - linkedInstances), + importResolver + ? importResolver + : std::make_shared>( + linkedInstances), linkedInstances) {} Literal makeFuncData(Name name, Type type) { diff --git a/src/wasm.h b/src/wasm.h index 2904bcefd5f..4f6c530aa31 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2492,10 +2492,11 @@ class Global : public Importable { class Tag : public Importable { public: + // TODO: This should ideally be const. HeapType type; - Type params() { return type.getSignature().params; } - Type results() { return type.getSignature().results; } + Type params() const { return type.getSignature().params; } + Type results() const { return type.getSignature().results; } }; // "Opaque" data, not part of the core wasm spec, that is held in binaries. diff --git a/test/spec/instance.wast b/test/spec/instance.wast new file mode 100644 index 00000000000..e5a7091b1b8 --- /dev/null +++ b/test/spec/instance.wast @@ -0,0 +1,148 @@ +;; Copy of test/spec/testsuite/instance.wast +;; TODO: Use the upstream test after adding support for default values for tables and module splitting testing with module instances. + +;; Instantiation is generative + +(module definition $M + (global (export "glob") (mut i32) (i32.const 0)) + (memory (export "mem") 1) + (tag (export "tag")) +) + +(module instance $I1 $M) +(module instance $I2 $M) +(register "I1" $I1) +(register "I2" $I2) + +(module + (import "I1" "glob" (global $glob1 (mut i32))) + (import "I2" "glob" (global $glob2 (mut i32))) + (import "I1" "mem" (memory $mem1 1)) + (import "I2" "mem" (memory $mem2 1)) + (import "I1" "tag" (tag $tag1)) + (import "I2" "tag" (tag $tag2)) + + (func $f) + (elem declare func $f) + + (func (export "glob") (result i32) + (global.set $glob1 (i32.const 1)) + (global.get $glob2) + ) + (func (export "mem") (result i32) + (i32.store $mem1 (i32.const 0) (i32.const 1)) + (i32.load $mem2 (i32.const 0)) + ) + (func (export "tag") (result i32) + (block $on_tag1 + (block $on_other + (try_table (catch $tag1 $on_tag1) (catch_all $on_other) + (throw $tag2) + ) + (unreachable) + ) + (return (i32.const 0)) + ) + (return (i32.const 1)) + ) +) + +(assert_return (invoke "glob") (i32.const 0)) +(assert_return (invoke "mem") (i32.const 0)) +(assert_return (invoke "tag") (i32.const 0)) + + +;; Import is not generative + +(module + (import "I1" "glob" (global $glob1 (mut i32))) + (import "I1" "glob" (global $glob2 (mut i32))) + (import "I1" "mem" (memory $mem1 1)) + (import "I1" "mem" (memory $mem2 1)) + (import "I1" "tag" (tag $tag1)) + (import "I1" "tag" (tag $tag2)) + + (func $f) + (elem declare func $f) + + (func (export "glob") (result i32) + (global.set $glob1 (i32.const 1)) + (global.get $glob2) + ) + (func (export "mem") (result i32) + (i32.store $mem1 (i32.const 0) (i32.const 1)) + (i32.load $mem2 (i32.const 0)) + ) + (func (export "tag") (result i32) + (block $on_tag1 + (block $on_other + (try_table (catch $tag1 $on_tag1) (catch_all $on_other) + (throw $tag2) + ) + (unreachable) + ) + (return (i32.const 0)) + ) + (return (i32.const 1)) + ) +) + +(assert_return (invoke "glob") (i32.const 1)) +(assert_return (invoke "mem") (i32.const 1)) +(assert_return (invoke "tag") (i32.const 1)) + + +;; Export is not generative + +(module definition $N + (global $glob (mut i32) (i32.const 0)) + (memory $mem 1) + (tag $tag) + + (export "glob1" (global $glob)) + (export "glob2" (global $glob)) + (export "mem1" (memory $mem)) + (export "mem2" (memory $mem)) + (export "tag1" (tag $tag)) + (export "tag2" (tag $tag)) +) + +(module instance $I $N) +(register "I" $I) + +(module + (import "I" "glob1" (global $glob1 (mut i32))) + (import "I" "glob2" (global $glob2 (mut i32))) + (import "I" "mem1" (memory $mem1 1)) + (import "I" "mem2" (memory $mem2 1)) + (import "I" "tag1" (tag $tag1)) + (import "I" "tag2" (tag $tag2)) + + (func $f) + (elem declare func $f) + + (func (export "glob") (result i32) + (global.set $glob1 (i32.const 1)) + (global.get $glob2) + ) + (func (export "mem") (result i32) + (i32.store $mem1 (i32.const 0) (i32.const 1)) + (i32.load $mem2 (i32.const 0)) + ) + (func (export "tag") (result i32) + (block $on_tag1 + (block $on_other + (try_table (catch $tag1 $on_tag1) (catch_all $on_other) + (throw $tag2) + ) + (unreachable) + ) + (return (i32.const 0)) + ) + (return (i32.const 1)) + ) +) + +(assert_return (invoke "glob") (i32.const 1)) +(assert_return (invoke "mem") (i32.const 1)) +(assert_return (invoke "tag") (i32.const 1))