Skip to content

Commit ddb7026

Browse files
Validate global imports (#8487)
* Add a RuntimeGlobal class to wrap the global's runtime value as well as its definition so we can check its type + mutability for import validation * Global imports are invariant for mutable globals and covariant for immutable globals: <img width="841" height="428" alt="image" src="https://github.com/user-attachments/assets/748cb719-6de9-4dfd-9719-e4051dc31c45" />. Mutability must match as well. * Fix old_import.wast to account for instantiations which should correctly fail. This test is now mostly the same as test/spec/imports.wast, but there are small differences so I plan to keep it until the upstream testsuite imports.wast passes. Example error message: ``` [trap Imported global Mref_ex.g-var-func with type: (mut (ref func)) isn't compatible with import declaration: (import "Mref_ex" "g-var-func" (global $gimport$0 (mut funcref))) ] ``` Part of #8261.
1 parent 3ce53e8 commit ddb7026

File tree

12 files changed

+158
-43
lines changed

12 files changed

+158
-43
lines changed

scripts/test/shared.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ def get_tests(test_dir, extensions=[], recursive=False):
423423
'if.wast', # Requires more precise unreachable validation
424424
'imports.wast', # Requires fixing handling of mutation to imported globals
425425
'proposals/threads/imports.wast', # Missing memory type validation on instantiation
426-
'linking.wast', # Missing global type validation on instantiation
426+
'linking.wast', # Incorrectly allows covariant subtyping for table imports
427427
'proposals/threads/memory.wast', # Missing memory type validation on instantiation
428428
'annotations.wast', # String annotations IDs should be allowed
429429
'instance.wast', # Requires support for table default elements

src/ir/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ set(ir_SOURCES
2020
public-type-validator.cpp
2121
ReFinalize.cpp
2222
return-utils.cpp
23+
runtime-global.cpp
2324
runtime-table.cpp
2425
stack-utils.cpp
2526
table-utils.cpp

src/ir/import-utils.h

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#define wasm_ir_import_h
1919

2020
#include "ir/import-names.h"
21+
#include "ir/runtime-global.h"
2122
#include "ir/runtime-table.h"
2223
#include "literal.h"
2324
#include "wasm-type.h"
@@ -130,11 +131,12 @@ class ImportResolver {
130131
public:
131132
virtual ~ImportResolver() = default;
132133

133-
// Returns null if the imported global does not exist. The returned Literals*
134-
// lives as long as the ImportResolver instance. Takes name, type, and mut as
135-
// parameters because these are parts of the global's externtype that the
136-
// environment is allowed to reflect on when providing imports.
137-
virtual Literals*
134+
// Returns null if the imported global does not exist. The returned
135+
// RuntimeGlobal* lives as long as the ImportResolver instance. Takes name,
136+
// type, and mut as parameters because these are parts of the global's
137+
// externtype that the environment is allowed to reflect on when providing
138+
// imports.
139+
virtual RuntimeGlobal*
138140
getGlobalOrNull(ImportNames name, Type type, bool mut) const = 0;
139141

140142
// Returns null if the imported table does not exist. The returned
@@ -153,7 +155,7 @@ class LinkedInstancesImportResolver : public ImportResolver {
153155
std::map<Name, std::shared_ptr<ModuleRunnerType>> linkedInstances)
154156
: linkedInstances(std::move(linkedInstances)) {}
155157

156-
Literals*
158+
RuntimeGlobal*
157159
getGlobalOrNull(ImportNames name, Type type, bool mut) const override {
158160
auto it = linkedInstances.find(name.module);
159161
if (it == linkedInstances.end()) {

src/ir/runtime-global.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2026 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "ir/runtime-global.h"
18+
19+
namespace wasm {
20+
21+
bool RuntimeGlobal::isSubType(const Global& global) const {
22+
if (global.mutable_ != (mutable_ == Mutable)) {
23+
return false;
24+
}
25+
26+
return global.mutable_ ? global.type == type
27+
: Type::isSubType(type, global.type);
28+
}
29+
30+
} // namespace wasm

src/ir/runtime-global.h

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2026 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef wasm_ir_runtime_global_h
18+
#define wasm_ir_runtime_global_h
19+
20+
#include "literal.h"
21+
#include "wasm-type.h"
22+
#include "wasm.h"
23+
24+
namespace wasm {
25+
26+
class RuntimeGlobal {
27+
public:
28+
RuntimeGlobal(Type type, Mutability mutable_, Literals literals = {})
29+
: literals(literals), type(type), mutable_(mutable_) {}
30+
31+
Literals literals;
32+
33+
Type getType() const { return type; }
34+
Mutability getMutable() const { return mutable_; }
35+
36+
bool isSubType(const Global& global) const;
37+
38+
private:
39+
const Type type;
40+
const Mutability mutable_;
41+
};
42+
43+
} // namespace wasm
44+
45+
#endif // wasm_ir_runtime_global_h

src/passes/Print.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4037,4 +4037,11 @@ std::ostream& operator<<(std::ostream& o, const Table& table) {
40374037
return o;
40384038
}
40394039

4040+
std::ostream& operator<<(std::ostream& o, const Global& global) {
4041+
wasm::PrintSExpression printer(o);
4042+
// TODO: visitGlobal should take a const Global*
4043+
printer.visitGlobal(const_cast<Global*>(&global));
4044+
return o;
4045+
}
4046+
40404047
} // namespace wasm

src/tools/execution-results.h

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ class FuzzerImportResolver
373373

374374
// We can synthesize imported externref globals. Use a deque for stable
375375
// addresses.
376-
mutable std::deque<Literals> synthesizedGlobals;
376+
mutable std::deque<RuntimeGlobal> synthesizedGlobals;
377377

378378
Tag* getTagOrNull(ImportNames name, const Signature& type) const override {
379379
if (name.module == "fuzzing-support") {
@@ -388,7 +388,7 @@ class FuzzerImportResolver
388388
return LinkedInstancesImportResolver::getTagOrNull(name, type);
389389
}
390390

391-
virtual Literals*
391+
virtual RuntimeGlobal*
392392
getGlobalOrNull(ImportNames name, Type type, bool mut) const override {
393393
// First look for globals available from linked instances.
394394
if (auto* global =
@@ -413,7 +413,10 @@ class FuzzerImportResolver
413413
payload = (payload + static_cast<Index>(c)) % 251;
414414
}
415415
}
416+
416417
synthesizedGlobals.emplace_back(
418+
type,
419+
mut ? Mutable : Immutable,
417420
Literals{Literal::makeExtern(payload, Unshared)});
418421
return &synthesizedGlobals.back();
419422
}
@@ -515,11 +518,11 @@ struct ExecutionResults {
515518
} else if (exp->kind == ExternalKind::Global) {
516519
// Log the global's value.
517520
std::cout << "[fuzz-exec] export " << exp->name << "\n";
518-
Literals* value = instance.getExportedGlobalOrNull(exp->name);
519-
assert(value);
520-
assert(value->size() == 1);
521+
RuntimeGlobal* global = instance.getExportedGlobalOrNull(exp->name);
522+
assert(global);
523+
assert(global->literals.size() == 1);
521524
std::cout << "[LoggingExternalInterface logging ";
522-
printValue((*value)[0]);
525+
printValue(global->literals[0]);
523526
std::cout << "]\n";
524527
}
525528
// Ignore other exports for now. TODO

src/tools/wasm-ctor-eval.cpp

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,14 @@ bool isNullableAndMutable(Expression* ref, Index fieldIndex) {
6868

6969
class EvallingImportResolver : public ImportResolver {
7070
public:
71-
EvallingImportResolver() : stubLiteral({Literal(0)}) {};
71+
EvallingImportResolver() : stubGlobal(Type::i32, Immutable, {Literal(0)}) {}
7272

7373
// Return an unused stub value. We throw FailToEvalException on reading any
7474
// imported globals. We ignore the type and return an i32 literal since some
7575
// types can't be created anyway (e.g. ref none).
76-
Literals*
76+
RuntimeGlobal*
7777
getGlobalOrNull(ImportNames name, Type type, bool mut) const override {
78-
return &stubLiteral;
78+
return &stubGlobal;
7979
}
8080

8181
RuntimeTable* getTableOrNull(ImportNames name,
@@ -97,7 +97,7 @@ class EvallingImportResolver : public ImportResolver {
9797
}
9898

9999
private:
100-
mutable Literals stubLiteral;
100+
mutable RuntimeGlobal stubGlobal;
101101
mutable std::unordered_map<ImportNames, Tag> importedTags;
102102
};
103103

@@ -565,8 +565,8 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface {
565565
void applyGlobalsToModule() {
566566
if (!wasm->features.hasGC()) {
567567
// Without GC, we can simply serialize the globals in place as they are.
568-
for (const auto& [name, values] : instance->allGlobals) {
569-
wasm->getGlobal(name)->init = getSerialization(*values);
568+
for (const auto& [name, global] : instance->allGlobals) {
569+
wasm->getGlobal(name)->init = getSerialization(global->literals);
570570
}
571571
return;
572572
}
@@ -603,7 +603,7 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface {
603603
// value when we created it.)
604604
auto iter = instance->allGlobals.find(oldGlobal->name);
605605
if (iter != instance->allGlobals.end()) {
606-
oldGlobal->init = getSerialization(*iter->second, name);
606+
oldGlobal->init = getSerialization(iter->second->literals, name);
607607
}
608608

609609
// Add the global back to the module.

src/wasm-interpreter.h

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#include "ir/memory-utils.h"
4242
#include "ir/module-utils.h"
4343
#include "ir/properties.h"
44+
#include "ir/runtime-global.h"
4445
#include "ir/runtime-table.h"
4546
#include "ir/table-utils.h"
4647
#include "support/bits.h"
@@ -3261,7 +3262,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
32613262
// Keyed by internal name. All globals in the module, including imports.
32623263
// `definedGlobals` contains non-imported globals. Points to `definedGlobals`
32633264
// of this instance and other instances.
3264-
std::unordered_map<Name, Literals*> allGlobals;
3265+
std::unordered_map<Name, RuntimeGlobal*> allGlobals;
32653266

32663267
// Like `allGlobals`. Keyed by internal name. All tables including imports.
32673268
std::unordered_map<Name, RuntimeTable*> allTables;
@@ -3355,7 +3356,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
33553356
func->type);
33563357
}
33573358

3358-
Literals* getExportedGlobalOrNull(Name name) {
3359+
RuntimeGlobal* getExportedGlobalOrNull(Name name) {
33593360
Export* export_ = wasm.getExportOrNull(name);
33603361
if (!export_ || export_->kind != ExternalKind::Global) {
33613362
return nullptr;
@@ -3389,7 +3390,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
33893390
<< " not found.")
33903391
.str());
33913392
}
3392-
return *global;
3393+
return global->literals;
33933394
}
33943395

33953396
Tag* getExportedTagOrNull(Name name) {
@@ -3429,7 +3430,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
34293430
// Globals that were defined in this module and not from an import.
34303431
// `allGlobals` contains these values + imported globals, keyed by their
34313432
// internal name.
3432-
std::vector<Literals> definedGlobals;
3433+
std::vector<RuntimeGlobal> definedGlobals;
34333434
std::vector<std::unique_ptr<RuntimeTable>> definedTables;
34343435
std::vector<Tag> definedTags;
34353436

@@ -3539,9 +3540,27 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
35393540
<< (*function)->type.getHeapType().getSignature().toString())
35403541
.str());
35413542
}
3543+
} else if (auto** globalDecl = std::get_if<Global*>(&import)) {
3544+
auto* exportedGlobal =
3545+
importResolver->getGlobalOrNull(importable->importNames(),
3546+
(*globalDecl)->type,
3547+
(*globalDecl)->mutable_);
3548+
if (!exportedGlobal->isSubType(**globalDecl)) {
3549+
trap(
3550+
(std::stringstream()
3551+
<< "Imported global " << importable->importNames()
3552+
<< " with type: "
3553+
<< (exportedGlobal->getMutable() == Mutability::Mutable ? "(mut "
3554+
: "")
3555+
<< exportedGlobal->getType()
3556+
<< (exportedGlobal->getMutable() == Mutability::Mutable ? ")"
3557+
: "")
3558+
<< " isn't compatible with import declaration: " << **globalDecl)
3559+
.str());
3560+
}
35423561
}
35433562

3544-
// TODO: remaining cases e.g. globals and tags.
3563+
// TODO: remaining cases e.g. tags.
35453564
});
35463565
}
35473566

@@ -3568,7 +3587,10 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
35683587
assert(inserted && "Unexpected repeated global name");
35693588
} else {
35703589
Literals init = self()->visit(global->init).values;
3571-
auto& definedGlobal = definedGlobals.emplace_back(std::move(init));
3590+
auto& definedGlobal =
3591+
definedGlobals.emplace_back(global->type,
3592+
global->mutable_ ? Mutable : Immutable,
3593+
std::move(init));
35723594

35733595
[[maybe_unused]] auto [_, inserted] =
35743596
allGlobals.try_emplace(global->name, &definedGlobal);
@@ -4120,13 +4142,13 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
41204142

41214143
Flow visitGlobalGet(GlobalGet* curr) {
41224144
auto name = curr->name;
4123-
return *allGlobals.at(name);
4145+
return allGlobals.at(name)->literals;
41244146
}
41254147
Flow visitGlobalSet(GlobalSet* curr) {
41264148
auto name = curr->name;
41274149
VISIT(flow, curr->value)
41284150

4129-
*allGlobals.at(name) = flow.values;
4151+
allGlobals.at(name)->literals = flow.values;
41304152
return Flow();
41314153
}
41324154

src/wasm.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2774,6 +2774,7 @@ std::ostream& operator<<(std::ostream& o, wasm::ModuleHeapType pair);
27742774
std::ostream& operator<<(std::ostream& os, wasm::MemoryOrder mo);
27752775
std::ostream& operator<<(std::ostream& o, const wasm::ImportNames& importNames);
27762776
std::ostream& operator<<(std::ostream& o, const Table& table);
2777+
std::ostream& operator<<(std::ostream& o, const wasm::Global& global);
27772778

27782779
} // namespace wasm
27792780

0 commit comments

Comments
 (0)