Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion scripts/test/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,6 @@ def get_tests(test_dir, extensions=[], recursive=False):
'instance.wast', # Requires wast `module definition` support
'table64.wast', # Requires wast `module definition` support
'table_grow.wast', # Incorrect table linking semantics in interpreter
'try_catch.wast', # Incorrect imported tag semantics in interpreter
'tag.wast', # Non-empty tag results allowed by stack switching
'try_table.wast', # Requires try_table interpretation
'local_init.wast', # Requires local validation to respect unnamed blocks
Expand Down
12 changes: 0 additions & 12 deletions src/literal.h
Original file line number Diff line number Diff line change
Expand Up @@ -785,18 +785,6 @@ struct GCData {
: type(type), values(std::move(values)), desc(desc) {}
};

// The data of a (ref exn) literal.
struct ExnData {
// The tag of this exn data.
// TODO: handle cross-module calls using something other than a Name here.
Name tag;

// The payload of this exn data.
Literals payload;

ExnData(Name tag, Literals payload) : tag(tag), payload(payload) {}
};

} // namespace wasm

namespace std {
Expand Down
35 changes: 25 additions & 10 deletions src/tools/execution-results.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,11 @@ struct LoggingExternalInterface : public ShellExternalInterface {
Name exportedTable;
Module& wasm;

// The name of the imported fuzzing tag for wasm.
Name wasmTag;
// The imported fuzzing tag for wasm.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to handle an arbitrary number of imported tags?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for the special imported tags from JS:

var wasmTag = imports['fuzzing-support']['wasmtag'] = new WebAssembly.Tag({
'parameters': ['i32']
});
// The JSTag that represents a JS tag.
imports['fuzzing-support']['jstag'] = WebAssembly.JSTag;

Tag wasmTag;

// The name of the imported tag for js exceptions. If it is not imported, we
// use a default name here (which should differentiate it from any wasm
// exceptions).
Name jsTag = "__private";
// The imported tag for js exceptions.
Tag jsTag;

// The ModuleRunner and this ExternalInterface end up needing links both ways,
// so we cannot init this in the constructor.
Expand All @@ -67,17 +65,34 @@ struct LoggingExternalInterface : public ShellExternalInterface {
}
}

// Default names for tags.
wasmTag.module = "fuzzing-support";
wasmTag.base = wasmTag.name = "wasmtag";

jsTag.module = "fuzzing-support";
jsTag.base = "jstag";
jsTag.name = "__private";

for (auto& tag : wasm.tags) {
if (tag->module == "fuzzing-support") {
if (tag->base == "wasmtag") {
wasmTag = tag->name;
wasmTag.name = tag->name;
} else if (tag->base == "jstag") {
jsTag = tag->name;
jsTag.name = tag->name;
}
}
}
}

Tag* getImportedTag(Name name) override {
if (name == jsTag.name) {
return &jsTag;
} else if (name == wasmTag.name) {
return &wasmTag;
}
Fatal() << "missing host tag " << name;
}

Literal getImportedFunction(Function* import) override {
if (linkedInstances.count(import->module)) {
return getImportInstance(import)->getExportedFunction(import->base);
Expand Down Expand Up @@ -122,7 +137,7 @@ struct LoggingExternalInterface : public ShellExternalInterface {
if (arguments[0].geti32() == 0) {
throwJSException();
} else {
auto payload = std::make_shared<ExnData>(wasmTag, arguments);
auto payload = std::make_shared<ExnData>(&wasmTag, arguments);
throwException(WasmException{Literal(payload)});
}
} else if (import->base == "table-get") {
Expand Down Expand Up @@ -213,7 +228,7 @@ struct LoggingExternalInterface : public ShellExternalInterface {
auto empty = HeapType(Struct{});
auto inner = Literal(std::make_shared<GCData>(empty, Literals{}), empty);
Literals arguments = {inner.externalize()};
auto payload = std::make_shared<ExnData>(jsTag, arguments);
auto payload = std::make_shared<ExnData>(&jsTag, arguments);
throwException(WasmException{Literal(payload)});
}

Expand Down
64 changes: 53 additions & 11 deletions src/wasm-interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,19 @@ struct FuncData {
}
};

// The data of a (ref exn) literal.
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably also needs an instance self pointer to differentiate tags defined by different instances of the same module.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I suppose it does. I'll add a TODO (atm we don't fuzz or test with that afaik)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testsuite/try_table.wast depends on this (so we skip it atm).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good. Then we can enable that later. (I just prefer to keep this PR small.)


// The payload of this exn data.
Literals payload;

ExnData(Tag* tag, Literals payload) : tag(tag), payload(payload) {}
};

