| import os |
| import random |
| import subprocess |
| import tempfile |
| import time |
| |
| from scripts.test import shared |
| |
| from . import utils |
| |
| |
| # Runs the fuzzer many times and allows checking for specific variety in the |
| # output. Calls hooks: |
| # |
| # self.found_variety() - checks if we found what we are looking for |
| # self.process_wat(wat) - receives the current fuzz wat |
| # |
| class FuzzerVarietyTester: |
| # Run until we find what we want. Stop only if we reached a max number |
| # of iterations and a timeout. |
| max_time = 60 |
| min_iters = 200 |
| |
| # The maximum size of the wasm-generating input |
| max_size = 1024 |
| |
| def __init__(self, initial): |
| self.initial = initial |
| |
| def test(self): |
| start_time = time.time() |
| stop_time = start_time + self.max_time |
| |
| self.temp_dir = tempfile.TemporaryDirectory() |
| |
| i = 0 |
| while True: |
| i += 1 |
| |
| # Stop early if we found what we are looking for. |
| if self.found_variety(): |
| print(f"{i} iterations {round(time.time() - start_time, 2)} seconds)") |
| print(f'proper import_params : {self.import_params}') |
| print(f'proper export_results: {self.export_results}') |
| return |
| |
| if i > self.min_iters and time.time() > stop_time: |
| raise Exception('looked too long and still failed') |
| |
| # Generate raw random data. |
| size = random.randint(1, self.max_size) |
| temp_dat = os.path.join(self.temp_dir.name, f'temp_{i}.dat') |
| with open(temp_dat, 'wb') as f: |
| f.write(bytes([random.randint(0, 255) for x in range(size)])) |
| |
| # Generate the fuzz testcase from the random data + the initial |
| # contents. |
| args = ['-ttf', temp_dat, '--initial-fuzz=' + self.initial, '-all'] |
| args += self.ttf_args |
| args += ['--print'] |
| wat = shared.run_process(shared.WASM_OPT + args, |
| stdout=subprocess.PIPE).stdout |
| |
| self.process_wat(wat) |
| |
| |
| class FuzzAgainstJSVarietyTester(FuzzerVarietyTester): |
| # When --fuzz-against-js is used, the wasm is only going to be fuzzed |
| # against JS, so the fuzzer mutates the boundary in valid ways, even if |
| # --fuzz-preserve-imports-exports is set. |
| # |
| # Testing this deterministically is too hard (as the fuzzer evolves, it |
| # will handle random data differently, and the test would constantly get |
| # out of date). Instead, test randomly, but in a way that the chance of |
| # a flake is unrealistic. |
| ttf_args = ['--fuzz-preserve-imports-exports', '--fuzz-against-js'] |
| |
| def __init__(self, initial): |
| super().__init__(initial) |
| |
| # The set of all params we see, for the import that is refinable. Ditto |
| # for export results. |
| self.import_params = set() |
| self.export_results = set() |
| |
| def found_variety(self): |
| return self.found_expected(self.import_params) and self.found_expected(self.export_results) |
| |
| def process_wat(self, wat): |
| # The things that begin reffed might end up not reffed, if mutation |
| # removes the refs. Check for that. |
| import_reffed_is_reffed = '(ref.func $import-reffed)' in wat |
| export_reffed_is_reffed = '(ref.func $export-reffed)' in wat |
| |
| # Find the params/results that might be refined. |
| for line in wat.splitlines(): |
| if line.startswith(' (import "module" "base" (func $import '): |
| params, results = self.parse_params_results(line) |
| self.import_params.add(params) |
| assert results == '(result eqref)', 'cannot refine import result' |
| elif line.startswith(' (import "module" "base" (func $import-reffed '): |
| params, results = self.parse_params_results(line) |
| if import_reffed_is_reffed: |
| assert params == '(param i32 anyref)', 'cannot refine reffed stuff' |
| assert results == '(result eqref)', 'cannot refine import result' |
| if line.startswith(' (func $export '): |
| params, results = self.parse_params_results(line) |
| assert params == '(param $0 i32) (param $1 anyref)', 'cannot refine export params' |
| self.export_results.add(results) |
| if line.startswith(' (func $export-reffed '): |
| params, results = self.parse_params_results(line) |
| assert params == '(param $0 i32) (param $1 anyref)', 'cannot refine export params' |
| if export_reffed_is_reffed: |
| assert results == '(result eqref)', 'cannot refine reffed stuff' |
| |
| # Given the types we saw for params or results, look in detail for the |
| # things we expect to see. |
| def found_expected(self, data): |
| # The many returns here seem to be the best way to write this code. |
| # ruff: noqa: PLR0911 |
| |
| # Look for significant variety. |
| if len(data) < 5: |
| return False |
| |
| string = str(data) |
| |
| # Each of the following has a 50% chance to get emitted each time, so |
| # over many iterations, the chance of failing to find them goes |
| # exponentially to nothing. |
| |
| # There must be nullable types. |
| if '(ref null' not in string: |
| return False |
| |
| # There must be non-nullable types. |
| if '(ref (' not in string and '(ref $' not in string: |
| return False |
| |
| string = string.replace('null ', '') |
| |
| # There must be defined types. |
| if ' $' not in string: |
| return False |
| |
| # There must be exact types. |
| if '(exact ' not in string: |
| return False |
| |
| # There must be inexact types. |
| if '(ref $' not in string: |
| return False |
| |
| return True |
| |
| # Given a line with wat params and results, parse and return them. |
| def parse_params_results(self, line): |
| # Find either params or results. |
| def get(what, line): |
| ret = '' |
| pos = 0 |
| |
| while True: |
| # Find the thing we are looking for. |
| start = line.find(what, pos) |
| if start < 0: |
| break |
| |
| # Find the end paren. |
| parens = 1 |
| end = start + 1 |
| while parens > 0: |
| if line[end] == '(': |
| parens += 1 |
| elif line[end] == ')': |
| parens -= 1 |
| end += 1 |
| |
| # Add (separated by a space). |
| if ret: |
| ret += ' ' |
| ret += line[start:end] |
| |
| # Keep looking. |
| pos = end |
| |
| return ret |
| |
| return get('(param', line), get('(result', line) |
| |
| |
| class PreserveFuzzTest(utils.BinaryenTestCase): |
| def test_against_js(self): |
| FuzzAgainstJSVarietyTester(self.input_path('fuzz.wat')).test() |