summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorAlon Zakai <azakai@google.com>2024-11-08 10:16:52 -0800
committerGitHub <noreply@github.com>2024-11-08 10:16:52 -0800
commit8c0429ac09d06d6056687e36fd4fb37f61681233 (patch)
treec26a80f84cbee89a09f3c4c114180cb8d1cb30df /scripts
parentb30067658459ca167e58fe0dee9d85ea6100c223 (diff)
downloadbinaryen-8c0429ac09d06d6056687e36fd4fb37f61681233.tar.gz
binaryen-8c0429ac09d06d6056687e36fd4fb37f61681233.tar.bz2
binaryen-8c0429ac09d06d6056687e36fd4fb37f61681233.zip
[EH] Fuzz calls from JS by calling wasm exports, sometimes catching (#7067)
This adds two new imports to fuzzer modules: * call-export, which gets an export index and calls it. * call-export-catch, which does the call in a try-catch, swallowing any error, and returning 1 if it saw an error. The former gives us calls back into the wasm, possibly making various trips between wasm and JS in interesting ways. The latter adds a try-catch which helps fuzz wasm EH. We do these calls using a wasm export index, i.e., the index in the list of exports. This is simple, but it does have the downside that it makes executing the wasm sensitive to changes in exports (e.g. wasm-merge adds more), which requires some handling in the fuzzer.
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/fuzz_opt.py28
-rw-r--r--scripts/fuzz_shell.js75
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));
}