-
Notifications
You must be signed in to change notification settings - Fork 814
[Exceptions] Fix cross-module Tag handling in interpreter #7955
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3bc8a54
403269c
1e98549
18acf83
6b45a18
167cc41
2b5a6d7
08827ee
7cd8db0
b876369
9bbdfe7
7047703
fbcfc9c
ac39b5f
d13eca9
dda230b
046fec3
0eb9913
360f53f
0b94658
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probably also needs an instance There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. testsuite/try_table.wast depends on this (so we skip it atm). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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()); | ||
|
@@ -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"); } | ||
|
@@ -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 | ||
|
@@ -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--) { | ||
|
@@ -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; | ||
} | ||
|
@@ -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]); | ||
} | ||
|
@@ -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()) { | ||
|
@@ -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) { | ||
|
@@ -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; | ||
} | ||
|
@@ -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)}); | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! I'm fine keeping the lit test, too. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 (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 | ||
|
||
|
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) | ||
) | ||
) | ||
) | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
binaryen/scripts/fuzz_shell.js
Lines 386 to 391 in 959d522