Skip to content

Commit d7b14a6

Browse files
authored
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.
1 parent 64e5a99 commit d7b14a6

File tree

1 file changed

+68
-25
lines changed

1 file changed

+68
-25
lines changed

scripts/fuzz_opt.py

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import contextlib
2626
import os
2727
import difflib
28+
import json
2829
import math
2930
import shutil
3031
import subprocess
@@ -452,6 +453,13 @@ def pick_initial_contents():
452453
STACK_LIMIT = '[trap stack limit]'
453454

454455

456+
# given a call line that includes FUZZ_EXEC_CALL_PREFIX, return the export that
457+
# is called
458+
def get_export_from_call_line(call_line):
459+
assert FUZZ_EXEC_CALL_PREFIX in call_line
460+
return call_line.split(FUZZ_EXEC_CALL_PREFIX)[1].strip()
461+
462+
455463
# compare two strings, strictly
456464
def compare(x, y, context, verbose=True):
457465
if x != y and x != IGNORE and y != IGNORE:
@@ -1119,6 +1127,30 @@ def can_run_on_feature_opts(self, feature_opts):
11191127
return all_disallowed(['exception-handling', 'simd', 'tail-call', 'reference-types', 'multivalue', 'gc', 'multi-memories'])
11201128

11211129

1130+
# given a wasm and a list of exports we want to keep, remove all other exports.
1131+
def filter_exports(wasm, output, keep):
1132+
# based on
1133+
# https://github.com/WebAssembly/binaryen/wiki/Pruning-unneeded-code-in-wasm-files-with-wasm-metadce#example-pruning-exports
1134+
1135+
# build json to represent the exports we want.
1136+
graph = [{
1137+
'name': 'outside',
1138+
'reaches': [f'export-{export}' for export in keep],
1139+
'root': True
1140+
}]
1141+
for export in keep:
1142+
graph.append({
1143+
'name': f'export-{export}',
1144+
'export': export
1145+
})
1146+
1147+
with open('graph.json', 'w') as f:
1148+
f.write(json.dumps(graph))
1149+
1150+
# prune the exports
1151+
run([in_bin('wasm-metadce'), wasm, '-o', output, '--graph-file', 'graph.json', '-all'])
1152+
1153+
11221154
# Fuzz the interpreter with --fuzz-exec -tnh. The tricky thing with traps-never-
11231155
# happen mode is that if a trap *does* happen then that is undefined behavior,
11241156
# and the optimizer was free to make changes to observable behavior there. The
@@ -1128,20 +1160,24 @@ class TrapsNeverHappen(TestCaseHandler):
11281160

11291161
def handle_pair(self, input, before_wasm, after_wasm, opts):
11301162
before = run_bynterp(before_wasm, ['--fuzz-exec-before'])
1131-
after_wasm_tnh = after_wasm + '.tnh.wasm'
1132-
run([in_bin('wasm-opt'), before_wasm, '-o', after_wasm_tnh, '-tnh'] + opts + FEATURE_OPTS)
1133-
after = run_bynterp(after_wasm_tnh, ['--fuzz-exec-before'])
1163+
1164+
if before == IGNORE:
1165+
# There is no point to continue since we can't compare this output
1166+
# to anything, and there is a risk since if we did so we might run
1167+
# into an infinite loop (see below).
1168+
return
11341169

11351170
# if a trap happened, we must stop comparing from that.
11361171
if TRAP_PREFIX in before:
11371172
trap_index = before.index(TRAP_PREFIX)
11381173
# we can't test this function, which the trap is in the middle of
11391174
# (tnh could move the trap around, so even things before the trap
1140-
# are unsafe). erase everything from this function's output and
1141-
# onward, so we only compare the previous trap-free code. first,
1142-
# find the function call during which the trap happened, by finding
1143-
# the call line right before us. that is, the output looks like
1144-
# this:
1175+
# are unsafe). we can only safely call exports before this one, so
1176+
# remove those from the binary.
1177+
#
1178+
# first, find the function call during which the trap happened, by
1179+
# finding the call line right before us. that is, the output looks
1180+
# like this:
11451181
#
11461182
# [fuzz-exec] calling foo
11471183
# .. stuff happening during foo ..
@@ -1164,23 +1200,30 @@ def handle_pair(self, input, before_wasm, after_wasm, opts):
11641200
# happens, which is something like "[fuzz-exec] calling bar", and
11651201
# it is unique since it contains the function being called.
11661202
call_line = before[call_start:call_end]
1167-
# remove everything from that call line onward.
1168-
lines_pre = before.count(os.linesep)
1169-
before = before[:call_start]
1170-
lines_post = before.count(os.linesep)
1171-
print(f'ignoring code due to trap (from "{call_line}"), lines to compare goes {lines_pre} => {lines_post} ')
1172-
1173-
# also remove the relevant lines from after.
1174-
if call_line not in after:
1175-
# the normal run hit a trap, and the tnh run hit a host
1176-
# limitation that forces us to ignore this run. for example,
1177-
# after running tnh we may end up doing an unbounded number of
1178-
# allocations, if that is what the program normally does (and
1179-
# the normal run only avoided that by trapping).
1180-
assert IGNORE in after
1181-
return
1182-
after_index = after.index(call_line)
1183-
after = after[:after_index]
1203+
trapping_export = get_export_from_call_line(call_line)
1204+
1205+
# now that we know the trapping export, we can leave only the safe
1206+
# ones that are before it
1207+
safe_exports = []
1208+
for line in before.splitlines():
1209+
if FUZZ_EXEC_CALL_PREFIX in line:
1210+
export = get_export_from_call_line(line)
1211+
if export == trapping_export:
1212+
break
1213+
safe_exports.append(export)
1214+
1215+
# filter out the other exports
1216+
filtered = before_wasm + '.filtered.wasm'
1217+
filter_exports(before_wasm, filtered, safe_exports)
1218+
before_wasm = filtered
1219+
1220+
# re-execute the now safe wasm
1221+
before = run_bynterp(before_wasm, ['--fuzz-exec-before'])
1222+
assert TRAP_PREFIX not in before, 'we should have fixed this problem'
1223+
1224+
after_wasm_tnh = after_wasm + '.tnh.wasm'
1225+
run([in_bin('wasm-opt'), before_wasm, '-o', after_wasm_tnh, '-tnh'] + opts + FEATURE_OPTS)
1226+
after = run_bynterp(after_wasm_tnh, ['--fuzz-exec-before'])
11841227

11851228
# some results cannot be compared, so we must filter them out here.
11861229
def ignore_references(out):

0 commit comments

Comments
 (0)