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
27 changes: 22 additions & 5 deletions scripts/fuzz_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,10 +658,20 @@ def filter_known_issues(output):
ret = run_unchecked(cmd)
return filter_known_issues(ret)
except subprocess.CalledProcessError:
# other known issues do make it fail, so re-run without checking for
# Other known issues do make it fail, so re-run without checking for
# success and see if we should ignore it
if filter_known_issues(run_unchecked(cmd)) == IGNORE:
raw = run_unchecked(cmd)
filtered = filter_known_issues(raw)
if filtered == IGNORE:
return IGNORE

# If we trap during instantiation, we do not need to ignore this run
# (we can see that all VMs have the same behavior), but we do need to
# not raise an error here.
if INSTANTIATE_ERROR in raw:
return INSTANTIATE_ERROR

# Otherwise, raise an error.
raise


Expand Down Expand Up @@ -1111,9 +1121,11 @@ def handle_pair(self, input, before_wasm, after_wasm, opts):
run([in_bin('wasm-opt'), before_wasm_temp, '-o', before_wasm_temp] + simplification_passes + FEATURE_OPTS)
# now that the before wasm is fixed up, generate a proper after wasm
run([in_bin('wasm-opt'), before_wasm_temp, '-o', after_wasm_temp] + opts + FEATURE_OPTS)
# always check for compiler crashes

# run before and after
before = self.run(before_wasm_temp)
after = self.run(after_wasm_temp)

