diff options
Diffstat (limited to 'scripts/fuzz_opt.py')
-rwxr-xr-x | scripts/fuzz_opt.py | 87 |
1 files changed, 85 insertions, 2 deletions
diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index e8c497306..57b1c0400 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -472,6 +472,9 @@ def numbers_are_close_enough(x, y): return False +FUZZ_EXEC_NOTE_RESULT = '[fuzz-exec] note result' + + # compare between vms, which may slightly change how numbers are printed def compare_between_vms(x, y, context): x_lines = x.splitlines() @@ -491,8 +494,7 @@ def compare_between_vms(x, y, context): y_val = y_line[len(LEI_LOGGING) + 1:-1] if numbers_are_close_enough(x_val, y_val): continue - NOTE_RESULT = '[fuzz-exec] note result' - if x_line.startswith(NOTE_RESULT) and y_line.startswith(NOTE_RESULT): + if x_line.startswith(FUZZ_EXEC_NOTE_RESULT) and y_line.startswith(FUZZ_EXEC_NOTE_RESULT): x_val = x_line.split(' ')[-1] y_val = y_line.split(' ')[-1] if numbers_are_close_enough(x_val, y_val): @@ -516,8 +518,14 @@ def fix_output(out): x = str(float(x)) return 'f64.const ' + x out = re.sub(r'f64\.const (-?[nanN:abcdefxIity\d+-.]+)', fix_double, out) + # mark traps from wasm-opt as exceptions, even though they didn't run in a vm out = out.replace(TRAP_PREFIX, 'exception: ' + TRAP_PREFIX) + + # funcref(0) has the index of the function in it, and optimizations can + # change that index, so ignore it + out = re.sub(r'funcref\([\d\w$+-_:]+\)', 'funcref()', out) + lines = out.splitlines() for i in range(len(lines)): line = lines[i] @@ -1036,6 +1044,80 @@ class Asyncify(TestCaseHandler): return all_disallowed(['exception-handling', 'simd', 'tail-call', 'reference-types', 'multivalue', 'gc']) +# 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 +# fuzzer therefore needs to ignore code that traps. +class TrapsNeverHappen(TestCaseHandler): + frequency = 1 + + 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 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: + # + # [fuzz-exec] calling foo + # .. stuff happening during foo .. + # [fuzz-exec] calling bar + # .. stuff happening during bar .. + # + # if the trap happened during bar, the relevant call line is + # "[fuzz-exec] calling bar". + call_start = before.rfind(FUZZ_EXEC_CALL_PREFIX, 0, trap_index) + if call_start < 0: + # the trap happened before we called an export, so it occured + # during startup (the start function, or memory segment + # operations, etc.). in that case there is nothing for us to + # compare here; just leave. + return + call_end = before.index('\n', call_start) + # we now know the contents of the call line after which the trap + # 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] + # find that call line, and remove everything from it onward. + before_index = before.index(call_line) + lines_pre = before.count(os.linesep) + before = before[:before_index] + 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. + after_index = after.index(call_line) + after = after[:after_index] + + # some results cannot be compared, so we must filter them out here. + def ignore_references(out): + ret = [] + for line in out.splitlines(): + # only result lines are relevant here, which look like + # [fuzz-exec] note result: foo => [...] + if FUZZ_EXEC_NOTE_RESULT in line: + # we want to filter out things like "anyref(null)" or + # "[ref null data]". + if 'ref(' in line or 'ref ' in line: + line = line[:line.index('=>') + 2] + ' ?' + ret.append(line) + return '\n'.join(ret) + + before = fix_output(ignore_references(before)) + after = fix_output(ignore_references(after)) + + compare_between_vms(before, after, 'TrapsNeverHappen') + + # Check that the text format round-trips without error. class RoundtripText(TestCaseHandler): frequency = 0.05 @@ -1056,6 +1138,7 @@ testcase_handlers = [ CheckDeterminism(), Wasm2JS(), Asyncify(), + TrapsNeverHappen(), # FIXME: Re-enable after https://github.com/WebAssembly/binaryen/issues/3989 # RoundtripText() ] |