blob: ea822d5477a6828ed298b9dad23995bdbf1a7de2 [file] [log] [blame] [edit]
/*
* Copyright 2017 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.
*/
//
// Shared execution result checking code
//
#include "shell-interface.h"
#include "wasm.h"
namespace wasm {
using Loggings = std::vector<Literal>;
// Logs every relevant import call parameter.
struct LoggingExternalInterface : public ShellExternalInterface {
private:
Loggings& loggings;
struct State {
// Legalization for JS emits get/setTempRet0 calls ("temp ret 0" means a
// temporary return value of 32 bits; "0" is the only important value for
// 64-bit legalization, which needs one such 32-bit chunk in addition to
// the normal return value which can handle 32 bits).
uint32_t tempRet0 = 0;
} state;
// The name of the table exported by the name 'table.' Imports access it.
Name exportedTable;
Module& wasm;
// The ModuleRunner and this ExternalInterface end up needing links both ways,
// so we cannot init this in the constructor.
ModuleRunner* instance = nullptr;
public:
LoggingExternalInterface(Loggings& loggings, Module& wasm)
: loggings(loggings), wasm(wasm) {
for (auto& exp : wasm.exports) {
if (exp->kind == ExternalKind::Table && exp->name == "table") {
exportedTable = exp->value;
break;
}
}
}
Literals callImport(Function* import, const Literals& arguments) override {
if (import->module == "fuzzing-support") {
if (import->base.startsWith("log")) {
// This is a logging function like log-i32 or log-f64
std::cout << "[LoggingExternalInterface logging";
loggings.push_back(Literal()); // buffer with a None between calls
for (auto argument : arguments) {
if (argument.type == Type::i64) {
// To avoid JS legalization changing logging results, treat a
// logging of an i64 as two i32s (which is what legalization would
// turn us into).
auto low = Literal(int32_t(argument.getInteger()));
auto high = Literal(int32_t(argument.getInteger() >> int32_t(32)));
std::cout << ' ' << low;
loggings.push_back(low);
std::cout << ' ' << high;
loggings.push_back(high);
} else {
std::cout << ' ' << argument;
loggings.push_back(argument);
}
}
std::cout << "]\n";
return {};
} else if (import->base == "throw") {
throwEmptyException();
} else if (import->base == "table-get") {
// Check for errors here, duplicating tableLoad(), because that will
// trap, and we just want to throw an exception (the same as JS would).
if (!exportedTable) {
throwEmptyException();
}
auto index = arguments[0].getUnsigned();
if (index >= tables[exportedTable].size()) {
throwEmptyException();
}
return {tableLoad(exportedTable, index)};
} else if (import->base == "table-set") {
if (!exportedTable) {
throwEmptyException();
}
auto index = arguments[0].getUnsigned();
if (index >= tables[exportedTable].size()) {
throwEmptyException();
}
tableStore(exportedTable, index, arguments[1]);
return {};
} else if (import->base == "call-export") {
callExportAsJS(arguments[0].geti32());
// Return nothing. If we wanted to return a value we'd need to have
// multiple such functions, one for each signature.
return {};
} else if (import->base == "call-export-catch") {
try {
callExportAsJS(arguments[0].geti32());
return {Literal(int32_t(0))};
} catch (const WasmException& e) {
return {Literal(int32_t(1))};
}
} else if (import->base == "call-ref") {
callRefAsJS(arguments[0]);
// Return nothing. If we wanted to return a value we'd need to have
// multiple such functions, one for each signature.
return {};
} else if (import->base == "call-ref-catch") {
try {
callRefAsJS(arguments[0]);
return {Literal(int32_t(0))};
} catch (const WasmException& e) {
return {Literal(int32_t(1))};
}
} else if (import->base == "sleep") {
// Do not actually sleep, just return the id.
return {arguments[1]};
} else {
WASM_UNREACHABLE("unknown fuzzer import");
}
} else if (import->module == ENV) {
if (import->base == "log_execution") {
std::cout << "[LoggingExternalInterface log-execution";
for (auto argument : arguments) {
std::cout << ' ' << argument;
}
std::cout << "]\n";
return {};
} else if (import->base == "setTempRet0") {
state.tempRet0 = arguments[0].geti32();
return {};
} else if (import->base == "getTempRet0") {
return {Literal(state.tempRet0)};
}
}
std::cerr << "[LoggingExternalInterface ignoring an unknown import "
<< import->module << " . " << import->base << '\n';
return {};
}
void throwEmptyException() {
// Use a hopefully private tag.
auto payload = std::make_shared<ExnData>("__private", Literals{});
throwException(WasmException{Literal(payload)});
}
Literals callExportAsJS(Index index) {
if (index >= wasm.exports.size()) {
// No export.
throwEmptyException();
}
auto& exp = wasm.exports[index];
if (exp->kind != ExternalKind::Function) {
// No callable export.
throwEmptyException();
}
return callFunctionAsJS(exp->value);
}
Literals callRefAsJS(Literal ref) {
if (!ref.isFunction()) {
// Not a callable ref.
throwEmptyException();
}
return callFunctionAsJS(ref.getFunc());
}
// Call a function in a "JS-ey" manner, adding arguments as needed, and
// throwing if necessary, the same way JS does.
Literals callFunctionAsJS(Name name) {
auto* func = wasm.getFunction(name);
// Send default values as arguments, or error if we need anything else.
Literals arguments;
for (const auto& param : func->getParams()) {
// An i64 param can work from JS, but fuzz_shell provides 0, which errors
// on attempts to convert it to BigInt. v128 and exnref are disalloewd.
if (param == Type::i64 || param == Type::v128 || param.isExn()) {
throwEmptyException();
}
if (!param.isDefaultable()) {
throwEmptyException();
}
arguments.push_back(Literal::makeZero(param));
}
// Error on illegal results. Note that this happens, as per JS semantics,
// *before* the call.
for (const auto& result : func->getResults()) {
// An i64 result is fine: a BigInt will be provided. But v128 and exnref
// still error.
if (result == Type::v128 || result.isExn()) {
throwEmptyException();
}
}
// Call the function.
return instance->callFunction(func->name, arguments);
}
void setModuleRunner(ModuleRunner* instance_) { instance = instance_; }
};
// gets execution results from a wasm module. this is useful for fuzzing
//
// we can only get results when there are no imports. we then call each method
// that has a result, with some values
struct ExecutionResults {
struct Trap {};
struct Exception {};
using FunctionResult = std::variant<Literals, Trap, Exception>;
std::map<Name, FunctionResult> results;
Loggings loggings;
// If set, we should ignore this and not compare it to anything.
bool ignore = false;
// get results of execution
void get(Module& wasm) {
LoggingExternalInterface interface(loggings, wasm);
try {
ModuleRunner instance(wasm, &interface);
interface.setModuleRunner(&instance);
// execute all exported methods (that are therefore preserved through
// opts)
for (auto& exp : wasm.exports) {
if (exp->kind != ExternalKind::Function) {
continue;
}
std::cout << "[fuzz-exec] calling " << exp->name << "\n";
auto* func = wasm.getFunction(exp->value);
FunctionResult ret = run(func, wasm, instance);
results[exp->name] = ret;
if (auto* values = std::get_if<Literals>(&ret)) {
// ignore the result if we hit an unreachable and returned no value
if (values->size() > 0) {
std::cout << "[fuzz-exec] note result: " << exp->name << " => ";
for (auto value : *values) {
printValue(value);
}
}
}
}
} catch (const TrapException&) {
// May throw in instance creation (init of offsets).
} catch (const HostLimitException&) {
// May throw in instance creation (e.g. array.new of huge size).
// This should be ignored and not compared with, as optimizations can
// change whether a host limit is reached.
ignore = true;
}
}
void printValue(Literal value) {
// Unwrap an externalized value to get the actual value.
if (Type::isSubType(value.type, Type(HeapType::ext, Nullable))) {
value = value.internalize();
}
// Don't print most reference values, as e.g. funcref(N) contains an index,
// which is not guaranteed to remain identical after optimizations. Do not
// print the type in detail (as even that may change due to closed-world
// optimizations); just print a simple type like JS does, 'object' or
// 'function', but also print null for a null (so a null function does not
// get printed as object, as in JS we have typeof null == 'object').
//
// The only references we print in full are strings and i31s, which have
// simple and stable internal structures that optimizations will not alter.
auto type = value.type;
if (type.isRef()) {
if (type.isString() || type.getHeapType().isMaybeShared(HeapType::i31)) {
std::cout << value << '\n';
} else if (value.isNull()) {
std::cout << "null\n";
} else if (type.isFunction()) {
std::cout << "function\n";
} else {
std::cout << "object\n";
}
return;
}
// Non-references can be printed in full.
std::cout << value << '\n';
}
// get current results and check them against previous ones
void check(Module& wasm) {
ExecutionResults optimizedResults;
optimizedResults.get(wasm);
if (optimizedResults != *this) {
std::cout << "[fuzz-exec] optimization passes changed results\n";
exit(1);
}
}
bool areEqual(Literal a, Literal b) {
// Don't compare references. There are several issues here that we can't
// fully handle, see https://github.com/WebAssembly/binaryen/issues/3378,
// but the core issue is that since we optimize assuming a closed world, the
// types and structure of GC data can arbitrarily change after
// optimizations, even in ways that are externally visible from outside
// the module.
//
// We can, however, compare strings as they refer to simple data that has a
// consistent representation (the same reasons as why we can print them in
// printValue(), above).
//
// TODO: Once we support optimizing under some form of open-world
// assumption, we should be able to check that the types and/or structure of
// GC data passed out of the module does not change.
if (a.type.isRef() && !a.type.isString() &&
!a.type.getHeapType().isMaybeShared(HeapType::i31)) {
return true;
}
if (a != b) {
std::cout << "values not identical! " << a << " != " << b << '\n';
return false;
}
return true;
}
bool areEqual(Literals a, Literals b) {
if (a.size() != b.size()) {
std::cout << "literal counts not identical! " << a << " != " << b << '\n';
return false;
}
for (Index i = 0; i < a.size(); i++) {
if (!areEqual(a[i], b[i])) {
return false;
}
}
return true;
}
bool operator==(ExecutionResults& other) {
if (ignore || other.ignore) {
std::cout << "ignoring comparison of ExecutionResults!\n";
return true;
}
for (auto& [name, _] : other.results) {
if (results.find(name) == results.end()) {
std::cout << "[fuzz-exec] missing " << name << '\n';
return false;
}
std::cout << "[fuzz-exec] comparing " << name << '\n';
if (results[name].index() != other.results[name].index()) {
return false;
}
auto* values = std::get_if<Literals>(&results[name]);
auto* otherValues = std::get_if<Literals>(&other.results[name]);
if (values && otherValues && !areEqual(*values, *otherValues)) {
return false;
}
}
if (loggings.size() != other.loggings.size()) {
std::cout << "logging counts not identical!\n";
return false;
}
for (Index i = 0; i < loggings.size(); i++) {
if (!areEqual(loggings[i], other.loggings[i])) {
return false;
}
}
return true;
}
bool operator!=(ExecutionResults& other) { return !((*this) == other); }
FunctionResult run(Function* func, Module& wasm) {
LoggingExternalInterface interface(loggings, wasm);
try {
ModuleRunner instance(wasm, &interface);
interface.setModuleRunner(&instance);
return run(func, wasm, instance);
} catch (const TrapException&) {
// May throw in instance creation (init of offsets).
return {};
} catch (const HostLimitException&) {
// May throw in instance creation (e.g. array.new of huge size).
// This should be ignored and not compared with, as optimizations can
// change whether a host limit is reached.
ignore = true;
return {};
}
}
FunctionResult run(Function* func, Module& wasm, ModuleRunner& instance) {
try {
// call the method
Literals arguments;
for (const auto& param : func->getParams()) {
// zeros in arguments TODO: more?
if (!param.isDefaultable()) {
std::cout << "[trap fuzzer can only send defaultable parameters to "
"exports]\n";
return Trap{};
}
arguments.push_back(Literal::makeZero(param));
}
return instance.callFunction(func->name, arguments);
} catch (const TrapException&) {
return Trap{};
} catch (const WasmException& e) {
std::cout << "[exception thrown: " << e << "]" << std::endl;
return Exception{};
} catch (const HostLimitException&) {
// This should be ignored and not compared with, as optimizations can
// change whether a host limit is reached.
ignore = true;
return {};
}
}
};
} // namespace wasm