import contextlib
import os
import difflib
import json
import math
import shutil
import subprocess
import random
import re
import sys
import time
import traceback
from os.path import abspath
from test import shared
from test import support
assert sys.version_info.major == 3, 'requires Python 3!'
CONSTANT_FEATURE_OPTS = ['--all-features']
INPUT_SIZE_MIN = 1024
INPUT_SIZE_MEAN = 40 * 1024
INPUT_SIZE_MAX = 5 * INPUT_SIZE_MEAN
PRINT_WATS = False
given_seed = None
CLOSED_WORLD_FLAG = '--closed-world'
def in_binaryen(*args):
return os.path.join(shared.options.binaryen_root, *args)
def in_bin(tool):
return os.path.join(shared.options.binaryen_bin, tool)
def random_size():
if random.random() < 0.25:
ret = int(random.expovariate(1.0 / INPUT_SIZE_MEAN))
if ret >= INPUT_SIZE_MIN and ret <= INPUT_SIZE_MAX:
return ret
return random.randint(INPUT_SIZE_MIN, 2 * INPUT_SIZE_MEAN - INPUT_SIZE_MIN)
def make_random_input(input_size, raw_input_data):
with open(raw_input_data, 'wb') as f:
f.write(bytes([random.randint(0, 255) for x in range(input_size)]))
def run(cmd, stderr=None, silent=False):
if not silent:
print(' '.join(cmd))
return subprocess.check_output(cmd, stderr=stderr, text=True)
def run_unchecked(cmd):
print(' '.join(cmd))
return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).communicate()[0]
def randomize_pass_debug():
if random.random() < 0.1:
print('[pass-debug]')
os.environ['BINARYEN_PASS_DEBUG'] = '1'
else:
os.environ['BINARYEN_PASS_DEBUG'] = '0'
del os.environ['BINARYEN_PASS_DEBUG']
print('randomized pass debug:', os.environ.get('BINARYEN_PASS_DEBUG', ''))
@contextlib.contextmanager
def no_pass_debug():
old_env = os.environ.copy()
if os.environ.get('BINARYEN_PASS_DEBUG'):
del os.environ['BINARYEN_PASS_DEBUG']
try:
yield
finally:
os.environ.update(old_env)
def randomize_feature_opts():
global FEATURE_OPTS
FEATURE_OPTS = CONSTANT_FEATURE_OPTS[:]
if random.random() < 0.1:
FEATURE_OPTS += FEATURE_DISABLE_FLAGS
elif random.random() < 0.333:
for possible in FEATURE_DISABLE_FLAGS:
if random.random() < 0.5:
FEATURE_OPTS.append(possible)
if possible in IMPLIED_FEATURE_OPTS:
FEATURE_OPTS.extend(IMPLIED_FEATURE_OPTS[possible])
else:
pass
print('randomized feature opts:', '\n ' + '\n '.join(FEATURE_OPTS))
global CLOSED_WORLD
CLOSED_WORLD = random.random() < 0.5
ALL_FEATURE_OPTS = ['--all-features', '-all', '--mvp-features', '-mvp']
def update_feature_opts(wasm):
global FEATURE_OPTS
EXTRA = [x for x in FEATURE_OPTS if not x.startswith('--enable') and
not x.startswith('--disable') and x not in ALL_FEATURE_OPTS]
FEATURE_OPTS = run([in_bin('wasm-opt'), wasm] + FEATURE_OPTS + ['--print-features']).strip().split('\n')
FEATURE_OPTS = [x for x in FEATURE_OPTS if x]
print(FEATURE_OPTS, EXTRA)
FEATURE_OPTS += EXTRA
def randomize_fuzz_settings():
global GEN_ARGS
GEN_ARGS = []
global FUZZ_OPTS
FUZZ_OPTS = []
global NANS
global OOB
global LEGALIZE
if random.random() < 0.5:
NANS = True
else:
NANS = False
GEN_ARGS += ['--denan']
if random.random() < 0.5:
OOB = True
else:
OOB = False
GEN_ARGS += ['--no-fuzz-oob']
if random.random() < 0.5:
LEGALIZE = True
GEN_ARGS += ['--legalize-and-prune-js-interface']
else:
LEGALIZE = False
if '--disable-gc' not in FEATURE_OPTS:
GEN_ARGS += ['--dce']
FUZZ_OPTS += ['--dce']
print('randomized settings (NaNs, OOB, legalize):', NANS, OOB, LEGALIZE)
def init_important_initial_contents():
fuzz_dir = os.path.join(shared.options.binaryen_root, 'fuzz')
fuzz_cases = shared.get_tests(fuzz_dir, test_suffixes, recursive=True)
FIXED_IMPORTANT_INITIAL_CONTENTS = fuzz_cases
RECENT_DAYS = 30
def auto_select_recent_initial_contents():
from datetime import datetime, timedelta, timezone
head_ts_str = run(['git', 'log', '-1', '--format=%cd', '--date=raw'],
silent=True).split()[0]
head_dt = datetime.utcfromtimestamp(int(head_ts_str))
start_dt = head_dt - timedelta(days=RECENT_DAYS)
start_ts = start_dt.replace(tzinfo=timezone.utc).timestamp()
log = run(['git', 'log', '--name-status', '--format=', '--date=raw', '--no-renames', f'--since={start_ts}'], silent=True).splitlines()
p = re.compile(r'^[AM]\stest' + os.sep + r'(.*\.(wat|wast))$')
matches = [p.match(e) for e in log]
auto_set = set([match.group(1) for match in matches if match])
auto_set = auto_set.difference(set(FIXED_IMPORTANT_INITIAL_CONTENTS))
return sorted(list(auto_set))
def is_git_repo():
try:
ret = run(['git', 'rev-parse', '--is-inside-work-tree'],
silent=True, stderr=subprocess.DEVNULL)
return ret == 'true\n'
except subprocess.CalledProcessError:
return False
if not is_git_repo() and shared.options.auto_initial_contents:
print('Warning: The current directory is not a git repository, ' +
'so not automatically selecting initial contents.')
shared.options.auto_initial_contents = False
print('- Important provided initial contents:')
for test in FIXED_IMPORTANT_INITIAL_CONTENTS:
print(' ' + test)
print()
recent_contents = []
print('- Recently added or modified initial contents ', end='')
if shared.options.auto_initial_contents:
print(f'(automatically selected: within last {RECENT_DAYS} days):')
recent_contents += auto_select_recent_initial_contents()
for test in recent_contents:
print(' ' + test)
print()
initial_contents = FIXED_IMPORTANT_INITIAL_CONTENTS + recent_contents
global IMPORTANT_INITIAL_CONTENTS
IMPORTANT_INITIAL_CONTENTS = [os.path.join(shared.get_test_dir('.'), t) for t in initial_contents]
INITIAL_CONTENTS_IGNORE = [
'relaxed-simd.wast',
'strings.wast',
'simplify-locals-strings.wast',
'string-lowering-instructions.wast',
'extern-conversions.wast',
'zlib.wasm',
'cubescript.wasm',
'class_with_dwarf_noprint.wasm',
'fib2_dwarf.wasm',
'fib_nonzero-low-pc_dwarf.wasm',
'inlined_to_start_dwarf.wasm',
'fannkuch3_manyopts_dwarf.wasm',
'fib2_emptylocspan_dwarf.wasm',
'fannkuch3_dwarf.wasm',
'multi_unit_abbrev_noprint.wasm',
'multi-memories-atomics64.wast',
'multi-memories-basics.wast',
'multi-memories-simd.wast',
'multi-memories-atomics64.wasm',
'multi-memories-basics.wasm',
'multi-memories-simd.wasm',
'multi-memories_size.wast',
'optimize-instructions-gc-extern.wast',
'gufa-extern.wast',
'multi-memory-lowering-import.wast',
'multi-memory-lowering-import-error.wast',
'typed_continuations.wast',
'typed_continuations_resume.wast',
'typed_continuations_contnew.wast',
'typed_continuations_contbind.wast',
'exception-handling.wast',
'translate-eh-old-to-new.wast',
'rse-eh.wast',
'string-lowering.wast',
]
def pick_initial_contents():
global INITIAL_CONTENTS
INITIAL_CONTENTS = None
if random.random() < 0.5:
return
if IMPORTANT_INITIAL_CONTENTS and random.random() < 0.5:
test_name = random.choice(IMPORTANT_INITIAL_CONTENTS)
else:
test_name = random.choice(all_tests)
print('initial contents:', test_name)
if shared.options.auto_initial_contents:
if not os.path.exists(test_name):
return
if os.path.basename(test_name) in INITIAL_CONTENTS_IGNORE:
return
assert os.path.exists(test_name)
if '.fail.' in test_name:
print('initial contents is just a .fail test')
return
if os.path.basename(test_name) in [
'limit-segments_disable-bulk-memory.wast',
'simd.wast',
'names.wast',
'too_much_for_liveness.wasm'
]:
print('initial contents is disallowed')
return
if test_name.endswith('.wast'):
split_parts = support.split_wast(test_name)
if len(split_parts) > 1:
index = random.randint(0, len(split_parts) - 1)
chosen = split_parts[index]
module, asserts = chosen
if not module:
print('initial contents has no module')
return
test_name = abspath('initial.wat')
with open(test_name, 'w') as f:
f.write(module)
print(' picked submodule %d from multi-module wast' % index)
global FEATURE_OPTS
FEATURE_OPTS += [
'--disable-memory64',
'--disable-multivalue',
]
if test_name.endswith('.wasm'):
temp_test_name = 'initial.wasm'
try:
run([in_bin('wasm-opt'), test_name, '-all', '--strip-target-features',
'-o', temp_test_name])
except Exception:
print('(initial contents are not valid wasm, ignoring)')
return
test_name = temp_test_name
args = FEATURE_OPTS
if CLOSED_WORLD:
args.append(CLOSED_WORLD_FLAG)
try:
run([in_bin('wasm-opt'), test_name] + args,
stderr=subprocess.PIPE,
silent=True)
except Exception:
print('(initial contents not valid for features, ignoring)')
return
INITIAL_CONTENTS = test_name
IGNORE = '[binaryen-fuzzer-ignore]'
TRAP_PREFIX = '[trap '
HOST_LIMIT_PREFIX = '[host limit '
FUZZ_EXEC_CALL_PREFIX = '[fuzz-exec] calling'
STACK_LIMIT = '[trap stack limit]'
V8_UNINITIALIZED_NONDEF_LOCAL = 'uninitialized non-defaultable local'
EXCEPTION_PREFIX = 'exception thrown: '
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()
def compare(x, y, context, verbose=True):
if x != y and x != IGNORE and y != IGNORE:
message = ''.join([a + '\n' for a in difflib.unified_diff(x.splitlines(), y.splitlines(), fromfile='expected', tofile='actual')])
if verbose:
raise Exception(context + " comparison error, expected to have '%s' == '%s', diff:\n\n%s" % (
x, y,
message
))
else:
raise Exception(context + "\nDiff:\n\n%s" % (message))
def unsign(x, bits):
return x & ((1 << bits) - 1)
def numbers_are_close_enough(x, y):
if 'nan' in x.lower() and 'nan' in y.lower():
return True
if ' ' in x or ' ' in y:
def to_64_bit(a):
if ' ' not in a:
return unsign(int(a), bits=64)
low, high = a.split(' ')
return unsign(int(low), 32) + (1 << 32) * unsign(int(high), 32)
return to_64_bit(x) == to_64_bit(y)
try:
return float(x) == float(y)
except Exception:
pass
try:
ex = eval(x)
ey = eval(y)
return ex == ey or float(ex) == float(ey)
except Exception as e:
print('failed to check if numbers are close enough:', e)
return False
FUZZ_EXEC_NOTE_RESULT = '[fuzz-exec] note result'
def compare_between_vms(x, y, context):
x_lines = x.splitlines()
y_lines = y.splitlines()
if len(x_lines) != len(y_lines):
return compare(x, y, context + ' (note: different number of lines between vms)')
num_lines = len(x_lines)
for i in range(num_lines):
x_line = x_lines[i]
y_line = y_lines[i]
if x_line != y_line:
LEI_LOGGING = '[LoggingExternalInterface logging'
if x_line.startswith(LEI_LOGGING) and y_line.startswith(LEI_LOGGING):
x_val = x_line[len(LEI_LOGGING) + 1:-1]
y_val = y_line[len(LEI_LOGGING) + 1:-1]
if numbers_are_close_enough(x_val, y_val):
continue
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):
continue
MARGIN = 3
start = max(i - MARGIN, 0)
end = min(i + MARGIN, num_lines)
return compare('\n'.join(x_lines[start:end]), '\n'.join(y_lines[start:end]), context)
def fix_output(out):
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)
out = out.replace(TRAP_PREFIX, EXCEPTION_PREFIX + TRAP_PREFIX)
out = re.sub(r'funcref\([\d\w$+-_:]+\)', 'funcref()', out)
out = re.sub(r'i31ref\((-?\d+)\)', r'\1', out)
out = re.sub(r' tag\$\d+', ' tag', out)
lines = out.splitlines()
for i in range(len(lines)):
line = lines[i]
if 'Warning: unknown flag' in line or 'Try --help for options' in line:
print(line)
lines[i] = None
elif EXCEPTION_PREFIX in line:
lines[i] = ' *exception*'
return '\n'.join([line for line in lines if line is not None])
def fix_spec_output(out):
out = fix_output(out)
out = '\n'.join(map(lambda x: x if 'runtime trap' not in x else x[x.find('runtime trap'):], out.splitlines()))
out = '\n'.join(map(lambda x: x if 'f32' not in x and 'f64' not in x else '', out.splitlines()))
return out
ignored_vm_runs = 0
ignored_vm_run_reasons = dict()
def note_ignored_vm_run(reason, extra_text=''):
global ignored_vm_runs
print(f'(ignore VM run: {reason}{extra_text})')
ignored_vm_runs += 1
ignored_vm_run_reasons.setdefault(reason, 0)
ignored_vm_run_reasons[reason] += 1
def run_vm(cmd):
def filter_known_issues(output):
known_issues = [
'local count too large',
'requested new array is too large',
'found br_if of type',
'out of memory',
'Maximum call stack size exceeded',
HOST_LIMIT_PREFIX,
V8_UNINITIALIZED_NONDEF_LOCAL,
]
for issue in known_issues:
if issue in output:
note_ignored_vm_run(issue)
return IGNORE
return output
try:
return filter_known_issues(run(cmd))
except subprocess.CalledProcessError:
if filter_known_issues(run_unchecked(cmd)) == IGNORE:
return IGNORE
raise
MAX_INTERPRETER_ENV_VAR = 'BINARYEN_MAX_INTERPRETER_DEPTH'
MAX_INTERPRETER_DEPTH = 1000
def run_bynterp(wasm, args):
os.environ[MAX_INTERPRETER_ENV_VAR] = str(MAX_INTERPRETER_DEPTH)
try:
return run_vm([in_bin('wasm-opt'), wasm] + FEATURE_OPTS + args)
finally:
del os.environ['BINARYEN_MAX_INTERPRETER_DEPTH']
V8_LIFTOFF_ARGS = ['--liftoff', '--no-wasm-tier-up']
def run_d8_js(js, args=[], liftoff=True):
cmd = [shared.V8] + shared.V8_OPTS
if liftoff:
cmd += V8_LIFTOFF_ARGS
cmd += [js]
if args:
cmd += ['--'] + args
return run_vm(cmd)
FUZZ_SHELL_JS = in_binaryen('scripts', 'fuzz_shell.js')
def run_d8_wasm(wasm, liftoff=True):
return run_d8_js(FUZZ_SHELL_JS, [wasm], liftoff=liftoff)
def all_disallowed(features):
return not any(('--enable-' + x) in FEATURE_OPTS for x in features)
class TestCaseHandler:
frequency = 1
def __init__(self):
self.num_runs = 0
def handle_pair(self, input, before_wasm, after_wasm, opts):
self.handle(before_wasm)
self.handle(after_wasm)
def can_run_on_feature_opts(self, feature_opts):
return True
def increment_runs(self):
self.num_runs += 1
def count_runs(self):
return self.num_runs
class FuzzExec(TestCaseHandler):
frequency = 1
def handle_pair(self, input, before_wasm, after_wasm, opts):
run([in_bin('wasm-opt'), before_wasm] + opts + ['--fuzz-exec'])
class CompareVMs(TestCaseHandler):
frequency = 0.66
def __init__(self):
super(CompareVMs, self).__init__()
class BinaryenInterpreter:
name = 'binaryen interpreter'
def run(self, wasm):
output = run_bynterp(wasm, ['--fuzz-exec-before'])
if output != IGNORE:
calls = output.count(FUZZ_EXEC_CALL_PREFIX)
errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX)
if errors > calls / 2:
note_ignored_vm_run('too many errors vs calls', extra_text=f' ({calls} calls, {errors} errors)')
return output
def can_run(self, wasm):
return True
def can_compare_to_self(self):
return True
def can_compare_to_others(self):
return True
class D8:
name = 'd8'
def run(self, wasm, extra_d8_flags=[]):
return run_vm([shared.V8, FUZZ_SHELL_JS] + shared.V8_OPTS + extra_d8_flags + ['--', wasm])
def can_run(self, wasm):
return True
def can_compare_to_self(self):
return not NANS
def can_compare_to_others(self):
return LEGALIZE and not NANS
class D8Liftoff(D8):
name = 'd8_liftoff'
def run(self, wasm):
return super(D8Liftoff, self).run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS)
class D8Turboshaft(D8):
name = 'd8_turboshaft'
def run(self, wasm):
return super(D8Turboshaft, self).run(wasm, extra_d8_flags=['--no-liftoff', '--turboshaft-wasm', '--turboshaft-wasm-instruction-selection-staged'])
class D8TurboFan(D8):
name = 'd8_turbofan'
def run(self, wasm):
return super(D8TurboFan, self).run(wasm, extra_d8_flags=['--no-liftoff'])
class Wasm2C:
name = 'wasm2c'
def __init__(self):
try:
wabt_bin = shared.which('wasm2c')
wabt_root = os.path.dirname(os.path.dirname(wabt_bin))
self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c')
if not os.path.isdir(self.wasm2c_dir):
print('wabt found, but not wasm2c support dir')
self.wasm2c_dir = None
except Exception as e:
print('warning: no wabt found:', e)
self.wasm2c_dir = None
def can_run(self, wasm):
if self.wasm2c_dir is None:
return False
if LEGALIZE:
return False
if random.random() < 0.5:
return False
return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc'])
def run(self, wasm):
run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS)
run(['wasm2c', wasm, '-o', 'wasm.c'])
compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror']
run(compile_cmd)
return run_vm(['./a.out'])
def can_compare_to_self(self):
return not NANS
def can_compare_to_others(self):
return not OOB and not NANS
class Wasm2C2Wasm(Wasm2C):
name = 'wasm2c2wasm'
def __init__(self):
super(Wasm2C2Wasm, self).__init__()
self.has_emcc = shared.which('emcc') is not None
def run(self, wasm):
run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS)
run(['wasm2c', wasm, '-o', 'wasm.c'])
compile_cmd = ['emcc', 'main.c', 'wasm.c',
os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'),
'-I' + self.wasm2c_dir,
'-lm',
'-s', 'ENVIRONMENT=shell',
'-s', 'ALLOW_MEMORY_GROWTH']
compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0']
if random.random() < 0.5:
compile_cmd += ['-O' + str(random.randint(1, 3))]
elif random.random() < 0.5:
if random.random() < 0.5:
compile_cmd += ['-Os']
else:
compile_cmd += ['-Oz']
with no_pass_debug():
run(compile_cmd)
return run_d8_js(abspath('a.out.js'))
def can_run(self, wasm):
if random.random() < 0.8:
return False
return super(Wasm2C2Wasm, self).can_run(wasm) and self.has_emcc and \
os.path.getsize(wasm) <= INPUT_SIZE_MEAN
def can_compare_to_others(self):
return not NANS
self.bynterpreter = BinaryenInterpreter()
self.vms = [self.bynterpreter,
D8(),
D8Liftoff(),
D8Turboshaft(),
D8TurboFan(),
]
def handle_pair(self, input, before_wasm, after_wasm, opts):
global ignored_vm_runs
ignored_before = ignored_vm_runs
before = self.run_vms(before_wasm)
if before[self.bynterpreter] == IGNORE:
assert(ignored_vm_runs > ignored_before)
return
after = self.run_vms(after_wasm)
self.compare_before_and_after(before, after)
def run_vms(self, wasm):
vm_results = {}
for vm in self.vms:
if vm.can_run(wasm):
print(f'[CompareVMs] running {vm.name}')
vm_results[vm] = fix_output(vm.run(wasm))
first_vm = None
for vm in vm_results.keys():
if vm.can_compare_to_others():
if first_vm is None:
first_vm = vm
else:
compare_between_vms(vm_results[first_vm], vm_results[vm], 'CompareVMs between VMs: ' + first_vm.name + ' and ' + vm.name)
return vm_results
def compare_before_and_after(self, before, after):
for vm in before.keys():
if vm in after and vm.can_compare_to_self():
compare(before[vm], after[vm], 'CompareVMs between before and after: ' + vm.name)
class CheckDeterminism(TestCaseHandler):
frequency = 0.2
def handle_pair(self, input, before_wasm, after_wasm, opts):
run([in_bin('wasm-opt'), before_wasm, '-o', abspath('b1.wasm')] + opts)
run([in_bin('wasm-opt'), before_wasm, '-o', abspath('b2.wasm')] + opts)
b1 = open('b1.wasm', 'rb').read()
b2 = open('b2.wasm', 'rb').read()
if (b1 != b2):
run([in_bin('wasm-dis'), abspath('b1.wasm'), '-o', abspath('b1.wat')] + FEATURE_OPTS)
run([in_bin('wasm-dis'), abspath('b2.wasm'), '-o', abspath('b2.wat')] + FEATURE_OPTS)
t1 = open(abspath('b1.wat'), 'r').read()
t2 = open(abspath('b2.wat'), 'r').read()
compare(t1, t2, 'Output must be deterministic.', verbose=False)
class Wasm2JS(TestCaseHandler):
frequency = 0.1
def handle_pair(self, input, before_wasm, after_wasm, opts):
before_wasm_temp = before_wasm + '.temp.wasm'
after_wasm_temp = after_wasm + '.temp.wasm'
run([in_bin('wasm-opt'), before_wasm, '--legalize-and-prune-js-interface', '-o', before_wasm_temp] + FEATURE_OPTS)
compare_before_to_after = random.random() < 0.5
compare_to_interpreter = compare_before_to_after and random.random() < 0.5
if compare_before_to_after:
simplification_passes = ['--stub-unsupported-js']
if compare_to_interpreter:
simplification_passes += ['--dealign', '--alignment-lowering']
run([in_bin('wasm-opt'), before_wasm_temp, '-o', before_wasm_temp] + simplification_passes + FEATURE_OPTS)
run([in_bin('wasm-opt'), before_wasm_temp, '-o', after_wasm_temp] + opts + FEATURE_OPTS)
before = self.run(before_wasm_temp)
after = self.run(after_wasm_temp)
if NANS:
return
interpreter = run_bynterp(before_wasm_temp, ['--fuzz-exec-before'])
if TRAP_PREFIX in interpreter:
trap_index = interpreter.index(TRAP_PREFIX)
call_start = interpreter.rindex(FUZZ_EXEC_CALL_PREFIX, 0, trap_index)
call_end = interpreter.index('\n', call_start)
call_line = interpreter[call_start:call_end]
before = before[:before.index(call_line)]
after = after[:after.index(call_line)]
interpreter = interpreter[:interpreter.index(call_line)]
def fix_output_for_js(x):
x = fix_output(x)
def is_basically_zero(x):
return x >= 0 and x <= 2.22507385850720088902e-308
def fix_number(x):
x = x.group(1)
try:
x = float(x)
if is_basically_zero(x):
x = 0
except ValueError:
pass
return ' => ' + str(x)
return re.sub(r' => (-?[\d+-.e\-+]+)', fix_number, x)
before = fix_output_for_js(before)
after = fix_output_for_js(after)
if compare_before_to_after:
compare_between_vms(before, after, 'Wasm2JS (before/after)')
if compare_to_interpreter:
interpreter = fix_output_for_js(interpreter)
compare_between_vms(before, interpreter, 'Wasm2JS (vs interpreter)')
def run(self, wasm):
with open(FUZZ_SHELL_JS) as f:
wrapper = f.read()
cmd = [in_bin('wasm2js'), wasm, '--emscripten']
if not NANS and not OOB and random.random() < 0.5:
cmd += ['-O', '--deterministic']
main = run(cmd + FEATURE_OPTS)
with open(os.path.join(shared.options.binaryen_root, 'scripts', 'wasm2js.js')) as f:
glue = f.read()
js_file = wasm + '.js'
with open(js_file, 'w') as f:
f.write(glue)
f.write(main)
f.write(wrapper)
return run_vm([shared.NODEJS, js_file, abspath('a.wasm')])
def can_run_on_feature_opts(self, feature_opts):
if INITIAL_CONTENTS:
return False
return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'multimemory'])
def filter_exports(wasm, output, keep):
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))
run([in_bin('wasm-metadce'), wasm, '-o', output, '--graph-file', 'graph.json', '-all'])
class TrapsNeverHappen(TestCaseHandler):
frequency = 0.25
def handle_pair(self, input, before_wasm, after_wasm, opts):
before = run_bynterp(before_wasm, ['--fuzz-exec-before'])
if before == IGNORE:
return
if TRAP_PREFIX in before:
trap_index = before.index(TRAP_PREFIX)
call_start = before.rfind(FUZZ_EXEC_CALL_PREFIX, 0, trap_index)
if call_start < 0:
return
call_end = before.index(os.linesep, call_start) + 1
call_line = before[call_start:call_end]
trapping_export = get_export_from_call_line(call_line)
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)
filtered = before_wasm + '.filtered.wasm'
filter_exports(before_wasm, filtered, safe_exports)
before_wasm = filtered
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'])
def ignore_references(out):
ret = []
for line in out.splitlines():
if FUZZ_EXEC_NOTE_RESULT in line:
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')
class CtorEval(TestCaseHandler):
frequency = 0.2
def handle(self, wasm):
wasm_exec = run_bynterp(wasm, ['--fuzz-exec-before'])
wat = run([in_bin('wasm-dis'), wasm] + FEATURE_OPTS)
p = re.compile(r'^ [(]export "(.*[^\\]?)" [(]func')
exports = []
for line in wat.splitlines():
m = p.match(line)
if m:
export = m[1]
exports.append(export)
if not exports:
return
ctors = ','.join(exports)
evalled_wasm = wasm + '.evalled.wasm'
output = run([in_bin('wasm-ctor-eval'), wasm, '-o', evalled_wasm, '--ctors=' + ctors, '--kept-exports=' + ctors, '--ignore-external-input'] + FEATURE_OPTS)
if '...stopping since could not flatten memory' in output or \
'...stopping since could not create module instance' in output:
return
if '...success' not in output and \
'...partial evalling success' not in output:
return
evalled_wasm_exec = run_bynterp(evalled_wasm, ['--fuzz-exec-before'])
compare_between_vms(fix_output(wasm_exec), fix_output(evalled_wasm_exec), 'CtorEval')
class Merge(TestCaseHandler):
frequency = 0.15
def handle(self, wasm):
wasm_size = os.stat(wasm).st_size
second_size = min(wasm_size, random_size())
second_input = abspath('second_input.dat')
make_random_input(second_size, second_input)
second_wasm = abspath('second.wasm')
run([in_bin('wasm-opt'), second_input, '-ttf', '-o', second_wasm] + GEN_ARGS + FEATURE_OPTS)
if random.random() < 0.5:
opts = get_random_opts()
run([in_bin('wasm-opt'), second_wasm, '-o', second_wasm, '-all'] + FEATURE_OPTS + opts)
merged = abspath('merged.wasm')
run([in_bin('wasm-merge'), wasm, 'first',
abspath('second.wasm'), 'second', '-o', merged,
'--skip-export-conflicts'] + FEATURE_OPTS + ['-all'])
if random.random() < 0.5:
opts = get_random_opts()
run([in_bin('wasm-opt'), merged, '-o', merged, '-all'] + FEATURE_OPTS + opts)
output = run_bynterp(wasm, ['--fuzz-exec-before', '-all'])
output = fix_output(output)
merged_output = run_bynterp(merged, ['--fuzz-exec-before', '-all'])
merged_output = fix_output(merged_output)
merged_output = merged_output[:len(output)]
compare_between_vms(output, merged_output, 'Merge')
class RoundtripText(TestCaseHandler):
frequency = 0.05
def handle(self, wasm):
run([in_bin('wasm-opt'), wasm, '--name-types', '-S', '-o', abspath('a.wast')] + FEATURE_OPTS)
run([in_bin('wasm-opt'), abspath('a.wast')] + FEATURE_OPTS)
testcase_handlers = [
FuzzExec(),
CompareVMs(),
CheckDeterminism(),
Wasm2JS(),
TrapsNeverHappen(),
CtorEval(),
Merge(),
]
test_suffixes = ['*.wasm', '*.wast', '*.wat']
core_tests = shared.get_tests(shared.get_test_dir('.'), test_suffixes)
passes_tests = shared.get_tests(shared.get_test_dir('passes'), test_suffixes)
spec_tests = shared.get_tests(shared.get_test_dir('spec'), test_suffixes)
wasm2js_tests = shared.get_tests(shared.get_test_dir('wasm2js'), test_suffixes)
lld_tests = shared.get_tests(shared.get_test_dir('lld'), test_suffixes)
unit_tests = shared.get_tests(shared.get_test_dir(os.path.join('unit', 'input')), test_suffixes)
lit_tests = shared.get_tests(shared.get_test_dir('lit'), test_suffixes, recursive=True)
all_tests = core_tests + passes_tests + spec_tests + wasm2js_tests + lld_tests + unit_tests + lit_tests
def test_one(random_input, given_wasm):
randomize_pass_debug()
randomize_feature_opts()
randomize_fuzz_settings()
pick_initial_contents()
opts = get_random_opts()
print('randomized opts:', '\n ' + '\n '.join(opts))
print()
if given_wasm:
try:
run([in_bin('wasm-opt'), given_wasm, '-o', abspath('a.wasm')] + GEN_ARGS + FEATURE_OPTS)
except Exception as e:
print("Internal error in fuzzer! Could not run given wasm")
raise e
else:
generate_command = [in_bin('wasm-opt'), random_input, '-ttf', '-o', abspath('a.wasm')] + GEN_ARGS + FEATURE_OPTS
if INITIAL_CONTENTS:
generate_command += ['--initial-fuzz=' + INITIAL_CONTENTS]
if PRINT_WATS:
printed = run(generate_command + ['--print'])
with open('a.printed.wast', 'w') as f:
f.write(printed)
else:
run(generate_command)
wasm_size = os.stat('a.wasm').st_size
bytes = wasm_size
print('pre wasm size:', wasm_size)
update_feature_opts('a.wasm')
generate_command = [in_bin('wasm-opt'), abspath('a.wasm'), '-o', abspath('b.wasm')] + opts + FUZZ_OPTS + FEATURE_OPTS
if PRINT_WATS:
printed = run(generate_command + ['--print'])
with open('b.printed.wast', 'w') as f:
f.write(printed)
else:
run(generate_command)
wasm_size = os.stat('b.wasm').st_size
bytes += wasm_size
print('post wasm size:', wasm_size)
relevant_handlers = [handler for handler in testcase_handlers if not hasattr(handler, 'get_commands') and handler.can_run_on_feature_opts(FEATURE_OPTS)]
if len(relevant_handlers) == 0:
return 0
filtered_handlers = [handler for handler in relevant_handlers if random.random() < handler.frequency]
if len(filtered_handlers) == 0:
filtered_handlers = [random.choice(relevant_handlers)]
NUM_PAIR_HANDLERS = 3
used_handlers = set()
for i in range(NUM_PAIR_HANDLERS):
testcase_handler = random.choice(filtered_handlers)
if testcase_handler in used_handlers:
continue
used_handlers.add(testcase_handler)
assert testcase_handler.can_run_on_feature_opts(FEATURE_OPTS)
print('running testcase handler:', testcase_handler.__class__.__name__)
testcase_handler.increment_runs()
testcase_handler.handle_pair(input=random_input, before_wasm=abspath('a.wasm'), after_wasm=abspath('b.wasm'), opts=opts + FEATURE_OPTS)
print('')
return bytes
def write_commands(commands, filename):
with open(filename, 'w') as f:
f.write('set -e\n')
for command in commands:
f.write('echo "%s"\n' % command)
pre = 'BINARYEN_PASS_DEBUG=%s ' % (os.environ.get('BINARYEN_PASS_DEBUG') or '0')
f.write(pre + command + ' &> /dev/null\n')
f.write('echo "ok"\n')
opt_choices = [
(),
('-O1',), ('-O2',), ('-O3',), ('-O4',), ('-Os',), ('-Oz',),
("--abstract-type-refining",),
("--cfp",),
("--coalesce-locals",),
("--code-pushing",),
("--code-folding",),
("--const-hoisting",),
("--dae",),
("--dae-optimizing",),
("--dce",),
("--directize",),
("--discard-global-effects",),
("--flatten", "--dfo",),
("--duplicate-function-elimination",),
("--flatten",),
("--inlining",),
("--inlining-optimizing",),
("--flatten", "--simplify-locals-notee-nostructure", "--local-cse",),
("--generate-global-effects",),
("--global-refining",),
("--gsi",),
("--gto",),
("--gufa",),
("--gufa-cast-all",),
("--gufa-optimizing",),
("--local-cse",),
("--heap2local",),
("--remove-unused-names", "--heap2local",),
("--generate-stack-ir",),
("--licm",),
("--local-subtyping",),
("--memory-packing",),
("--merge-blocks",),
('--merge-locals',),
('--monomorphize',),
('--monomorphize-always',),
('--once-reduction',),
("--optimize-casts",),
("--optimize-instructions",),
("--optimize-stack-ir",),
("--generate-stack-ir", "--optimize-stack-ir",),
("--generate-stack-ir", "--optimize-stack-ir", "--roundtrip"),
("--pick-load-signs",),
("--precompute",),
("--precompute-propagate",),
("--print",),
("--remove-unused-brs",),
("--remove-unused-nonfunction-module-elements",),
("--remove-unused-module-elements",),
("--remove-unused-names",),
("--remove-unused-types",),
("--reorder-functions",),
("--reorder-locals",),
("--flatten", "--rereloop",),
("--roundtrip",),
("--rse",),
("--signature-pruning",),
("--signature-refining",),
("--simplify-globals",),
("--simplify-globals-optimizing",),
("--simplify-locals",),
("--simplify-locals-nonesting",),
("--simplify-locals-nostructure",),
("--simplify-locals-notee",),
("--simplify-locals-notee-nostructure",),
("--ssa",),
("--tuple-optimization",),
("--type-finalizing",),
("--type-refining",),
("--type-merging",),
("--type-ssa",),
("--type-unfinalizing",),
("--unsubtyping",),
("--vacuum",),
]
requires_closed_world = {("--type-refining",),
("--signature-pruning",),
("--signature-refining",),
("--gto",),
("--remove-unused-types",),
("--abstract-type-refining",),
("--cfp",),
("--gsi",),
("--type-ssa",),
("--type-merging",)}
def get_random_opts():
flag_groups = []
has_flatten = False
if CLOSED_WORLD:
usable_opt_choices = opt_choices
else:
usable_opt_choices = [choice
for choice in opt_choices
if choice not in requires_closed_world]
while 1:
choice = random.choice(usable_opt_choices)
if '--flatten' in choice or '-O4' in choice:
if has_flatten:
print('avoiding multiple --flatten in a single command, due to exponential overhead')
continue
if '--enable-multivalue' in FEATURE_OPTS and '--enable-reference-types' in FEATURE_OPTS:
print('avoiding --flatten due to multivalue + reference types not supporting it (spilling of non-nullable tuples)')
print('TODO: Resolving https://github.com/WebAssembly/binaryen/issues/4824 may fix this')
continue
if '--gc' not in FEATURE_OPTS:
print('avoiding --flatten due to GC not supporting it (spilling of non-nullable locals)')
continue
if INITIAL_CONTENTS and os.path.getsize(INITIAL_CONTENTS) > 2000:
print('avoiding --flatten due using a large amount of initial contents, which may blow up')
continue
else:
has_flatten = True
if ('--rereloop' in choice or '--dfo' in choice) and \
'--enable-exception-handling' in FEATURE_OPTS:
print('avoiding --rereloop or --dfo due to exception-handling not supporting it')
continue
flag_groups.append(choice)
if len(flag_groups) > 20 or random.random() < 0.3:
break
if random.random() < 0.5:
pos = random.randint(0, len(flag_groups))
flag_groups = flag_groups[:pos] + [('--roundtrip',)] + flag_groups[pos:]
ret = [flag for group in flag_groups for flag in group]
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))]
if random.random() < 0.05:
ret += ['--converge']
if random.random() < 0.5:
ret += ['-fimfs=99999999']
if random.random() < 0.5:
ret += ['-pii=4']
if CLOSED_WORLD:
ret += [CLOSED_WORLD_FLAG]
assert ret.count('--flatten') <= 1
return ret
FEATURE_DISABLE_FLAGS = run([in_bin('wasm-opt'), '--print-features', in_binaryen('test', 'hello_world.wat')] + CONSTANT_FEATURE_OPTS).replace('--enable', '--disable').strip().split('\n')
print('FEATURE_DISABLE_FLAGS:', FEATURE_DISABLE_FLAGS)
IMPLIED_FEATURE_OPTS = {
'--disable-reference-types': ['--disable-gc'],
}
print('''
<<< fuzz_opt.py >>>
''')
if not shared.V8:
print('The v8 shell, d8, must be in the path')
sys.exit(1)
if __name__ == '__main__':
given_wasm = None
if len(shared.requested) >= 1:
given_seed = int(shared.requested[0])
print('checking a single given seed', given_seed)
if len(shared.requested) >= 2:
given_wasm = shared.requested[1]
print('using given wasm file', given_wasm)
else:
given_seed = None
print('checking infinite random inputs')
init_important_initial_contents()
seed = time.time() * os.getpid()
raw_input_data = abspath('input.dat')
counter = 0
total_wasm_size = 0
total_input_size = 0
total_input_size_squares = 0
start_time = time.time()
while True:
counter += 1
if given_seed is not None:
seed = given_seed
given_seed_passed = True
else:
seed = random.randint(0, 1 << 64)
random.seed(seed)
input_size = random_size()
total_input_size += input_size
total_input_size_squares += input_size ** 2
print('')
mean = float(total_input_size) / counter
mean_of_squares = float(total_input_size_squares) / counter
stddev = math.sqrt(mean_of_squares - (mean ** 2))
elapsed = max(0.000001, time.time() - start_time)
print('ITERATION:', counter, 'seed:', seed, 'size:', input_size,
'(mean:', str(mean) + ', stddev:', str(stddev) + ')',
'speed:', counter / elapsed, 'iters/sec, ',
total_wasm_size / counter, 'wasm_bytes/iter')
if ignored_vm_runs:
print(f'(ignored {ignored_vm_runs} iters, for reasons {ignored_vm_run_reasons})')
print()
make_random_input(input_size, raw_input_data)
assert os.path.getsize(raw_input_data) == input_size
if os.path.exists('a.wasm'):
os.remove('a.wasm')
try:
total_wasm_size += test_one(raw_input_data, given_wasm)
except KeyboardInterrupt:
print('(stopping by user request)')
break
except Exception as e:
ex_type, ex, tb = sys.exc_info()
print('!')
print('-----------------------------------------')
print('Exception:')
traceback.print_tb(tb)
print('-----------------------------------------')
print('!')
for arg in e.args:
print(arg)
if given_seed is not None:
given_seed_passed = False
if not given_wasm:
if not os.path.exists('a.wasm'):
print('''\
================================================================================
You found a bug in the fuzzer itself! It failed to generate a valid wasm file
from the random input. Please report it with
seed: %(seed)d
and the exact version of Binaryen you found it on, plus the exact Python
version (hopefully deterministic random numbers will be identical).
You can run that testcase again with "fuzz_opt.py %(seed)d"
(We can't automatically reduce this testcase since we can only run the reducer
on valid wasm files.)
================================================================================
''' % {'seed': seed})
break
original_wasm = abspath('original.wasm')
shutil.copyfile('a.wasm', original_wasm)
auto_init = ''
if not shared.options.auto_initial_contents:
auto_init = '--no-auto-initial-contents'
with open('reduce.sh', 'w') as reduce_sh:
reduce_sh.write('''\
# check the input is even a valid wasm file
echo "The following value should be 0:"
%(wasm_opt)s %(features)s %(temp_wasm)s
echo " " $?
# run the command
echo "The following value should be 1:"
./scripts/fuzz_opt.py %(auto_init)s --binaryen-bin %(bin)s %(seed)d %(temp_wasm)s > o 2> e
echo " " $?
#
# You may want to print out part of "o" or "e", if the output matters and not
# just the return code. For example,
#
# cat o | tail -n 10
#
# would print out the last few lines of stdout, which might be useful if that
# mentions the specific error you want. Make sure that includes the right
# details (sometimes stderr matters too), and preferably no more (less details
# allow more reduction, but raise the risk of it reducing to something you don't
# quite want).
#
# To do a "dry run" of what the reducer will do, copy the original file to the
# test file that this script will run on,
#
# cp %(original_wasm)s %(temp_wasm)s
#
# and then run
#
# bash %(reduce_sh)s
#
# You may also need to add --timeout 5 or such if the testcase is a slow one.
#
''' % {'wasm_opt': in_bin('wasm-opt'),
'bin': shared.options.binaryen_bin,
'seed': seed,
'auto_init': auto_init,
'original_wasm': original_wasm,
'temp_wasm': abspath('t.wasm'),
'features': ' '.join(FEATURE_OPTS),
'reduce_sh': abspath('reduce.sh')})
print('''\
================================================================================
You found a bug! Please report it with
seed: %(seed)d
and the exact version of Binaryen you found it on, plus the exact Python
version (hopefully deterministic random numbers will be identical).
You can run that testcase again with "fuzz_opt.py %(seed)d"
The initial wasm file used here is saved as %(original_wasm)s
You can reduce the testcase by running this now:
||||
vvvv
%(wasm_reduce)s %(features)s %(original_wasm)s '--command=bash %(reduce_sh)s' -t %(temp_wasm)s -w %(working_wasm)s
^^^^
||||
Make sure to verify by eye that the output says something like this:
The following value should be 0:
0
The following value should be 1:
1
(If it does not, then one possible issue is that the fuzzer fails to write a
valid binary. If so, you can print the output of the fuzzer's first command
(using -ttf / --translate-to-fuzz) in text form and run the reduction from that,
passing --text to the reducer.)
You can also read "%(reduce_sh)s" which has been filled out for you and includes
docs and suggestions.
After reduction, the reduced file will be in %(working_wasm)s
================================================================================
''' % {'seed': seed,
'original_wasm': original_wasm,
'temp_wasm': abspath('t.wasm'),
'working_wasm': abspath('w.wasm'),
'wasm_reduce': in_bin('wasm-reduce'),
'reduce_sh': abspath('reduce.sh'),
'features': ' '.join(FEATURE_OPTS)})
break
if given_seed is not None:
break
print('\nInvocations so far:')
for testcase_handler in testcase_handlers:
print(' ', testcase_handler.__class__.__name__ + ':', testcase_handler.count_runs())
if given_seed is not None:
if given_seed_passed:
print('(finished running seed %d without error)' % given_seed)
sys.exit(0)
else:
print('(finished running seed %d, see error above)' % given_seed)
sys.exit(1)