diff options
-rwxr-xr-x | scripts/fuzz_opt.py | 182 | ||||
-rw-r--r-- | src/tools/execution-results.h | 1 | ||||
-rw-r--r-- | src/tools/wasm-opt.cpp | 17 | ||||
-rw-r--r-- | src/tools/wasm2c-wrapper.h | 186 |
4 files changed, 323 insertions, 63 deletions
diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index fa42c5640..16a09eeb6 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -292,27 +292,78 @@ class TestCaseHandler: # Run VMs and compare results class VM: - def __init__(self, name, run, deterministic_nans, requires_legalization): + def __init__(self, name, run, can_run, can_compare_to_self, can_compare_to_others): self.name = name self.run = run - self.deterministic_nans = deterministic_nans - self.requires_legalization = requires_legalization + self.can_run = can_run + self.can_compare_to_self = can_compare_to_self + self.can_compare_to_others = can_compare_to_others class CompareVMs(TestCaseHandler): def __init__(self): super(CompareVMs, self).__init__() - def run_binaryen_interpreter(wasm): + def byn_run(wasm): return run_bynterp(wasm, ['--fuzz-exec-before']) - def run_v8(wasm): + def v8_run(wasm): run([in_bin('wasm-opt'), wasm, '--emit-js-wrapper=' + wasm + '.js'] + FEATURE_OPTS) return run_vm([shared.V8, wasm + '.js'] + shared.V8_OPTS + ['--', wasm]) + def yes(): + return True + + def if_legal_and_no_nans(): + return LEGALIZE and not NANS + + def if_no_nans(): + return not NANS + + class Wasm2C(VM): + name = 'wasm2c' + + def __init__(self): + # look for wabt in the path. if it's not here, don't run wasm2c + try: + wabt_bin = shared.which('wasm2c') + wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) + self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') + except Exception as e: + print('warning: no wabt found:', e) + self.wasm2c_dir = None + + def can_run(self): + if self.wasm2c_dir is None: + return False + # if we legalize for JS, the ABI is not what C wants + if LEGALIZE: + return False + # wasm2c doesn't support most features + return all([x in FEATURE_OPTS for x in ['--disable-exception-handling', '--disable-simd', '--disable-threads', '--disable-bulk-memory', '--disable-nontrapping-float-to-int', '--disable-tail-call', '--disable-sign-ext', '--disable-reference-types', '--disable-multivalue']]) + + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] + run(compile_cmd) + return run_vm(['./a.out']) + + def can_compare_to_self(self): + # The binaryen optimizer changes NaNs in the ways that wasm + # expects, but that's not quite what C has + return not NANS + + def can_compare_to_others(self): + # C won't trap on OOB, and NaNs can differ from wasm VMs + return not OOB and not NANS + self.vms = [ - VM('binaryen interpreter', run_binaryen_interpreter, deterministic_nans=True, requires_legalization=False), - VM('d8', run_v8, deterministic_nans=False, requires_legalization=True), + VM('binaryen interpreter', byn_run, can_run=yes, can_compare_to_self=yes, can_compare_to_others=yes), + # with nans, VM differences can confuse us, so only very simple VMs can compare to themselves after opts in that case. + # if not legalized, the JS will fail immediately, so no point to compare to others + VM('d8', v8_run, can_run=yes, can_compare_to_self=if_no_nans, can_compare_to_others=if_legal_and_no_nans), + Wasm2C() ] def handle_pair(self, input, before_wasm, after_wasm, opts): @@ -321,32 +372,38 @@ class CompareVMs(TestCaseHandler): self.compare_before_and_after(before, after) def run_vms(self, wasm): - results = [] + # vm_results will contain pairs of (vm, result) + vm_results = [] for vm in self.vms: - results.append(fix_output(vm.run(wasm))) + if vm.can_run(): + vm_results.append((vm, fix_output(vm.run(wasm)))) # compare between the vms on this specific input - # NaNs are a source of nondeterminism between VMs; don't compare them. - if not NANS: - first = None - for i in range(len(results)): - # No legalization for JS means we can't compare JS to others, as any - # illegal export will fail immediately. - if LEGALIZE or not vm.requires_legalization: - if first is None: - first = i - else: - compare_between_vms(results[first], results[i], 'CompareVMs between VMs: ' + self.vms[first].name + ' and ' + self.vms[i].name) - - return results + first_vm = None + first_result = None + for vm, result in vm_results: + if vm.can_compare_to_others(): + if first_vm is None: + first_vm = vm + first_result = result + else: + compare_between_vms(first_result, result, 'CompareVMs between VMs: ' + first_vm.name + ' and ' + vm.name) + + return vm_results def compare_before_and_after(self, before, after): + # we received lists of (vm, result). the lists must be of the same size, + # and with the same vms + assert len(before) == len(after) + num = len(before) + for i in range(num): + assert before[i][0] == after[i][0] + # compare each VM to itself on the before and after inputs - for i in range(len(before)): - vm = self.vms[i] - if vm.deterministic_nans: - compare(before[i], after[i], 'CompareVMs between before and after: ' + vm.name) + for i in range(num): + if before[i][0].can_compare_to_self(): + compare(before[i][1], after[i][1], 'CompareVMs between before and after: ' + before[i][0].name) def can_run_on_feature_opts(self, feature_opts): return all([x in feature_opts for x in ['--disable-simd', '--disable-reference-types', '--disable-exception-handling', '--disable-multivalue']]) @@ -487,7 +544,7 @@ testcase_handlers = [ # Do one test, given an input file for -ttf and some optimizations to run -def test_one(random_input, opts): +def test_one(random_input, opts, allow_autoreduce): randomize_pass_debug() randomize_feature_opts() randomize_fuzz_settings() @@ -535,40 +592,41 @@ def test_one(random_input, opts): try: write_commands_and_test(opts) except subprocess.CalledProcessError: - print('') - print('====================') - print('Found a problem! See "t.sh" for the commands, and "input.wasm" for the input. Auto-reducing to "reduced.wasm" and "tt.sh"...') - print('====================') - print('') - # first, reduce the fuzz opts: keep removing until we can't - while 1: - reduced = False - for i in range(len(opts)): - # some opts can't be removed, like --flatten --dfo requires flatten - if opts[i] == '--flatten': - if i != len(opts) - 1 and opts[i + 1] in ('--dfo', '--local-cse', '--rereloop'): - continue - shorter = opts[:i] + opts[i + 1:] - try: - write_commands_and_test(shorter) - except subprocess.CalledProcessError: - # great, the shorter one is good as well - opts = shorter - print('reduced opts to ' + ' '.join(opts)) - reduced = True + if allow_autoreduce: + print('') + print('====================') + print('Found a problem! See "t.sh" for the commands, and "input.wasm" for the input. Auto-reducing to "reduced.wasm" and "tt.sh"...') + print('====================') + print('') + # first, reduce the fuzz opts: keep removing until we can't + while 1: + reduced = False + for i in range(len(opts)): + # some opts can't be removed, like --flatten --dfo requires flatten + if opts[i] == '--flatten': + if i != len(opts) - 1 and opts[i + 1] in ('--dfo', '--local-cse', '--rereloop'): + continue + shorter = opts[:i] + opts[i + 1:] + try: + write_commands_and_test(shorter) + except subprocess.CalledProcessError: + # great, the shorter one is good as well + opts = shorter + print('reduced opts to ' + ' '.join(opts)) + reduced = True + break + if not reduced: break - if not reduced: - break - # second, reduce the wasm - # copy a.wasm to a safe place as the reducer will use the commands on new inputs, and the commands work on a.wasm - shutil.copyfile('a.wasm', 'input.wasm') - # add a command to verify the input. this lets the reducer see that it is indeed working on the input correctly - commands = [in_bin('wasm-opt') + ' -all a.wasm'] + get_commands(opts) - write_commands(commands, 'tt.sh') - # reduce the input to something smaller with the same behavior on the script - subprocess.check_call([in_bin('wasm-reduce'), 'input.wasm', '--command=bash tt.sh', '-t', 'a.wasm', '-w', 'reduced.wasm']) - print('Finished reduction. See "tt.sh" and "reduced.wasm".') - raise Exception('halting after autoreduction') + # second, reduce the wasm + # copy a.wasm to a safe place as the reducer will use the commands on new inputs, and the commands work on a.wasm + shutil.copyfile('a.wasm', 'input.wasm') + # add a command to verify the input. this lets the reducer see that it is indeed working on the input correctly + commands = [in_bin('wasm-opt') + ' -all a.wasm'] + get_commands(opts) + write_commands(commands, 'tt.sh') + # reduce the input to something smaller with the same behavior on the script + subprocess.check_call([in_bin('wasm-reduce'), 'input.wasm', '--command=bash tt.sh', '-t', 'a.wasm', '-w', 'reduced.wasm']) + print('Finished reduction. See "tt.sh" and "reduced.wasm".') + raise Exception('halting after autoreduction') print('') # create a second wasm for handlers that want to look at pairs. @@ -736,7 +794,9 @@ if __name__ == '__main__': opts = randomize_opt_flags() print('randomized opts:', ' '.join(opts)) try: - total_wasm_size += test_one(raw_input_data, opts) + # don't autoreduce if we are given a specific case to test, as this + # is a reproduction of the test case, not the first finding of it + total_wasm_size += test_one(raw_input_data, opts, allow_autoreduce=given_seed is None) except KeyboardInterrupt: print('(stopping by user request)') break diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 23b565082..e5d4dab00 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -18,7 +18,6 @@ // Shared execution result checking code // -#include "ir/import-utils.h" #include "shell-interface.h" #include "wasm.h" diff --git a/src/tools/wasm-opt.cpp b/src/tools/wasm-opt.cpp index c4443bde4..6fd3b7c8d 100644 --- a/src/tools/wasm-opt.cpp +++ b/src/tools/wasm-opt.cpp @@ -37,6 +37,7 @@ #include "wasm-printing.h" #include "wasm-s-parser.h" #include "wasm-validator.h" +#include "wasm2c-wrapper.h" #define DEBUG_TYPE "opt" @@ -87,6 +88,7 @@ int main(int argc, const char* argv[]) { bool fuzzOOB = true; std::string emitJSWrapper; std::string emitSpecWrapper; + std::string emitWasm2CWrapper; std::string inputSourceMapFilename; std::string outputSourceMapFilename; std::string outputSourceMapUrl; @@ -185,6 +187,14 @@ int main(int argc, const char* argv[]) { [&](Options* o, const std::string& arguments) { emitSpecWrapper = arguments; }) + .add("--emit-wasm2c-wrapper", + "-esw", + "Emit a C wrapper file that can run the wasm after it is compiled " + "with wasm2c, useful for fuzzing", + Options::Arguments::One, + [&](Options* o, const std::string& arguments) { + emitWasm2CWrapper = arguments; + }) .add("--input-source-map", "-ism", "Consume source map from the specified file", @@ -293,13 +303,18 @@ int main(int argc, const char* argv[]) { outfile << generateJSWrapper(wasm); outfile.close(); } - if (emitSpecWrapper.size() > 0) { std::ofstream outfile; outfile.open(emitSpecWrapper, std::ofstream::out); outfile << generateSpecWrapper(wasm); outfile.close(); } + if (emitWasm2CWrapper.size() > 0) { + std::ofstream outfile; + outfile.open(emitWasm2CWrapper, std::ofstream::out); + outfile << generateWasm2CWrapper(wasm); + outfile.close(); + } std::string firstOutput; diff --git a/src/tools/wasm2c-wrapper.h b/src/tools/wasm2c-wrapper.h new file mode 100644 index 000000000..684bd52f7 --- /dev/null +++ b/src/tools/wasm2c-wrapper.h @@ -0,0 +1,186 @@ +/* + * Copyright 2020 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. + */ + +// +// Emit a C wrapper file that can run the wasm after it is compiled with +// wasm2c, useful for fuzzing. +// + +#include <string> + +#include "wasm.h" + +namespace wasm { + +static std::string generateWasm2CWrapper(Module& wasm) { + // First, emit implementations of the wasm's imports so that the wasm2c code + // can call them. The names use wasm2c's name mangling. + std::string ret = R"( +#include <stdint.h> +#include <stdio.h> + +#include "wasm-rt-impl.h" +#include "wasm.h" + +void _Z_fuzzingZ2DsupportZ_logZ2Di32Z_vi(u32 x) { + printf("[LoggingExternalInterface logging %d]\n", x); +} +void (*Z_fuzzingZ2DsupportZ_logZ2Di32Z_vi)(u32) = _Z_fuzzingZ2DsupportZ_logZ2Di32Z_vi; + +void _Z_fuzzingZ2DsupportZ_logZ2Di64Z_vj(u64 x) { + printf("[LoggingExternalInterface logging %ld]\n", (long)x); +} +void (*Z_fuzzingZ2DsupportZ_logZ2Di64Z_vj)(u64) = _Z_fuzzingZ2DsupportZ_logZ2Di64Z_vj; + +void _Z_fuzzingZ2DsupportZ_logZ2Di64Z_vii(u32 x, u32 y) { + printf("[LoggingExternalInterface logging %d %d]\n", x, y); +} +void (*Z_fuzzingZ2DsupportZ_logZ2Di64Z_vii)(u32, u32) = _Z_fuzzingZ2DsupportZ_logZ2Di64Z_vii; + +void _Z_fuzzingZ2DsupportZ_logZ2Df32Z_vf(f32 x) { + printf("[LoggingExternalInterface logging %.17e]\n", x); +} +void (*Z_fuzzingZ2DsupportZ_logZ2Df32Z_vf)(f32) = _Z_fuzzingZ2DsupportZ_logZ2Df32Z_vf; + +void _Z_fuzzingZ2DsupportZ_logZ2Df64Z_vd(f64 x) { + printf("[LoggingExternalInterface logging %.17le]\n", x); +} +void (*Z_fuzzingZ2DsupportZ_logZ2Df64Z_vd)(f64) = _Z_fuzzingZ2DsupportZ_logZ2Df64Z_vd; + +// Miscellaneous imports + +u32 tempRet0 = 0; + +void _Z_envZ_setTempRet0Z_vi(u32 x) { + tempRet0 = x; +} +void (*Z_envZ_setTempRet0Z_vi)(u32) = _Z_envZ_setTempRet0Z_vi; + +u32 _Z_envZ_getTempRet0Z_iv(void) { + return tempRet0; +} +u32 (*Z_envZ_getTempRet0Z_iv)(void) = _Z_envZ_getTempRet0Z_iv; + +// Main + +int main(int argc, char** argv) { + init(); + +)"; + + // For each export in the wasm, emit code to call it and log its result, + // similar to the other wrappers. + for (auto& exp : wasm.exports) { + if (exp->kind != ExternalKind::Function) { + continue; + } + + // Always call the hang limit initializer before each export. + ret += " (*Z_hangLimitInitializerZ_vv)();\n"; + + auto* func = wasm.getFunction(exp->value); + + // Emit a setjmp so that we can handle traps from the compiled wasm. + ret += + std::string(" puts(\"[fuzz-exec] calling ") + exp->name.str + "\");\n"; + ret += " if (setjmp(g_jmp_buf) == 0) {\n"; + auto result = func->sig.results; + + // Emit the call itself. + ret += " "; + if (result != Type::none) { + ret += std::string("printf(\"[fuzz-exec] note result: ") + exp->name.str + + " => "; + switch (result.getSingle()) { + case Type::i32: + ret += "%d\\n\", "; + break; + case Type::i64: + ret += "%ld\\n\", (long)"; + break; + case Type::f32: + ret += "%.17e\\n\", "; + break; + case Type::f64: + ret += "%.17le\\n\", "; + break; + default: + Fatal() << "unhandled wasm2c wrapper result type: " << result; + } + } + + // Emit the callee's name with wasm2c name mangling. + ret += std::string("(*Z_") + exp->name.str + "Z_"; + auto params = func->sig.params.expand(); + + auto wasm2cSignature = [](Type type) { + switch (type.getSingle()) { + case Type::none: + return 'v'; + case Type::i32: + return 'i'; + case Type::i64: + return 'j'; + case Type::f32: + return 'f'; + case Type::f64: + return 'd'; + default: + Fatal() << "unhandled wasm2c wrapper signature type: " << type; + } + }; + + ret += wasm2cSignature(result); + if (params.empty()) { + ret += wasm2cSignature(Type::none); + } else { + for (auto param : params) { + ret += wasm2cSignature(param); + } + } + ret += ")("; + + // Emit the parameters (all 0s, like the other wrappers). + bool first = true; + for (auto param : params) { + WASM_UNUSED(param); + if (!first) { + ret += ", "; + } + ret += "0"; + first = false; + } + if (result != Type::none) { + ret += ")"; + } + ret += ");\n"; + + // Handle a longjmp which indicates a trap happened. + ret += " } else {\n"; + ret += " puts(\"exception!\");\n"; + ret += " }\n"; + } + + ret += R"( + + return 0; +} +)"; + + return ret; +} + +} // namespace wasm |