|
| 1 | +# |
| 2 | +# Copyright 2024 WebAssembly Community Group participants |
| 3 | +# |
| 4 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +# you may not use this file except in compliance with the License. |
| 6 | +# You may obtain a copy of the License at |
| 7 | +# |
| 8 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +# |
| 10 | +# Unless required by applicable law or agreed to in writing, software |
| 11 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +# See the License for the specific language governing permissions and |
| 14 | +# limitations under the License. |
| 15 | + |
| 16 | +''' |
| 17 | +ClusterFuzz run.py script: when run by ClusterFuzz, it uses wasm-opt to generate |
| 18 | +a fixed number of testcases. This is a "blackbox fuzzer", see |
| 19 | +
|
| 20 | +https://google.github.io/clusterfuzz/setting-up-fuzzing/blackbox-fuzzing/ |
| 21 | +
|
| 22 | +This file should be bundled up together with the other files it needs, see |
| 23 | +bundle_clusterfuzz.py. |
| 24 | +''' |
| 25 | + |
| 26 | +import os |
| 27 | +import getopt |
| 28 | +import random |
| 29 | +import subprocess |
| 30 | +import sys |
| 31 | + |
| 32 | +# The V8 flags we put in the "fuzzer flags" files, which tell ClusterFuzz how to |
| 33 | +# run V8. By default we apply all staging flags. |
| 34 | +FUZZER_FLAGS_FILE_CONTENTS = '--wasm-staging' |
| 35 | + |
| 36 | +# Maximum size of the random data that we feed into wasm-opt -ttf. This is |
| 37 | +# smaller than fuzz_opt.py's INPUT_SIZE_MAX because that script is tuned for |
| 38 | +# fuzzing large wasm files (to reduce the overhead we have of launching many |
| 39 | +# processes per file), which is less of an issue on ClusterFuzz. |
| 40 | +MAX_RANDOM_SIZE = 15 * 1024 |
| 41 | + |
| 42 | +# The prefix for fuzz files. |
| 43 | +FUZZ_FILENAME_PREFIX = 'fuzz-' |
| 44 | + |
| 45 | +# The prefix for flags files. |
| 46 | +FLAGS_FILENAME_PREFIX = 'flags-' |
| 47 | + |
| 48 | +# The name of the fuzzer (appears after FUZZ_FILENAME_PREFIX / |
| 49 | +# FLAGS_FILENAME_PREFIX). |
| 50 | +FUZZER_NAME_PREFIX = 'binaryen-' |
| 51 | + |
| 52 | +# The root directory of the bundle this will be in, which is the directory of |
| 53 | +# this very file. |
| 54 | +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 55 | + |
| 56 | +# The path to the wasm-opt binary that we run to generate testcases. |
| 57 | +FUZZER_BINARY_PATH = os.path.join(ROOT_DIR, 'bin', 'wasm-opt') |
| 58 | + |
| 59 | +# The path to the fuzz_shell.js script that will execute the wasm in each |
| 60 | +# testcase. |
| 61 | +JS_SHELL_PATH = os.path.join(ROOT_DIR, 'scripts', 'fuzz_shell.js') |
| 62 | + |
| 63 | +# The arguments we provide to wasm-opt to generate wasm files. |
| 64 | +FUZZER_ARGS = [ |
| 65 | + # Generate a wasm from random data. |
| 66 | + '--translate-to-fuzz', |
| 67 | + # Run some random passes, to further shape the random wasm we emit. |
| 68 | + '--fuzz-passes', |
| 69 | + # Enable all features but disable ones not yet ready for fuzzing. This may |
| 70 | + # be a smaller set than fuzz_opt.py, as that enables a few experimental |
| 71 | + # flags, while here we just fuzz with d8's --wasm-staging. |
| 72 | + '-all', |
| 73 | + '--disable-shared-everything', |
| 74 | + '--disable-fp16', |
| 75 | +] |
| 76 | + |
| 77 | + |
| 78 | +# Returns the file name for fuzz or flags files. |
| 79 | +def get_file_name(prefix, index): |
| 80 | + return f'{prefix}{FUZZER_NAME_PREFIX}{index}.js' |
| 81 | + |
| 82 | + |
| 83 | +# Returns the contents of a .js fuzz file, given particular wasm contents that |
| 84 | +# we want to be executed. |
| 85 | +def get_js_file_contents(wasm_contents): |
| 86 | + # Start with the standard JS shell. |
| 87 | + with open(JS_SHELL_PATH) as file: |
| 88 | + js = file.read() |
| 89 | + |
| 90 | + # Prepend the wasm contents, so they are used (rather than the normal |
| 91 | + # mechanism where the wasm file's name is provided in argv). |
| 92 | + wasm_contents = ','.join([str(c) for c in wasm_contents]) |
| 93 | + js = f'var binary = new Uint8Array([{wasm_contents}]);\n\n' + js |
| 94 | + return js |
| 95 | + |
| 96 | + |
| 97 | +def main(argv): |
| 98 | + # Parse the options. See |
| 99 | + # https://google.github.io/clusterfuzz/setting-up-fuzzing/blackbox-fuzzing/#uploading-a-fuzzer |
| 100 | + output_dir = '.' |
| 101 | + num = 100 |
| 102 | + expected_flags = ['input_dir=', 'output_dir=', 'no_of_files='] |
| 103 | + optlist, _ = getopt.getopt(argv[1:], '', expected_flags) |
| 104 | + for option, value in optlist: |
| 105 | + if option == '--output_dir': |
| 106 | + output_dir = value |
| 107 | + elif option == '--no_of_files': |
| 108 | + num = int(value) |
| 109 | + |
| 110 | + for i in range(1, num + 1): |
| 111 | + input_data_file_path = os.path.join(output_dir, f'{i}.input') |
| 112 | + wasm_file_path = os.path.join(output_dir, f'{i}.wasm') |
| 113 | + |
| 114 | + # wasm-opt may fail to run in rare cases (when the fuzzer emits code it |
| 115 | + # detects as invalid). Just try again in such a case. |
| 116 | + for attempt in range(0, 100): |
| 117 | + # Generate random data. |
| 118 | + random_size = random.SystemRandom().randint(1, MAX_RANDOM_SIZE) |
| 119 | + with open(input_data_file_path, 'wb') as file: |
| 120 | + file.write(os.urandom(random_size)) |
| 121 | + |
| 122 | + # Generate wasm from the random data. |
| 123 | + cmd = [FUZZER_BINARY_PATH] + FUZZER_ARGS |
| 124 | + cmd += ['-o', wasm_file_path, input_data_file_path] |
| 125 | + try: |
| 126 | + subprocess.check_call(cmd) |
| 127 | + except subprocess.CalledProcessError: |
| 128 | + # Try again. |
| 129 | + print('(oops, retrying wasm-opt)') |
| 130 | + attempt += 1 |
| 131 | + if attempt == 99: |
| 132 | + # Something is very wrong! |
| 133 | + raise |
| 134 | + continue |
| 135 | + # Success, leave the loop. |
| 136 | + break |
| 137 | + |
| 138 | + # Generate a testcase from the wasm |
| 139 | + with open(wasm_file_path, 'rb') as file: |
| 140 | + wasm_contents = file.read() |
| 141 | + testcase_file_path = os.path.join(output_dir, |
| 142 | + get_file_name(FUZZ_FILENAME_PREFIX, i)) |
| 143 | + js_file_contents = get_js_file_contents(wasm_contents) |
| 144 | + with open(testcase_file_path, 'w') as file: |
| 145 | + file.write(js_file_contents) |
| 146 | + |
| 147 | + # Emit a corresponding flags file. |
| 148 | + flags_file_path = os.path.join(output_dir, |
| 149 | + get_file_name(FLAGS_FILENAME_PREFIX, i)) |
| 150 | + with open(flags_file_path, 'w') as file: |
| 151 | + file.write(FUZZER_FLAGS_FILE_CONTENTS) |
| 152 | + |
| 153 | + print(f'Created testcase: {testcase_file_path}, {len(wasm_contents)} bytes') |
| 154 | + |
| 155 | + # Remove temporary files. |
| 156 | + os.remove(input_data_file_path) |
| 157 | + os.remove(wasm_file_path) |
| 158 | + |
| 159 | + print(f'Created {num} testcases.') |
| 160 | + |
| 161 | + |
| 162 | +if __name__ == '__main__': |
| 163 | + main(sys.argv) |
0 commit comments