diff options
author | Alon Zakai <azakai@google.com> | 2023-05-12 11:00:28 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-12 11:00:28 -0700 |
commit | d7b14a67ca4e9b111d5733239af88328cb779030 (patch) | |
tree | b244025f92c0d689d6fd91c623ae618e26a85350 /scripts/fuzz_opt.py | |
parent | 64e5a99ee014af3a138a36f5a87addfacfc95f0c (diff) | |
download | binaryen-d7b14a67ca4e9b111d5733239af88328cb779030.tar.gz binaryen-d7b14a67ca4e9b111d5733239af88328cb779030.tar.bz2 binaryen-d7b14a67ca4e9b111d5733239af88328cb779030.zip |
Prune trapping code during TrapsNeverHappen fuzzing (#5717)
This removes the trapping export and all others after it. This avoids a potential
infinite loop that can happen when fuzzing TNH, as if TNH is set and a trap
happens then the optimizer can cause an iloop, and while that is valid, it would hang the
fuzzer. We could check for a timeout, but it is faster and more robust to just
remove the code we can't compare anyhow.
This uses wasm-metadce to remove the exports from the failing one.
Diffstat (limited to 'scripts/fuzz_opt.py')
-rwxr-xr-x | scripts/fuzz_opt.py | 93 |
1 files changed, 68 insertions, 25 deletions
diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 03334ffa7..8dc3041bd 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -25,6 +25,7 @@ script covers different options being passed) import contextlib import os import difflib +import json import math import shutil import subprocess @@ -452,6 +453,13 @@ FUZZ_EXEC_CALL_PREFIX = '[fuzz-exec] calling' STACK_LIMIT = '[trap stack limit]' +# given a call line that includes FUZZ_EXEC_CALL_PREFIX, return the export that +# is called +def get_export_from_call_line(call_line): + assert FUZZ_EXEC_CALL_PREFIX in call_line + return call_line.split(FUZZ_EXEC_CALL_PREFIX)[1].strip() + + # compare two strings, strictly def compare(x, y, context, verbose=True): if x != y and x != IGNORE and y != IGNORE: @@ -1119,6 +1127,30 @@ class Asyncify(TestCaseHandler): return all_disallowed(['exception-handling', 'simd', 'tail-call', 'reference-types', 'multivalue', 'gc', 'multi-memories']) +# given a wasm and a list of exports we want to keep, remove all other exports. +def filter_exports(wasm, output, keep): + # based on + # https://github.com/WebAssembly/binaryen/wiki/Pruning-unneeded-code-in-wasm-files-with-wasm-metadce#example-pruning-exports + + # build json to represent the exports we want. + graph = [{ + 'name': 'outside', + 'reaches': [f'export-{export}' for export in keep], + 'root': True + }] + for export in keep: + graph.append({ + 'name': f'export-{export}', + 'export': export + }) + + with open('graph.json', 'w') as f: + f.write(json.dumps(graph)) + + # prune the exports + run([in_bin('wasm-metadce'), wasm, '-o', output, '--graph-file', 'graph.json', '-all']) + + # 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 @@ -1128,20 +1160,24 @@ class TrapsNeverHappen(TestCaseHandler): def handle_pair(self, input, before_wasm, after_wasm, opts): before = run_bynterp(before_wasm, ['--fuzz-exec-before']) - after_wasm_tnh = after_wasm + '.tnh.wasm' - run([in_bin('wasm-opt'), before_wasm, '-o', after_wasm_tnh, '-tnh'] + opts + FEATURE_OPTS) - after = run_bynterp(after_wasm_tnh, ['--fuzz-exec-before']) + + if before == IGNORE: + # There is no point to continue since we can't compare this output + # to anything, and there is a risk since if we did so we might run + # into an infinite loop (see below). + return # if a trap happened, we must stop comparing from that. if TRAP_PREFIX in before: trap_index = before.index(TRAP_PREFIX) # we can't test this function, which the trap is in the middle of # (tnh could move the trap around, so even things before the trap - # are unsafe). erase everything from this function's output and - # onward, so we only compare the previous trap-free code. first, - # find the function call during which the trap happened, by finding - # the call line right before us. that is, the output looks like - # this: + # are unsafe). we can only safely call exports before this one, so + # remove those from the binary. + # + # first, find the function call during which the trap happened, by + # finding the call line right before us. that is, the output looks + # like this: # # [fuzz-exec] calling foo # .. stuff happening during foo .. @@ -1164,23 +1200,30 @@ class TrapsNeverHappen(TestCaseHandler): # happens, which is something like "[fuzz-exec] calling bar", and # it is unique since it contains the function being called. call_line = before[call_start:call_end] - # remove everything from that call line onward. - lines_pre = before.count(os.linesep) - before = before[:call_start] - lines_post = before.count(os.linesep) - print(f'ignoring code due to trap (from "{call_line}"), lines to compare goes {lines_pre} => {lines_post} ') - - # also remove the relevant lines from after. - if call_line not in after: - # the normal run hit a trap, and the tnh run hit a host - # limitation that forces us to ignore this run. for example, - # after running tnh we may end up doing an unbounded number of - # allocations, if that is what the program normally does (and - # the normal run only avoided that by trapping). - assert IGNORE in after - return - after_index = after.index(call_line) - after = after[:after_index] + trapping_export = get_export_from_call_line(call_line) + + # now that we know the trapping export, we can leave only the safe + # ones that are before it + safe_exports = [] + for line in before.splitlines(): + if FUZZ_EXEC_CALL_PREFIX in line: + export = get_export_from_call_line(line) + if export == trapping_export: + break + safe_exports.append(export) + + # filter out the other exports + filtered = before_wasm + '.filtered.wasm' + filter_exports(before_wasm, filtered, safe_exports) + before_wasm = filtered + + # re-execute the now safe wasm + before = run_bynterp(before_wasm, ['--fuzz-exec-before']) + assert TRAP_PREFIX not in before, 'we should have fixed this problem' + + after_wasm_tnh = after_wasm + '.tnh.wasm' + run([in_bin('wasm-opt'), before_wasm, '-o', after_wasm_tnh, '-tnh'] + opts + FEATURE_OPTS) + after = run_bynterp(after_wasm_tnh, ['--fuzz-exec-before']) # some results cannot be compared, so we must filter them out here. def ignore_references(out): |