diff options
Diffstat (limited to 'scripts')
-rwxr-xr-x | scripts/fuzz_opt.py | 28 | ||||
-rw-r--r-- | scripts/fuzz_shell.js | 75 |
2 files changed, 96 insertions, 7 deletions
diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 7522dce8c..bf712c821 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -1230,6 +1230,16 @@ def filter_exports(wasm, output, keep, keep_defaults=True): run([in_bin('wasm-metadce'), wasm, '-o', output, '--graph-file', 'graph.json'] + FEATURE_OPTS) +# Check if a wasm file would notice changes to exports. Normally removing an +# export that is not called, for example, would not be observable, but if the +# "call-export*" functions are present then such changes can break us. +def wasm_notices_export_changes(wasm): + # we could be more precise here and disassemble the wasm to look for an + # actual import with name "call-export*", but looking for the string should + # have practically no false positives. + return b'call-export' in open(wasm, 'rb').read() + + # Fuzz the interpreter with --fuzz-exec -tnh. The tricky thing with traps-never- # happen mode is that if a trap *does* happen then that is undefined behavior, # and the optimizer was free to make changes to observable behavior there. The @@ -1323,6 +1333,12 @@ class TrapsNeverHappen(TestCaseHandler): compare_between_vms(before, after, 'TrapsNeverHappen') + def can_run_on_wasm(self, wasm): + # If the wasm is sensitive to changes in exports then we cannot alter + # them, but we must remove trapping exports (see above), so we cannot + # run in such a case. + return not wasm_notices_export_changes(wasm) + # Tests wasm-ctor-eval class CtorEval(TestCaseHandler): @@ -1354,6 +1370,12 @@ class CtorEval(TestCaseHandler): compare_between_vms(fix_output(wasm_exec), fix_output(evalled_wasm_exec), 'CtorEval') + def can_run_on_wasm(self, wasm): + # ctor-eval modifies exports, because it assumes they are ctors and so + # are only called once (so if it evals them away, they can be + # removed). If the wasm might notice that, we cannot run. + return not wasm_notices_export_changes(wasm) + # Tests wasm-merge class Merge(TestCaseHandler): @@ -1427,6 +1449,12 @@ class Merge(TestCaseHandler): compare_between_vms(output, merged_output, 'Merge') + def can_run_on_wasm(self, wasm): + # wasm-merge combines exports, which can alter their indexes and lead to + # noticeable differences if the wasm is sensitive to such things, which + # prevents us from running. + return not wasm_notices_export_changes(wasm) + FUNC_NAMES_REGEX = re.compile(r'\n [(]func [$](\S+)') diff --git a/scripts/fuzz_shell.js b/scripts/fuzz_shell.js index 72120cf7f..d9a994896 100644 --- a/scripts/fuzz_shell.js +++ b/scripts/fuzz_shell.js @@ -134,6 +134,29 @@ function logValue(x, y) { console.log('[LoggingExternalInterface logging ' + printed(x, y) + ']'); } +// Some imports need to access exports by index. +var exportsList; +function getExportByIndex(index) { + if (!exportsList) { + exportsList = []; + for (var e in exports) { + exportsList.push(e); + } + } + return exports[exportsList[index]]; +} + +// Given a wasm function, call it as best we can from JS, and return the result. +function callFunc(func) { + // Send the function a null for each parameter. Null can be converted without + // error to both a number and a reference. + var args = []; + for (var i = 0; i < func.length; i++) { + args.push(null); + } + return func.apply(null, args); +} + // Table get/set operations need a BigInt if the table has 64-bit indexes. This // adds a proper cast as needed. function toAddressType(table, index) { @@ -172,6 +195,50 @@ var imports = { 'table-set': (index, value) => { exports.table.set(toAddressType(exports.table, index), value); }, + + // Export operations. + 'call-export': (index) => { + callFunc(getExportByIndex(index)); + }, + 'call-export-catch': (index) => { + try { + callFunc(getExportByIndex(index)); + return 0; + } catch (e) { + // We only want to catch exceptions, not wasm traps: traps should still + // halt execution. Handling this requires different code in wasm2js, so + // check for that first (wasm2js does not define RuntimeError, so use + // that for the check - when wasm2js is run, we override the entire + // WebAssembly object with a polyfill, so we know exactly what it + // contains). + var wasm2js = !WebAssembly.RuntimeError; + if (!wasm2js) { + // When running native wasm, we can detect wasm traps. + if (e instanceof WebAssembly.RuntimeError) { + throw e; + } + } + var text = e + ''; + // We must not swallow host limitations here: a host limitation is a + // problem that means we must not compare the outcome here to any other + // VM. + var hostIssues = ['requested new array is too large', + 'out of memory', + 'Maximum call stack size exceeded']; + if (wasm2js) { + // When wasm2js does trap, it just throws an "abort" error. + hostIssues.push('abort'); + } + for (var hostIssue of hostIssues) { + if (text.includes(hostIssue)) { + throw e; + } + } + // Otherwise, this is a normal exception we want to catch (a wasm + // exception, or a conversion error on the wasm/JS boundary, etc.). + return 1; + } + }, }, // Emscripten support. 'env': { @@ -250,16 +317,10 @@ for (var e of exportsToCall) { if (typeof exports[e] !== 'function') { continue; } - // Send the function a null for each parameter. Null can be converted without - // error to both a number and a reference. var func = exports[e]; - var args = []; - for (var i = 0; i < func.length; i++) { - args.push(null); - } try { console.log('[fuzz-exec] calling ' + e); - var result = func.apply(null, args); + var result = callFunc(func); if (typeof result !== 'undefined') { console.log('[fuzz-exec] note result: ' + e + ' => ' + printed(result)); } |