Skip to content

Commit a53356a

Browse files
authored
wasm-opt fuzz script (#1682) [ci skip]
A small fuzz script I've been using locally. Runs wasm-opt on random inputs and random passes, looking for breakage or the passes changing something. Can also run VMs before and after the passes, and compare the VMs.
1 parent cefbbfa commit a53356a

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed

scripts/fuzz_opt.py

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
'''
2+
Runs random passes and options on random inputs, using wasm-opt.
3+
4+
Can be configured to run just wasm-opt itself (using --fuzz-exec)
5+
or also run VMs on it.
6+
7+
For afl-fuzz integration, you probably don't want this, and can use
8+
something like
9+
10+
BINARYEN_CORES=1 BINARYEN_PASS_DEBUG=1 afl-fuzz -i afl-testcases/ -o afl-findings/ -m 100 -d -- bin/wasm-opt -ttf --fuzz-exec --Os @@
11+
12+
(that is on a fixed set of arguments to wasm-opt, though - this
13+
script covers different options being passed)
14+
'''
15+
16+
import os
17+
import sys
18+
import difflib
19+
import subprocess
20+
import random
21+
import shutil
22+
import time
23+
24+
# parameters
25+
26+
LOG_LIMIT = 125
27+
INPUT_SIZE_LIMIT = 250 * 1024
28+
29+
30+
def random_size():
31+
return random.randint(1, INPUT_SIZE_LIMIT)
32+
33+
34+
def run(cmd):
35+
print(' '.join(cmd)[:LOG_LIMIT])
36+
return subprocess.check_output(cmd)
37+
38+
39+
def run_unchecked(cmd):
40+
print(' '.join(cmd)[:LOG_LIMIT])
41+
return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0]
42+
43+
44+
def randomize_pass_debug():
45+
if random.random() < 0.125:
46+
print('[pass-debug]')
47+
os.environ['BINARYEN_PASS_DEBUG'] = '1'
48+
else:
49+
os.environ['BINARYEN_PASS_DEBUG'] = '0'
50+
del os.environ['BINARYEN_PASS_DEBUG']
51+
52+
53+
def test_one(infile, opts):
54+
def compare(x, y, comment):
55+
if x != y:
56+
message = ''.join([a.rstrip() + '\n' for a in difflib.unified_diff(x.split('\n'), y.split('\n'), fromfile='expected', tofile='actual')])
57+
raise Exception(str(comment) + ": Expected to have '%s' == '%s', diff:\n\n%s" % (
58+
x, y,
59+
message
60+
))
61+
62+
def run_vms(prefix):
63+
def fix_output(out):
64+
# exceptions may differ when optimizing, but an exception should occur. so ignore their types
65+
# also js engines print them out slightly differently
66+
return '\n'.join(map(lambda x: ' *exception*' if 'exception' in x else x, out.split('\n')))
67+
68+
# normalize different vm output
69+
# also the binaryen optimizer can reorder traps (but not remove them), so
70+
# it really just matters if you trap, not how you trap
71+
return out.replace('unreachable executed', 'unreachable') \
72+
.replace('integer result unrepresentable', 'integer overflow') \
73+
.replace('invalid conversion to integer', 'integer overflow') \
74+
.replace('memory access out of bounds', 'index out of bounds') \
75+
.replace('integer divide by zero', 'divide by zero') \
76+
.replace('integer remainder by zero', 'remainder by zero') \
77+
.replace('remainder by zero', 'divide by zero') \
78+
.replace('divide result unrepresentable', 'integer overflow') \
79+
.replace('divide by zero', 'integer overflow') \
80+
.replace('index out of bounds', 'integer overflow') \
81+
.replace('out of bounds memory access', 'integer overflow')
82+
83+
def fix_spec_output(out):
84+
out = fix_output(out)
85+
# spec shows a pointer when it traps, remove that
86+
out = '\n'.join(map(lambda x: x if 'runtime trap' not in x else x[x.find('runtime trap'):], out.split('\n')))
87+
# https://github.com/WebAssembly/spec/issues/543 , float consts are messed up
88+
out = '\n'.join(map(lambda x: x if 'f32' not in x and 'f64' not in x else '', out.split('\n')))
89+
return out
90+
91+
results = []
92+
# append to this list to add results from VMs
93+
# results += [fix_output(run([os.path.expanduser('d8'), '--', prefix + 'js', prefix + 'wasm']))]
94+
# spec has no mechanism to not halt on a trap. so we just check until the first trap, basically
95+
# run(['../spec/interpreter/wasm', prefix + 'wasm'])
96+
# results += [fix_spec_output(run_unchecked(['../spec/interpreter/wasm', prefix + 'wasm', '-e', open(prefix + 'wat').read()]))]
97+
98+
if len(results) == 0:
99+
results = [0]
100+
101+
first = results[0]
102+
for i in range(len(results)):
103+
compare(first, results[i], 'comparing between vms at ' + str(i))
104+
105+
return results
106+
107+
randomize_pass_debug()
108+
109+
bytes = 0
110+
111+
# fuzz vms
112+
# gather VM outputs on input file
113+
run(['bin/wasm-opt', infile, '-ttf', '--emit-js-wrapper=a.js', '--emit-spec-wrapper=a.wat', '-o', 'a.wasm'])
114+
wasm_size = os.stat('a.wasm').st_size
115+
bytes += wasm_size
116+
print('pre js size :', os.stat('a.js').st_size, ' wasm size:', wasm_size)
117+
before = run_vms('a.')
118+
print('----------------')
119+
# gather VM outputs on processed file
120+
run(['bin/wasm-opt', 'a.wasm', '-o', 'b.wasm'] + opts)
121+
wasm_size = os.stat('b.wasm').st_size
122+
bytes += wasm_size
123+
print('post js size:', os.stat('a.js').st_size, ' wasm size:', wasm_size)
124+
shutil.copyfile('a.js', 'b.js')
125+
after = run_vms('b.')
126+
for i in range(len(before)):
127+
compare(before[i], after[i], 'comparing between builds at ' + str(i))
128+
# fuzz binaryen interpreter itself. separate invocation so result is easily fuzzable
129+
run(['bin/wasm-opt', 'a.wasm', '--fuzz-exec', '--fuzz-binary'] + opts)
130+
131+
return bytes
132+
133+
134+
# main
135+
136+
opt_choices = [
137+
[],
138+
['-O1'], ['-O2'], ['-O3'], ['-O4'], ['-Os'], ['-Oz'],
139+
["--coalesce-locals"],
140+
# XXX slow, non-default ["--coalesce-locals-learning"],
141+
["--code-pushing"],
142+
["--code-folding"],
143+
["--const-hoisting"],
144+
["--dae"],
145+
["--dae-optimizing"],
146+
["--dce"],
147+
["--flatten", "--dfo"],
148+
["--duplicate-function-elimination"],
149+
["--flatten"],
150+
# ["--fpcast-emu"], # removes indirect call failures as it makes them go through regardless of type
151+
["--inlining"],
152+
["--inlining-optimizing"],
153+
["--flatten", "--local-cse"],
154+
["--generate-stack-ir"],
155+
["--licm"],
156+
["--memory-packing"],
157+
["--merge-blocks"],
158+
['--merge-locals'],
159+
["--optimize-instructions"],
160+
["--optimize-stack-ir"],
161+
["--generate-stack-ir", "--optimize-stack-ir"],
162+
["--pick-load-signs"],
163+
["--precompute"],
164+
["--precompute-propagate"],
165+
["--remove-unused-brs"],
166+
["--remove-unused-nonfunction-module-elements"],
167+
["--remove-unused-module-elements"],
168+
["--remove-unused-names"],
169+
["--reorder-functions"],
170+
["--reorder-locals"],
171+
["--flatten", "--rereloop"],
172+
["--rse"],
173+
["--simplify-locals"],
174+
["--simplify-locals-nonesting"],
175+
["--simplify-locals-nostructure"],
176+
["--simplify-locals-notee"],
177+
["--simplify-locals-notee-nostructure"],
178+
["--ssa"],
179+
["--vacuum"],
180+
]
181+
182+
183+
def get_multiple_opt_choices():
184+
ret = []
185+
# core opts
186+
while 1:
187+
ret += random.choice(opt_choices)
188+
if len(ret) > 20 or random.random() < 0.3:
189+
break
190+
# modifiers (if not already implied by a -O? option)
191+
if '-O' not in str(ret):
192+
if random.random() < 0.5:
193+
ret += ['--optimize-level=' + str(random.randint(0, 3))]
194+
if random.random() < 0.5:
195+
ret += ['--shrink-level=' + str(random.randint(0, 3))]
196+
return ret
197+
198+
199+
# main
200+
201+
if len(sys.argv) >= 2:
202+
print('checking given input')
203+
if len(sys.argv) >= 3:
204+
test_one(sys.argv[1], sys.argv[2:])
205+
else:
206+
for opts in opt_choices:
207+
print(opts)
208+
test_one(sys.argv[1], opts)
209+
else:
210+
print('checking infinite random inputs')
211+
random.seed(time.time())
212+
temp = 'input.dat'
213+
counter = 0
214+
bytes = 0 # wasm bytes tested
215+
start_time = time.time()
216+
while True:
217+
counter += 1
218+
f = open(temp, 'w')
219+
size = random_size()
220+
print('\nITERATION:', counter, 'size:', size, 'speed:', counter / (time.time() - start_time), 'iters/sec, ', bytes / (time.time() - start_time), 'bytes/sec\n')
221+
for x in range(size):
222+
f.write(chr(random.randint(0, 255)))
223+
f.close()
224+
opts = get_multiple_opt_choices()
225+
print('opts:', ' '.join(opts))
226+
bytes += test_one('input.dat', opts)

0 commit comments

Comments
 (0)