// Suspend/resume support.
//
// As we operate directly on our structured IR, we do not have a program counter
Expand Down Expand Up @@ -324,7 +337,7 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
}

// Same as makeGCData but for ExnData.
Literal makeExnData(Name tag, const Literals& payload) {
Literal makeExnData(Tag* tag, const Literals& payload) {
auto allocation = std::make_shared<ExnData>(tag, payload);
#if __has_feature(leak_sanitizer) || __has_feature(address_sanitizer)
__lsan_ignore_object(allocation.get());
Expand Down Expand Up @@ -1890,9 +1903,13 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
Flow visitTry(Try* curr) { WASM_UNREACHABLE("unimp"); }
Flow visitTryTable(TryTable* curr) { WASM_UNREACHABLE("unimp"); }
Flow visitThrow(Throw* curr) {
// Single-module implementation. This is used from Precompute, for example.
// It is overriden in ModuleRunner to add logic for finding the proper
// imported tag (which single-module cases don't care about).
Literals arguments;
VISIT_ARGUMENTS(flow, curr->operands, arguments);
throwException(WasmException{makeExnData(curr->tag, arguments)});
throwException(WasmException{
self()->makeExnData(self()->getModule()->getTag(curr->tag), arguments)});
WASM_UNREACHABLE("throw");
}
Flow visitRethrow(Rethrow* curr) { WASM_UNREACHABLE("unimp"); }
Expand Down Expand Up @@ -2908,6 +2925,9 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
virtual void trap(const char* why) = 0;
virtual void hostLimit(const char* 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.
virtual Tag* getImportedTag(Name name) { WASM_UNREACHABLE("missing imported tag"); }

// the default impls for load and store switch on the sizes. you can either
// customize load/store, or the sub-functions which they call
Expand Down Expand Up @@ -3194,6 +3214,18 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
return iter->second;
}

Tag* getExportedTag(Name name) {
Export* export_ = wasm.getExportOrNull(name);
if (!export_ || export_->kind != ExternalKind::Tag) {
externalInterface->trap("exported tag not found");
}
auto* tag = wasm.getTag(*export_->getInternalName());
if (tag->imported()) {
tag = externalInterface->getImportedTag(name);
}
return tag;
}

std::string printFunctionStack() {
std::string ret = "/== (binaryen interpreter stack trace)\n";
for (int i = int(functionStack.size()) - 1; i >= 0; i--) {
Expand Down Expand Up @@ -3446,9 +3478,12 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
auto* inst = self();
auto* tag = inst->wasm.getTag(name);
while (tag->imported()) {
inst = inst->linkedInstances.at(tag->module).get();
auto* tagExport = inst->wasm.getExport(tag->base);
tag = inst->wasm.getTag(*tagExport->getInternalName());
auto iter = inst->linkedInstances.find(tag->module);
if (iter == inst->linkedInstances.end()) {
return externalInterface->getImportedTag(name);
}
inst = iter->second.get();
tag = inst->getExportedTag(tag->base);
}
return tag;
}
Expand Down Expand Up @@ -4354,7 +4389,8 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {

auto exnData = e.exn.getExnData();
for (size_t i = 0; i < curr->catchTags.size(); i++) {
if (curr->catchTags[i] == exnData->tag) {
auto* tag = self()->getCanonicalTag(curr->catchTags[i]);
if (tag == exnData->tag) {
multiValues.push_back(exnData->payload);
return processCatchBody(curr->catchBodies[i]);
}
Expand All @@ -4377,7 +4413,8 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
auto exnData = e.exn.getExnData();
for (size_t i = 0; i < curr->catchTags.size(); i++) {
auto catchTag = curr->catchTags[i];
if (!catchTag.is() || catchTag == exnData->tag) {
if (!catchTag.is() ||
self()->getCanonicalTag(catchTag) == exnData->tag) {
Flow ret;
ret.breakTo = curr->catchDests[i];
if (catchTag.is()) {
Expand All @@ -4395,6 +4432,13 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
throw;
}
}
Flow visitThrow(Throw* curr) {
Literals arguments;
VISIT_ARGUMENTS(flow, curr->operands, arguments);
throwException(WasmException{
self()->makeExnData(self()->getCanonicalTag(curr->tag), arguments)});
WASM_UNREACHABLE("throw");
}
Flow visitRethrow(Rethrow* curr) {
for (int i = exceptionStack.size() - 1; i >= 0; i--) {
if (exceptionStack[i].second == curr->target) {
Expand Down Expand Up @@ -4463,9 +4507,8 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
assert(self()->restoredValuesMap.empty());
// Throw, if we were resumed by resume_throw;
if (auto* tag = currContinuation->exceptionTag) {
// XXX tag->name lacks cross-module support
throwException(WasmException{
self()->makeExnData(tag->name, currContinuation->resumeArguments)});
self()->makeExnData(tag, currContinuation->resumeArguments)});
}
return currContinuation->resumeArguments;
}
Expand Down Expand Up @@ -4668,9 +4711,8 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
// set), so resuming is done. (And throw, if resume_throw.)
self()->continuationStore->resuming = false;
if (auto* tag = currContinuation->exceptionTag) {
// XXX tag->name lacks cross-module support
throwException(WasmException{
self()->makeExnData(tag->name, currContinuation->resumeArguments)});
self()->makeExnData(tag, currContinuation->resumeArguments)});
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/wasm/wasm-interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace wasm {

std::ostream& operator<<(std::ostream& o, const WasmException& exn) {
auto exnData = exn.exn.getExnData();
return o << exnData->tag << " " << exnData->payload;
return o << exnData->tag->name << " " << exnData->payload;
}

} // namespace wasm
26 changes: 26 additions & 0 deletions test/lit/exec/tag-cross-module.wast
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to get the newly-imported spec tests for this running as well. In general spec tests are better for testing multi-module execution because the several modules can go in a single file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, looks like that relevant spec test passes! I enabled it now.

Is it worth removing the new test from this PR? I think an exec test is still useful to have for this, as extra coverage. The fuzz-exec output is also more explicit than spec test output.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I'm fine keeping the lit test, too.

Copy link
Member

@tlively tlively Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the spec test doesn't tickle the case where we would need a self pointer to differentiate the tags, so it would be good to add such a spectest upstream.

(I can look at this since I need to add some new upstream tests anyway for the function import cast stuff.)

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
;; RUN: wasm-opt %s -all --fuzz-exec-before --fuzz-exec-second=%s.second -q -o /dev/null 2>&1 | filecheck %s

;; Define a tag in this module, and another tag in the secondary module, with
;; the same name but different (incompatible) contents. The second module will
;; call our export, and when we throw our tag, it should not catch it.

(module
(tag $tag (param structref))

(export "primary-tag" (tag $tag))

(func $func (export "func") (result i32)
(throw $tag
(ref.null struct)
)
)
)

;; CHECK: [fuzz-exec] calling func
;; CHECK-NEXT: [exception thrown: tag nullref]
;; CHECK-NEXT: [fuzz-exec] calling func2-internal
;; CHECK-NEXT: [exception thrown: tag nullref]
;; CHECK-NEXT: [fuzz-exec] calling func2-imported
;; CHECK-NEXT: func2-imported => null


32 changes: 32 additions & 0 deletions test/lit/exec/tag-cross-module.wast.second
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
(module
(import "primary" "func" (func $import (result i32)))

(import "primary" "primary-tag" (tag $ptag (param structref)))

(tag $tag (param (ref array)))

(func $func2-internal (export "func2-internal") (result (ref array))
;; Try to catch the internal tag. This fails to catch.
(block $block (result (ref array))
(try_table (catch $tag $block)
(drop
(call $import)
)
)
(unreachable)
)
)

(func $func2-imported (export "func2-imported") (result structref)
;; Try to catch the imported tag. This successfully catches.
(block $block (result structref)
(try_table (catch $ptag $block)
(drop
(call $import)
)
)
(unreachable)
)
)
)

Loading