From 6b63ad60f7ad3b810a8d9edde7c4ce6425e80ea1 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 17 Mar 2026 22:21:08 +0000 Subject: [PATCH] Fix for imported globals --- scripts/test/shared.py | 2 +- src/ir/CMakeLists.txt | 1 + src/ir/import-utils.h | 14 +++++++----- src/ir/runtime-global.cpp | 30 +++++++++++++++++++++++++ src/ir/runtime-global.h | 41 +++++++++++++++++++++++++++++++++++ src/passes/Print.cpp | 7 ++++++ src/tools/execution-results.h | 19 ++++++++++------ src/tools/wasm-ctor-eval.cpp | 14 ++++++------ src/wasm-interpreter.h | 31 +++++++++++++++++++------- src/wasm.h | 1 + test/spec/imports.wast | 7 ++---- test/spec/old_import.wast | 27 ++++++++++++++--------- 12 files changed, 150 insertions(+), 44 deletions(-) create mode 100644 src/ir/runtime-global.cpp create mode 100644 src/ir/runtime-global.h diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 57c2d107eef..26a0e1a0909 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -423,7 +423,7 @@ def get_tests(test_dir, extensions=[], recursive=False): 'if.wast', # Requires more precise unreachable validation 'imports.wast', # Requires fixing handling of mutation to imported globals 'proposals/threads/imports.wast', # Missing memory type validation on instantiation - 'linking.wast', # Missing global type validation on instantiation + 'linking.wast', # Incorrectly allows covariant subtyping for table imports 'proposals/threads/memory.wast', # Missing memory type validation on instantiation 'annotations.wast', # String annotations IDs should be allowed 'instance.wast', # Requires support for table default elements diff --git a/src/ir/CMakeLists.txt b/src/ir/CMakeLists.txt index a67ee5a91d3..919069770c5 100644 --- a/src/ir/CMakeLists.txt +++ b/src/ir/CMakeLists.txt @@ -20,6 +20,7 @@ set(ir_SOURCES public-type-validator.cpp ReFinalize.cpp return-utils.cpp + runtime-global.cpp runtime-table.cpp stack-utils.cpp table-utils.cpp diff --git a/src/ir/import-utils.h b/src/ir/import-utils.h index 3697b9f0269..36a993f1939 100644 --- a/src/ir/import-utils.h +++ b/src/ir/import-utils.h @@ -18,6 +18,7 @@ #define wasm_ir_import_h #include "ir/import-names.h" +#include "ir/runtime-global.h" #include "ir/runtime-table.h" #include "literal.h" #include "wasm-type.h" @@ -130,11 +131,12 @@ class ImportResolver { public: virtual ~ImportResolver() = default; - // Returns null if the imported global does not exist. The returned Literals* - // lives as long as the ImportResolver instance. Takes name, type, and mut as - // parameters because these are parts of the global's externtype that the - // environment is allowed to reflect on when providing imports. - virtual Literals* + // Returns null if the imported global does not exist. The returned + // RuntimeGlobal* lives as long as the ImportResolver instance. Takes name, + // type, and mut as parameters because these are parts of the global's + // externtype that the environment is allowed to reflect on when providing + // imports. + virtual RuntimeGlobal* getGlobalOrNull(ImportNames name, Type type, bool mut) const = 0; // Returns null if the imported table does not exist. The returned @@ -153,7 +155,7 @@ class LinkedInstancesImportResolver : public ImportResolver { std::map> linkedInstances) : linkedInstances(std::move(linkedInstances)) {} - Literals* + RuntimeGlobal* getGlobalOrNull(ImportNames name, Type type, bool mut) const override { auto it = linkedInstances.find(name.module); if (it == linkedInstances.end()) { diff --git a/src/ir/runtime-global.cpp b/src/ir/runtime-global.cpp new file mode 100644 index 00000000000..cc9533a9b48 --- /dev/null +++ b/src/ir/runtime-global.cpp @@ -0,0 +1,30 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ir/runtime-global.h" + +namespace wasm { + +bool RuntimeGlobal::isSubType(const Global& global) const { + if (global.mutable_ != definition.mutable_) { + return false; + } + + return global.mutable_ ? global.type == definition.type + : Type::isSubType(definition.type, global.type); +} + +} // namespace wasm diff --git a/src/ir/runtime-global.h b/src/ir/runtime-global.h new file mode 100644 index 00000000000..5d88426cac2 --- /dev/null +++ b/src/ir/runtime-global.h @@ -0,0 +1,41 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef wasm_ir_runtime_global_h +#define wasm_ir_runtime_global_h + +#include "literal.h" +#include "wasm.h" + +namespace wasm { + +class RuntimeGlobal { +public: + RuntimeGlobal(Global definition, Literals literals = {}) + : literals(literals), definition(definition) {} + + Literals literals; + + const Global* getDefinition() const { return &definition; } + bool isSubType(const Global& global) const; + +private: + const Global definition; +}; + +} // namespace wasm + +#endif // wasm_ir_runtime_global_h diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index f887075fc2d..478e19db6bb 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -4037,4 +4037,11 @@ std::ostream& operator<<(std::ostream& o, const Table& table) { return o; } +std::ostream& operator<<(std::ostream& o, const Global& global) { + wasm::PrintSExpression printer(o); + // TODO: visitGlobal should take a const Global* + printer.visitGlobal(const_cast(&global)); + return o; +} + } // namespace wasm diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 1161d47cf78..ddf82bd03b3 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -373,7 +373,7 @@ class FuzzerImportResolver // We can synthesize imported externref globals. Use a deque for stable // addresses. - mutable std::deque synthesizedGlobals; + mutable std::deque synthesizedGlobals; Tag* getTagOrNull(ImportNames name, const Signature& type) const override { if (name.module == "fuzzing-support") { @@ -388,7 +388,7 @@ class FuzzerImportResolver return LinkedInstancesImportResolver::getTagOrNull(name, type); } - virtual Literals* + virtual RuntimeGlobal* getGlobalOrNull(ImportNames name, Type type, bool mut) const override { // First look for globals available from linked instances. if (auto* global = @@ -404,7 +404,11 @@ class FuzzerImportResolver return nullptr; } // TODO: Generate a distinct payload for each global. - synthesizedGlobals.emplace_back(Literals{Literal::makeExtern(0, Unshared)}); + Global global; + global.type = type; + global.mutable_ = mut; + synthesizedGlobals.emplace_back( + RuntimeGlobal{global, Literals{Literal::makeExtern(0, Unshared)}}); return &synthesizedGlobals.back(); } @@ -506,11 +510,12 @@ struct ExecutionResults { // Log the global's value. (We use "calling" here to match the output // for calls, which simplifies the fuzzer.) std::cout << "[fuzz-exec] calling " << exp->name << "\n"; - Literals* value = instance.getExportedGlobalOrNull(exp->name); - assert(value); - assert(value->size() == 1); + RuntimeGlobal* runtimeGlobal = + instance.getExportedGlobalOrNull(exp->name); + assert(runtimeGlobal); + assert(runtimeGlobal->literals.size() == 1); std::cout << "[LoggingExternalInterface logging "; - printValue((*value)[0]); + printValue(runtimeGlobal->literals[0]); std::cout << "]\n"; } // Ignore other exports for now. TODO diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 9b2800a2d84..6f19d6c5e89 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -67,14 +67,14 @@ bool isNullableAndMutable(Expression* ref, Index fieldIndex) { class EvallingImportResolver : public ImportResolver { public: - EvallingImportResolver() : stubLiteral({Literal(0)}) {}; + EvallingImportResolver() : stubGlobal({Global(), {Literal(0)}}) {} // Return an unused stub value. We throw FailToEvalException on reading any // imported globals. We ignore the type and return an i32 literal since some // types can't be created anyway (e.g. ref none). - Literals* + RuntimeGlobal* getGlobalOrNull(ImportNames name, Type type, bool mut) const override { - return &stubLiteral; + return &stubGlobal; } RuntimeTable* getTableOrNull(ImportNames name, @@ -96,7 +96,7 @@ class EvallingImportResolver : public ImportResolver { } private: - mutable Literals stubLiteral; + mutable RuntimeGlobal stubGlobal; mutable std::unordered_map importedTags; }; @@ -564,8 +564,8 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { void applyGlobalsToModule() { if (!wasm->features.hasGC()) { // Without GC, we can simply serialize the globals in place as they are. - for (const auto& [name, values] : instance->allGlobals) { - wasm->getGlobal(name)->init = getSerialization(*values); + for (const auto& [name, global] : instance->allGlobals) { + wasm->getGlobal(name)->init = getSerialization(global->literals); } return; } @@ -602,7 +602,7 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { // value when we created it.) auto iter = instance->allGlobals.find(oldGlobal->name); if (iter != instance->allGlobals.end()) { - oldGlobal->init = getSerialization(*iter->second, name); + oldGlobal->init = getSerialization(iter->second->literals, name); } // Add the global back to the module. diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 2b397dd1672..0b496b8005b 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -41,6 +41,7 @@ #include "ir/memory-utils.h" #include "ir/module-utils.h" #include "ir/properties.h" +#include "ir/runtime-global.h" #include "ir/runtime-table.h" #include "ir/table-utils.h" #include "support/bits.h" @@ -3260,7 +3261,7 @@ class ModuleRunnerBase : public ExpressionRunner { // Keyed by internal name. All globals in the module, including imports. // `definedGlobals` contains non-imported globals. Points to `definedGlobals` // of this instance and other instances. - std::unordered_map allGlobals; + std::unordered_map allGlobals; // Like `allGlobals`. Keyed by internal name. All tables including imports. std::unordered_map allTables; @@ -3354,7 +3355,7 @@ class ModuleRunnerBase : public ExpressionRunner { func->type); } - Literals* getExportedGlobalOrNull(Name name) { + RuntimeGlobal* getExportedGlobalOrNull(Name name) { Export* export_ = wasm.getExportOrNull(name); if (!export_ || export_->kind != ExternalKind::Global) { return nullptr; @@ -3388,7 +3389,7 @@ class ModuleRunnerBase : public ExpressionRunner { << " not found.") .str()); } - return *global; + return global->literals; } Tag* getExportedTagOrNull(Name name) { @@ -3428,7 +3429,7 @@ class ModuleRunnerBase : public ExpressionRunner { // Globals that were defined in this module and not from an import. // `allGlobals` contains these values + imported globals, keyed by their // internal name. - std::vector definedGlobals; + std::vector definedGlobals; std::vector> definedTables; std::vector definedTags; @@ -3538,9 +3539,22 @@ class ModuleRunnerBase : public ExpressionRunner { << (*function)->type.getHeapType().getSignature().toString()) .str()); } + } else if (auto** globalDecl = std::get_if(&import)) { + auto* exportedGlobal = + importResolver->getGlobalOrNull(importable->importNames(), + (*globalDecl)->type, + (*globalDecl)->mutable_); + if (!exportedGlobal->isSubType(**globalDecl)) { + trap( + (std::stringstream() + << "Imported global " << importable->importNames() + << " with definition " << *exportedGlobal->getDefinition() + << " isn't compatible with import declaration: " << **globalDecl) + .str()); + } } - // TODO: remaining cases e.g. globals and tags. + // TODO: remaining cases e.g. tags. }); } @@ -3567,7 +3581,8 @@ class ModuleRunnerBase : public ExpressionRunner { assert(inserted && "Unexpected repeated global name"); } else { Literals init = self()->visit(global->init).values; - auto& definedGlobal = definedGlobals.emplace_back(std::move(init)); + auto& definedGlobal = + definedGlobals.emplace_back(RuntimeGlobal{*global, std::move(init)}); [[maybe_unused]] auto [_, inserted] = allGlobals.try_emplace(global->name, &definedGlobal); @@ -4119,13 +4134,13 @@ class ModuleRunnerBase : public ExpressionRunner { Flow visitGlobalGet(GlobalGet* curr) { auto name = curr->name; - return *allGlobals.at(name); + return allGlobals.at(name)->literals; } Flow visitGlobalSet(GlobalSet* curr) { auto name = curr->name; VISIT(flow, curr->value) - *allGlobals.at(name) = flow.values; + allGlobals.at(name)->literals = flow.values; return Flow(); } diff --git a/src/wasm.h b/src/wasm.h index 716cf27a2cb..3632edca7ad 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2774,6 +2774,7 @@ std::ostream& operator<<(std::ostream& o, wasm::ModuleHeapType pair); std::ostream& operator<<(std::ostream& os, wasm::MemoryOrder mo); std::ostream& operator<<(std::ostream& o, const wasm::ImportNames& importNames); std::ostream& operator<<(std::ostream& o, const Table& table); +std::ostream& operator<<(std::ostream& o, const wasm::Global& global); } // namespace wasm diff --git a/test/spec/imports.wast b/test/spec/imports.wast index 1e4b661665f..1cbd3ebf768 100644 --- a/test/spec/imports.wast +++ b/test/spec/imports.wast @@ -10,14 +10,11 @@ (func (export "func-i64->i64") (param i64) (result i64) (local.get 0)) (global (export "global-i32") i32 (i32.const 55)) (global (export "global-f32") f32 (f32.const 44)) - ;;; FIXME: Exporting a mutable global is currently not supported. Make mutable - ;;; when support is added. - (global (export "global-mut-i64") i64 (i64.const 66)) + (global (export "global-mut-i64") (mut i64) (i64.const 66)) (table (export "table-10-inf") 10 funcref) (table (export "table-10-20") 10 20 funcref) (memory (export "memory-2-inf") 2) - ;; Multiple memories are not yet supported - ;; (memory (export "memory-2-4") 2 4) + (memory (export "memory-2-4") 2 4) ) (register "test") diff --git a/test/spec/old_import.wast b/test/spec/old_import.wast index 7ab52950c3d..77ff3506545 100644 --- a/test/spec/old_import.wast +++ b/test/spec/old_import.wast @@ -77,18 +77,25 @@ (assert_return (invoke "get-x") (i32.const 666)) (assert_return (invoke "get-y") (i32.const 666)) -;; (assert_unlinkable -;; (module (import "spectest" "unknown" (global i32))) -;; "unknown import" -;; ) -;; (assert_unlinkable -;; (module (import "spectest" "print" (global i32))) -;; "type mismatch" -;; ) +(assert_unlinkable + (module (import "spectest" "unknown" (global i32))) + "unknown import" +) +(assert_unlinkable + (module (import "spectest" "print" (global i32))) + "type mismatch" +) (module (import "spectest" "global_i64" (global i64))) -(module (import "spectest" "global_i64" (global f32))) -(module (import "spectest" "global_i64" (global f64))) + +(assert_unlinkable + (module (import "spectest" "global_i64" (global f32))) + "type mismatch" +) +(assert_unlinkable + (module (import "spectest" "global_i64" (global f64))) + "type mismatch" +) ;; Tables