blob: fb777d2358d7331070f4587716ee8e7989db1533 [file] [log] [blame] [edit]
'''
Runs random passes and options on random inputs, using wasm-opt.
Can be configured to run just wasm-opt itself (using --fuzz-exec)
or also run VMs on it.
For afl-fuzz integration, you probably don't want this, and can use
something like
BINARYEN_CORES=1 BINARYEN_PASS_DEBUG=1 afl-fuzz -i afl-testcases/ -o afl-findings/ -m 100 -d -- bin/wasm-opt -ttf --fuzz-exec --Os @@
(that is on a fixed set of arguments to wasm-opt, though - this
script covers different options being passed)
'''
import os
import difflib
import subprocess
import random
import re
import shutil
import time
from test.shared import options, NODEJS
# parameters
NANS = True
FEATURE_OPTS = [] # '--all-features' etc
FUZZ_OPTS = []
V8_OPTS = [
'--experimental-wasm-eh',
'--experimental-wasm-mv',
'--experimental-wasm-sat-f2i-conversions',
'--experimental-wasm-se',
'--experimental-wasm-threads',
'--experimental-wasm-simd',
'--experimental-wasm-anyref',
'--experimental-wasm-bulk-memory',
'--experimental-wasm-return-call'
]
INPUT_SIZE_LIMIT = 150 * 1024
LOG_LIMIT = 125
# utilities
def in_bin(tool):
return os.path.join(options.binaryen_root, 'bin', tool)
def random_size():
return random.randint(1, INPUT_SIZE_LIMIT)
def run(cmd):
print(' '.join(cmd)[:LOG_LIMIT])
return subprocess.check_output(cmd)
def run_unchecked(cmd):
print(' '.join(cmd)[:LOG_LIMIT])
return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0]
def randomize_pass_debug():
if random.random() < 0.125:
print('[pass-debug]')
os.environ['BINARYEN_PASS_DEBUG'] = '1'
else:
os.environ['BINARYEN_PASS_DEBUG'] = '0'
del os.environ['BINARYEN_PASS_DEBUG']
# Test outputs we want to ignore are marked this way.
IGNORE = '[binaryen-fuzzer-ignore]'
def compare(x, y, comment):
if x != y and x != IGNORE and y != IGNORE:
message = ''.join([a.rstrip() + '\n' for a in difflib.unified_diff(x.split('\n'), y.split('\n'), fromfile='expected', tofile='actual')])
raise Exception(str(comment) + ": Expected to have '%s' == '%s', diff:\n\n%s" % (
x, y,
message
))
def fix_output(out):
# large doubles may print slightly different on different VMs
def fix_double(x):
x = x.group(1)
if 'nan' in x or 'NaN' in x:
x = 'nan'
else:
x = x.replace('Infinity', 'inf')
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 ', 'exception: [trap ')
# exceptions may differ when optimizing, but an exception should occur. so ignore their types
# also js engines print them out slightly differently
return '\n'.join(map(lambda x: ' *exception*' if 'exception' in x else x, out.split('\n')))
def fix_spec_output(out):
out = fix_output(out)
# spec shows a pointer when it traps, remove that
out = '\n'.join(map(lambda x: x if 'runtime trap' not in x else x[x.find('runtime trap'):], out.split('\n')))
# https://github.com/WebAssembly/spec/issues/543 , float consts are messed up
out = '\n'.join(map(lambda x: x if 'f32' not in x and 'f64' not in x else '', out.split('\n')))
return out
def run_vm(cmd):
# ignore some vm assertions, if bugs have already been filed
known_issues = [
'local count too large', # ignore this; can be caused by flatten, ssa, etc. passes
'liftoff-assembler.cc, line 239\n', # https://bugs.chromium.org/p/v8/issues/detail?id=8631
'liftoff-assembler.cc, line 245\n', # https://bugs.chromium.org/p/v8/issues/detail?id=8631
'liftoff-register.h, line 86\n', # https://bugs.chromium.org/p/v8/issues/detail?id=8632
]
try:
return run(cmd)
except:
output = run_unchecked(cmd)
for issue in known_issues:
if issue in output:
return IGNORE
raise
def run_bynterp(wasm):
return fix_output(run_vm([in_bin('wasm-opt'), wasm, '--fuzz-exec-before'] + FEATURE_OPTS))
def run_wasm2js(wasm):
wrapper = run([in_bin('wasm-opt'), wasm, '--emit-js-wrapper=/dev/stdout'] + FEATURE_OPTS)
main = run([in_bin('wasm2js'), wasm, '--emscripten'] + FEATURE_OPTS)
with open(os.path.join(options.binaryen_root, 'scripts', 'wasm2js.js')) as f:
glue = f.read()
with open('js.js', 'w') as f:
f.write(glue)
f.write(main)
f.write(wrapper)
out = fix_output(run_vm([NODEJS, 'js.js', 'a.wasm']))
if 'exception' in out:
# exception, so ignoring - wasm2js does not have normal wasm trapping, so opts can eliminate a trap
out = IGNORE
return out
def run_vms(prefix):
wasm = prefix + 'wasm'
results = []
results.append(run_bynterp(wasm))
results.append(fix_output(run_vm([os.path.expanduser('d8'), prefix + 'js'] + V8_OPTS + ['--', wasm])))
# results.append(run_wasm2js(wasm))
# append to add results from VMs
# results += [fix_output(run_vm([os.path.expanduser('d8'), prefix + 'js'] + V8_OPTS + ['--', prefix + 'wasm']))]
# results += [fix_output(run_vm([os.path.expanduser('~/.jsvu/jsc'), prefix + 'js', '--', prefix + 'wasm']))]
# spec has no mechanism to not halt on a trap. so we just check until the first trap, basically
# run(['../spec/interpreter/wasm', prefix + 'wasm'])
# results += [fix_spec_output(run_unchecked(['../spec/interpreter/wasm', prefix + 'wasm', '-e', open(prefix + 'wat').read()]))]
if len(results) == 0:
results = [0]
# NaNs are a source of nondeterminism between VMs; don't compare them
if not NANS:
first = results[0]
for i in range(len(results)):
compare(first, results[i], 'comparing between vms at ' + str(i))
return results
def test_one(infile, opts):
randomize_pass_debug()
bytes = 0
# fuzz vms
# gather VM outputs on input file
run([in_bin('wasm-opt'), infile, '-ttf', '--emit-js-wrapper=a.js', '--emit-spec-wrapper=a.wat', '-o', 'a.wasm'] + FUZZ_OPTS + FEATURE_OPTS)
wasm_size = os.stat('a.wasm').st_size
bytes += wasm_size
print('pre js size :', os.stat('a.js').st_size, ' wasm size:', wasm_size)
before = run_vms('a.')
print('----------------')
# gather VM outputs on processed file
run([in_bin('wasm-opt'), 'a.wasm', '-o', 'b.wasm'] + opts + FUZZ_OPTS + FEATURE_OPTS)
wasm_size = os.stat('b.wasm').st_size
bytes += wasm_size
print('post js size:', os.stat('a.js').st_size, ' wasm size:', wasm_size)
shutil.copyfile('a.js', 'b.js')
after = run_vms('b.')
for i in range(len(before)):
compare(before[i], after[i], 'comparing between builds at ' + str(i))
# with nans, we can only compare the binaryen interpreter to itself
if NANS:
break
# fuzz binaryen interpreter itself. separate invocation so result is easily fuzzable
run([in_bin('wasm-opt'), 'a.wasm', '--fuzz-exec', '--fuzz-binary'] + opts + FUZZ_OPTS + FEATURE_OPTS)
# check for determinism
run([in_bin('wasm-opt'), 'a.wasm', '-o', 'b.wasm'] + opts + FUZZ_OPTS + FEATURE_OPTS)
run([in_bin('wasm-opt'), 'a.wasm', '-o', 'c.wasm'] + opts + FUZZ_OPTS + FEATURE_OPTS)
assert open('b.wasm').read() == open('c.wasm').read(), 'output must be deterministic'
return bytes
# main
opt_choices = [
[],
['-O1'], ['-O2'], ['-O3'], ['-O4'], ['-Os'], ['-Oz'],
["--coalesce-locals"],
# XXX slow, non-default ["--coalesce-locals-learning"],
["--code-pushing"],
["--code-folding"],
["--const-hoisting"],
["--dae"],
["--dae-optimizing"],
["--dce"],
["--directize"],
["--flatten", "--dfo"],
["--duplicate-function-elimination"],
["--flatten"],
# ["--fpcast-emu"], # removes indirect call failures as it makes them go through regardless of type
["--inlining"],
["--inlining-optimizing"],
["--flatten", "--local-cse"],
["--generate-stack-ir"],
["--licm"],
["--memory-packing"],
["--merge-blocks"],
['--merge-locals'],
["--optimize-instructions"],
["--optimize-stack-ir"],
["--generate-stack-ir", "--optimize-stack-ir"],
["--pick-load-signs"],
["--precompute"],
["--precompute-propagate"],
["--print"],
["--remove-unused-brs"],
["--remove-unused-nonfunction-module-elements"],
["--remove-unused-module-elements"],
["--remove-unused-names"],
["--reorder-functions"],
["--reorder-locals"],
["--flatten", "--rereloop"],
["--rse"],
["--simplify-locals"],
["--simplify-locals-nonesting"],
["--simplify-locals-nostructure"],
["--simplify-locals-notee"],
["--simplify-locals-notee-nostructure"],
["--ssa"],
["--vacuum"],
]
def get_multiple_opt_choices():
ret = []
# core opts
while 1:
ret += random.choice(opt_choices)
if len(ret) > 20 or random.random() < 0.3:
break
# modifiers (if not already implied by a -O? option)
if '-O' not in str(ret):
if random.random() < 0.5:
ret += ['--optimize-level=' + str(random.randint(0, 3))]
if random.random() < 0.5:
ret += ['--shrink-level=' + str(random.randint(0, 3))]
return ret
# main
if not NANS:
FUZZ_OPTS += ['--no-fuzz-nans']
if __name__ == '__main__':
print('checking infinite random inputs')
random.seed(time.time() * os.getpid())
temp = 'input.dat'
counter = 0
bytes = 0 # wasm bytes tested
start_time = time.time()
while True:
counter += 1
f = open(temp, 'w')
size = random_size()
print('')
print('ITERATION:', counter, 'size:', size, 'speed:', counter / (time.time() - start_time), 'iters/sec, ', bytes / (time.time() - start_time), 'bytes/sec\n')
for x in range(size):
f.write(chr(random.randint(0, 255)))
f.close()
opts = get_multiple_opt_choices()
print('opts:', ' '.join(opts))
bytes += test_one('input.dat', opts)