/* * 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; // 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("__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; std::map 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(&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(&results[name]); auto* otherValues = std::get_if(&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