blob: e1a3dc2fe93034527eeaa6a2db40055ba7c7ed54 [file] [edit]
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()