if NANS:
# with NaNs we can't compare the output, as a reinterpret through
# memory might end up different in JS than wasm
Expand Down Expand Up @@ -1491,8 +1503,13 @@ def traps_in_instantiation(output):
if trap_index == -1:
# In "fixed" output, traps are replaced with *exception*.
trap_index = output.find('*exception*')
if trap_index == -1:
return False
# An exception can occur during the start function.
exception_index = output.find(EXCEPTION_PREFIX)
# Look at the first of a trap or an exception.
if exception_index >= 0 and (trap_index == -1 or exception_index < trap_index):
trap_index = exception_index
if trap_index == -1:
return False
export_index = output.find(FUZZ_EXEC_EXPORT_PREFIX)
if export_index == -1:
return True
Expand Down
3 changes: 1 addition & 2 deletions scripts/fuzz_shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,7 @@ function build(binary, isSecond) {
try {
instance = new WebAssembly.Instance(module, imports);
} catch (e) {
console.log('exception thrown: failed to instantiate module: ' + e);
quit();
throw new Error('exception thrown: failed to instantiate module: ' + e);
}

// Do not add the second instance's exports to the list, as that would be
Expand Down
5 changes: 5 additions & 0 deletions src/tools/fuzzing.h
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,11 @@ class TranslateToFuzzReader {
// Checks if a function is a callRef* import (call-ref or call-ref-catch).
bool isCallRefImport(Name func);

// Pick a start function.
Name pickStart();
// Fix the start function so it is valid for the fuzzer.
void fixStart();

// statistical distributions

// 0 to the limit, logarithmic scale
Expand Down
87 changes: 79 additions & 8 deletions src/tools/fuzzing/fuzzing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

#include "tools/fuzzing.h"
#include "ir/eh-utils.h"
#include "ir/gc-type-utils.h"
#include "ir/glbs.h"
#include "ir/iteration.h"
Expand Down Expand Up @@ -1672,6 +1673,23 @@ void TranslateToFuzzReader::processFunctions() {
}
}

// Decide what to do with the start function. Most of the time we remove it,
// as that is the least risky for fuzzing (any trap in the start will make
// the entire module not execute), but other cases are important too.
switch (upTo(10)) {
case 0:
// Do not modify the start, potentially leaving the existing one.
break;
case 1:
// Pick a new start.
wasm.start = pickStart();
break;
default:
// Remove it.
wasm.start = Name();
}
fixStart();

// At the very end, add hang limit checks (so no modding can override them).
if (fuzzParams->HANG_LIMIT > 0) {
for (auto& func : wasm.functions) {
Expand Down Expand Up @@ -2395,14 +2413,6 @@ void TranslateToFuzzReader::modifyInitialFunctions() {
func->body = make(func->getResults());
}
}

// Remove a start function - the fuzzing harness expects code to run only
// from exports. When preserving imports and exports, however, we need to
// keep any start method, as it may be important to keep the contract between
// the wasm and the outside.
if (!preserveImportsAndExports) {
wasm.start = Name();
}
}

void TranslateToFuzzReader::mutateJSBoundary() {
Expand Down Expand Up @@ -6720,4 +6730,65 @@ bool TranslateToFuzzReader::isCallRefImport(Name target) {
func->base.startsWith("call-ref");
}

Name TranslateToFuzzReader::pickStart() {
// Any none-none function is an option.
std::vector<Name> options;
for (auto& func : wasm.functions) {
// Avoid imports, see fixStart().
if (func->imported()) {
continue;
}
if (func->getParams() == Type::none && func->getResults() == Type::none) {
options.push_back(func->name);
}
}
return options.empty() ? Name() : pick(options);
}

void TranslateToFuzzReader::fixStart() {
if (!wasm.start) {
return;
}

// Fuzzing with a start function is tricky, as if it calls imports that could
// cause reentrancy issues (the module is not yet returned to the JS/VM side,
// yet, so things are not ready for calls to happen yet). Still, fuzzing the
// start is important, so just remove all calls, which still leaves a chance
// for interesting things like modifying globals and memory etc.
Comment on lines +6753 to +6757
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Given that we can still throw an exception or trap in the start function, how bad would it be for us to allow calling imports? The worst they can do is throw, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The reentrancy is even worse than throwing. It doesn't necessarily throw, in fact - it might call an import that calls exports with a try-catch, swallowing the exception on no exports existing. I took a look at some stuff here and it was not fun.

Though maybe this is important to fuzz for VM errors, even if it isn't for real-world code?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess if it silently messes up the fuzz_shell.js state, that would be bad. But I'm not sure how that would happen. Maybe we can add an initializationFinished flag in fuzz_shell.js and add logging with early exits to all the imports we provide (or maybe just those that would call back in to exports).

But if there are problems that would require more invasive fixes than that, then avoiding the issue by removing calls sounds ok.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ok, I found a testcase and reminded myself why this is so bad. This is confusing in VMs, but in wasm-opt's own interpreter, it just breaks entirely. We are still running initialization and I guess in the C++ constructor, when this reentrancy happens, leading to crashes. Might be worth looking into it but it could require a large refactoring of our interpreter.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh yeah doing fallible work in a constructor isn't great. Want to at least add a TODO about doing that refactoring and allowing fuzzing of start functions calling other functions, including imports?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, added in the last commit actually.

// TODO: While confusing in VMs, fuzzing this might be useful there, but we
// need to fix our own interpreter to not crash.

auto* start = wasm.getFunction(wasm.start);
// We definitely can't call an import here in the fuzzer, for the above
// reasons, and should have only chosen something valid.
assert(!start->imported());

struct Fixer : public PostWalker<Fixer> {
Module& wasm;
TranslateToFuzzReader& parent;

Fixer(Module& wasm, TranslateToFuzzReader& parent)
: wasm(wasm), parent(parent) {}

void visitCall(Call* curr) { replace(); }
void visitCallIndirect(CallIndirect* curr) { replace(); }
void visitCallRef(CallRef* curr) { replace(); }

void replace() {
std::vector<Expression*> list;
for (auto* child : ChildIterator(getCurrent())) {
list.push_back(parent.builder.makeDrop(child));
}
list.push_back(parent.makeTrivial(getCurrent()->type));
replaceCurrent(parent.builder.makeBlock(list));
}
} fixer(wasm, *this);

FunctionCreationContext context(*this, start);
fixer.walk(start->body);

// Added blocks may require fixups.
EHUtils::handleBlockNestedPops(start, wasm);
}

} // namespace wasm
Loading