#!/usr/bin/env python3 # # Copyright 2016 WebAssembly Community Group participants # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ''' This fuzzes passes, by starting with a working program, then running random passes on the wast, and seeing if they break something Usage: Provide a base filename for a runnable program, e.g. a.out.js. Then we will modify a.out.wast. Note that the program must be built to run using that wast (BINARYEN_METHOD=interpret-s-expr) Other parameters after the first are used when calling the program. ''' from __future__ import print_function import os import random import shutil import subprocess import sys PASSES = [ "duplicate-function-elimination", "dce", "remove-unused-brs", "remove-unused-names", "optimize-instructions", "precompute", "simplify-locals", "vacuum", "coalesce-locals", "reorder-locals", "merge-blocks", "remove-unused-functions", ] # main base = sys.argv[1] wast = base[:-3] + '.wast' print('>>> base program:', base, ', wast:', wast) args = sys.argv[2:] def run(): if os.path.exists(wast): print('>>> running using a wast of size', os.stat(wast).st_size) cmd = ['mozjs', base] + args try: return subprocess.check_output(cmd, stderr=subprocess.STDOUT) except Exception as e: print(">>> !!! ", e, " !!!") original_wast = None try: # get normal output normal = run() print('>>> normal output:\n', normal) assert normal, 'must be output' # ensure we actually use the wast original_wast = wast + '.original.wast' shutil.move(wast, original_wast) assert run() != normal, 'running without the wast must fail' # ensure a bad pass makes it fail def apply_passes(passes): wasm_opt = os.path.join('bin', 'wasm-opt') subprocess.check_call([wasm_opt, original_wast] + passes + ['-o', wast]) apply_passes(['--remove-imports']) assert run() != normal, 'running after a breaking pass must fail' # loop, looking for failures def simplify(passes): # passes is known to fail, try to simplify down by removing more = True while more: more = False print('>>> trying to reduce:', ' '.join(passes), ' [' + str(len(passes)) + ']') for i in range(len(passes)): smaller = passes[:i] + passes[i + 1:] print('>>>>>> try to reduce to:', ' '.join(smaller), ' [' + str(len(smaller)) + ']') try: apply_passes(smaller) assert run() == normal except Exception: # this failed too, so it's a good reduction passes = smaller print('>>> reduction successful') more = True break print('>>> reduced to:', ' '.join(passes)) tested = set() def pick_passes(): ret = [] while 1: str_ret = str(ret) if random.random() < 0.1 and str_ret not in tested: tested.add(str_ret) return ret ret.append('--' + random.choice(PASSES)) counter = 0 while 1: passes = pick_passes() print('>>> [' + str(counter) + '] testing:', ' '.join(passes)) counter += 1 try: apply_passes(passes) except Exception as e: print(e) simplify(passes) break seen = run() if seen != normal: print('>>> bad output:\n', seen) simplify(passes) break finally: if original_wast: shutil.move(original_wast, wast)