summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlon Zakai <azakai@google.com>2022-08-22 16:03:00 -0700
committerGitHub <noreply@github.com>2022-08-22 23:03:00 +0000
commit195c4e1804d5a4530c8216d4c9a138b56f676d10 (patch)
tree5aeca907c334f2673865868ce30ff5f111cbae4d
parent0e0c2d9d45068c450ad5df5de47948532dd12c53 (diff)
downloadbinaryen-195c4e1804d5a4530c8216d4c9a138b56f676d10.tar.gz
binaryen-195c4e1804d5a4530c8216d4c9a138b56f676d10.tar.bz2
binaryen-195c4e1804d5a4530c8216d4c9a138b56f676d10.zip
[Fuzzer] Fuzz TrapsNeverHappen mode (#4936)
This mode is tricky to fuzz because the mode is basically "assume traps never happen; if a trap does happen, that is undefined behavior". So if any trap occurs in the random fuzz testcase, we can't optimize with -tnh and assume the results stay to same. To avoid that, we ignore all functions from the first one that traps, that is, we only compare the code that ran without trapping. That often is a small subset of the total functions, sadly, but I do see that this ends up with some useful coverage despite the drawback. This also requires some fixes to comparing of references, specifically, funcrefs are printed with the function name/index, but that can change during opts, so ignore that. This wasn't noticed before because this new fuzzer mode does comparisons of --fuzz-exec-before output, instead of doing --fuzz-exec which runs it before and after and compares it internally in wasm-opt. Here we are comparing the output externally, which we didn't do before.
-rwxr-xr-xscripts/fuzz_opt.py87
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()
]