Skip to content

Commit 45714b5

Browse files
authored
Compare binaryen fuzz-exec to JS VMs (#1856)
The main fuzz_opt.py script compares JS VMs, and separately runs binaryen's fuzz-exec that compares the binaryen interpreter to itself (before and after opts). This PR lets us directly compare binaryen's interpreter output to JS VMs. This found a bunch of minor things we can do better on both sides, giving more fuzz coverage. To enable this, a bunch of tiny fixes were needed: * Add --fuzz-exec-before which is like --fuzz-exec but just runs the code before opts are run, instead of before and after. * Normalize double printing (so JS and C++ print comparable things). This includes negative zero in JS, which we never printed properly til now. * Various improvements to how we print fuzz-exec logging - remove unuseful things, and normalize the others across JS and C++. * Properly legalize the wasm when --emit-js-wrapper (i.e., we will run the code from JS), and use that in the JS wrapper code.
1 parent 4084d6e commit 45714b5

File tree

11 files changed

+200
-151
lines changed

11 files changed

+200
-151
lines changed

check.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ def check_expected(actual, expected):
354354
expected = open(expected).read()
355355

356356
# fix it up, our pretty (i32.const 83) must become compared to a homely 83 : i32
357-
def fix(x):
357+
def fix_expected(x):
358358
x = x.strip()
359359
if not x:
360360
return x
@@ -363,7 +363,13 @@ def fix(x):
363363
v = v[:-1] # remove trailing '.'
364364
return '(' + t + '.const ' + v + ')'
365365

366-
expected = '\n'.join(map(fix, expected.split('\n')))
366+
def fix_actual(x):
367+
if '[trap ' in x:
368+
return ''
369+
return x
370+
371+
expected = '\n'.join(map(fix_expected, expected.split('\n')))
372+
actual = '\n'.join(map(fix_actual, actual.split('\n')))
367373
print ' (using expected output)'
368374
actual = actual.strip()
369375
expected = expected.strip()

scripts/fuzz_opt.py

Lines changed: 76 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import difflib
1818
import subprocess
1919
import random
20+
import re
2021
import shutil
2122
import time
2223

@@ -64,85 +65,88 @@ def randomize_pass_debug():
6465
IGNORE = '[binaryen-fuzzer-ignore]'
6566

6667

67-
def test_one(infile, opts):
68-
def compare(x, y, comment):
69-
if x != y and x != IGNORE and y != IGNORE:
70-
message = ''.join([a.rstrip() + '\n' for a in difflib.unified_diff(x.split('\n'), y.split('\n'), fromfile='expected', tofile='actual')])
71-
raise Exception(str(comment) + ": Expected to have '%s' == '%s', diff:\n\n%s" % (
72-
x, y,
73-
message
74-
))
75-
76-
def run_vms(prefix):
77-
def fix_output(out):
78-
# exceptions may differ when optimizing, but an exception should occur. so ignore their types
79-
# also js engines print them out slightly differently
80-
return '\n'.join(map(lambda x: ' *exception*' if 'exception' in x else x, out.split('\n')))
81-
82-
# normalize different vm output
83-
# also the binaryen optimizer can reorder traps (but not remove them), so
84-
# it really just matters if you trap, not how you trap
85-
return out.replace('unreachable executed', 'unreachable') \
86-
.replace('integer result unrepresentable', 'integer overflow') \
87-
.replace('invalid conversion to integer', 'integer overflow') \
88-
.replace('memory access out of bounds', 'index out of bounds') \
89-
.replace('integer divide by zero', 'divide by zero') \
90-
.replace('integer remainder by zero', 'remainder by zero') \
91-
.replace('remainder by zero', 'divide by zero') \
92-
.replace('divide result unrepresentable', 'integer overflow') \
93-
.replace('divide by zero', 'integer overflow') \
94-
.replace('index out of bounds', 'integer overflow') \
95-
.replace('out of bounds memory access', 'integer overflow')
96-
97-
def fix_spec_output(out):
98-
out = fix_output(out)
99-
# spec shows a pointer when it traps, remove that
100-
out = '\n'.join(map(lambda x: x if 'runtime trap' not in x else x[x.find('runtime trap'):], out.split('\n')))
101-
# https://github.com/WebAssembly/spec/issues/543 , float consts are messed up
102-
out = '\n'.join(map(lambda x: x if 'f32' not in x and 'f64' not in x else '', out.split('\n')))
103-
return out
104-
105-
def run_vm(cmd):
106-
# ignore some vm assertions, if bugs have already been filed
107-
known_issues = [
108-
'local count too large', # ignore this; can be caused by flatten, ssa, etc. passes
109-
'liftoff-assembler.cc, line 239\n', # https://bugs.chromium.org/p/v8/issues/detail?id=8631
110-
'liftoff-register.h, line 86\n', # https://bugs.chromium.org/p/v8/issues/detail?id=8632
111-
]
112-
try:
113-
return run(cmd)
114-
except:
115-
output = run_unchecked(cmd)
116-
for issue in known_issues:
117-
if issue in output:
118-
return IGNORE
119-
raise
120-
121-
results = []
122-
# append to this list to add results from VMs
123-
results += [fix_output(run_vm([os.path.expanduser('d8'), prefix + 'js', '--', prefix + 'wasm']))]
124-
results += [fix_output(run_vm([os.path.expanduser('d8-debug'), '--wasm-tier-up', prefix + 'js', '--', prefix + 'wasm']))]
125-
results += [fix_output(run_vm([os.path.expanduser('d8-debug'), '--no-wasm-tier-up', prefix + 'js', '--', prefix + 'wasm']))]
126-
# spec has no mechanism to not halt on a trap. so we just check until the first trap, basically
127-
# run(['../spec/interpreter/wasm', prefix + 'wasm'])
128-
# results += [fix_spec_output(run_unchecked(['../spec/interpreter/wasm', prefix + 'wasm', '-e', open(prefix + 'wat').read()]))]
129-
130-
if len(results) == 0:
131-
results = [0]
132-
133-
first = results[0]
134-
for i in range(len(results)):
135-
compare(first, results[i], 'comparing between vms at ' + str(i))
136-
137-
return results
68+
def compare(x, y, comment):
69+
if x != y and x != IGNORE and y != IGNORE:
70+
message = ''.join([a.rstrip() + '\n' for a in difflib.unified_diff(x.split('\n'), y.split('\n'), fromfile='expected', tofile='actual')])
71+
raise Exception(str(comment) + ": Expected to have '%s' == '%s', diff:\n\n%s" % (
72+
x, y,
73+
message
74+
))
75+
76+
77+
def run_vms(prefix):
78+
def fix_output(out):
79+
# large doubles may print slightly different on different VMs
80+
def fix_double(x):
81+
x = x.group(1)
82+
if 'nan' in x or 'NaN' in x:
83+
x = 'nan'
84+
else:
85+
x = x.replace('Infinity', 'inf')
86+
x = str(float(x))
87+
return 'f64.const ' + x
88+
out = re.sub(r'f64\.const (-?[nanN:abcdefxIity\d+-.]+)', fix_double, out)
89+
90+
# mark traps from wasm-opt as exceptions, even though they didn't run in a vm
91+
out = out.replace('[trap ', 'exception: [trap ')
92+
93+
# exceptions may differ when optimizing, but an exception should occur. so ignore their types
94+
# also js engines print them out slightly differently
95+
return '\n'.join(map(lambda x: ' *exception*' if 'exception' in x else x, out.split('\n')))
96+
97+
def fix_spec_output(out):
98+
out = fix_output(out)
99+
# spec shows a pointer when it traps, remove that
100+
out = '\n'.join(map(lambda x: x if 'runtime trap' not in x else x[x.find('runtime trap'):], out.split('\n')))
101+
# https://github.com/WebAssembly/spec/issues/543 , float consts are messed up
102+
out = '\n'.join(map(lambda x: x if 'f32' not in x and 'f64' not in x else '', out.split('\n')))
103+
return out
104+
105+
def run_vm(cmd):
106+
# ignore some vm assertions, if bugs have already been filed
107+
known_issues = [
108+
'local count too large', # ignore this; can be caused by flatten, ssa, etc. passes
109+
'liftoff-assembler.cc, line 239\n', # https://bugs.chromium.org/p/v8/issues/detail?id=8631
110+
'liftoff-assembler.cc, line 245\n', # https://bugs.chromium.org/p/v8/issues/detail?id=8631
111+
'liftoff-register.h, line 86\n', # https://bugs.chromium.org/p/v8/issues/detail?id=8632
112+
]
113+
try:
114+
return run(cmd)
115+
except:
116+
output = run_unchecked(cmd)
117+
for issue in known_issues:
118+
if issue in output:
119+
return IGNORE
120+
raise
121+
122+
results = []
123+
# append to this list to add results from VMs
124+
results += [fix_output(run_vm([in_bin('wasm-opt'), prefix + 'wasm', '--fuzz-exec-before']))]
125+
results += [fix_output(run_vm([os.path.expanduser('d8'), '--experimental-wasm-sat_f2i_conversions', prefix + 'js', '--', prefix + 'wasm']))]
126+
results += [fix_output(run_vm([os.path.expanduser('d8-debug'), '--experimental-wasm-sat_f2i_conversions', '--wasm-tier-up', prefix + 'js', '--', prefix + 'wasm']))]
127+
results += [fix_output(run_vm([os.path.expanduser('d8-debug'), '--experimental-wasm-sat_f2i_conversions', '--no-wasm-tier-up', prefix + 'js', '--', prefix + 'wasm']))]
128+
# spec has no mechanism to not halt on a trap. so we just check until the first trap, basically
129+
# run(['../spec/interpreter/wasm', prefix + 'wasm'])
130+
# results += [fix_spec_output(run_unchecked(['../spec/interpreter/wasm', prefix + 'wasm', '-e', open(prefix + 'wat').read()]))]
131+
132+
if len(results) == 0:
133+
results = [0]
134+
135+
first = results[0]
136+
for i in range(len(results)):
137+
compare(first, results[i], 'comparing between vms at ' + str(i))
138+
139+
return results
140+
138141

142+
def test_one(infile, opts):
139143
randomize_pass_debug()
140144

141145
bytes = 0
142146

143147
# fuzz vms
144148
# gather VM outputs on input file
145-
run([in_bin('wasm-opt'), infile, '-ttf', '--emit-js-wrapper=a.js', '--emit-spec-wrapper=a.wat', '-o', 'a.wasm', '--mvp-features'])
149+
run([in_bin('wasm-opt'), infile, '-ttf', '--emit-js-wrapper=a.js', '--emit-spec-wrapper=a.wat', '-o', 'a.wasm', '--mvp-features', '--enable-nontrapping-float-to-int'])
146150
wasm_size = os.stat('a.wasm').st_size
147151
bytes += wasm_size
148152
print('pre js size :', os.stat('a.js').st_size, ' wasm size:', wasm_size)

src/shell-interface.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ struct ShellExternalInterface : ModuleInstance::ExternalInterface {
200200
}
201201

202202
void trap(const char* why) override {
203-
std::cerr << "[trap " << why << "]\n";
203+
std::cout << "[trap " << why << "]\n";
204204
throw TrapException();
205205
}
206206
};

src/tools/execution-results.h

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,22 @@ namespace wasm {
2626

2727
typedef std::vector<Literal> Loggings;
2828

29-
// Logs every single import call parameter.
29+
// Logs every relevant import call parameter.
3030
struct LoggingExternalInterface : public ShellExternalInterface {
3131
Loggings& loggings;
3232

3333
LoggingExternalInterface(Loggings& loggings) : loggings(loggings) {}
3434

3535
Literal callImport(Function* import, LiteralList& arguments) override {
36-
std::cout << "[LoggingExternalInterface logging";
37-
loggings.push_back(Literal()); // buffer with a None between calls
38-
for (auto argument : arguments) {
39-
std::cout << ' ' << argument;
40-
loggings.push_back(argument);
36+
if (import->module == "fuzzing-support") {
37+
std::cout << "[LoggingExternalInterface logging";
38+
loggings.push_back(Literal()); // buffer with a None between calls
39+
for (auto argument : arguments) {
40+
std::cout << ' ' << argument;
41+
loggings.push_back(argument);
42+
}
43+
std::cout << "]\n";
4144
}
42-
std::cout << "]\n";
4345
return Literal();
4446
}
4547
};
@@ -60,21 +62,23 @@ struct ExecutionResults {
6062
// execute all exported methods (that are therefore preserved through opts)
6163
for (auto& exp : wasm.exports) {
6264
if (exp->kind != ExternalKind::Function) continue;
65+
std::cout << "[fuzz-exec] calling " << exp->name << "\n";
6366
auto* func = wasm.getFunction(exp->value);
6467
if (func->result != none) {
6568
// this has a result
6669
results[exp->name] = run(func, wasm, instance);
67-
std::cout << "[fuzz-exec] note result: " << exp->name << " => " << results[exp->name] << '\n';
70+
// ignore the result if we hit an unreachable and returned no value
71+
if (isConcreteType(results[exp->name].type)) {
72+
std::cout << "[fuzz-exec] note result: " << exp->name << " => " << results[exp->name] << '\n';
73+
}
6874
} else {
6975
// no result, run it anyhow (it might modify memory etc.)
7076
run(func, wasm, instance);
71-
std::cout << "[fuzz-exec] no result for void func: " << exp->name << '\n';
7277
}
7378
}
7479
} catch (const TrapException&) {
7580
// may throw in instance creation (init of offsets)
7681
}
77-
std::cout << "[fuzz-exec] " << results.size() << " results noted\n";
7882
}
7983

8084
// get current results and check them against previous ones
@@ -85,7 +89,6 @@ struct ExecutionResults {
8589
std::cout << "[fuzz-exec] optimization passes changed execution results";
8690
abort();
8791
}
88-
std::cout << "[fuzz-exec] " << results.size() << " results match\n";
8992
}
9093

9194
bool operator==(ExecutionResults& other) {
@@ -127,8 +130,8 @@ struct ExecutionResults {
127130
try {
128131
LiteralList arguments;
129132
// init hang support, if present
130-
if (wasm.getFunctionOrNull("hangLimitInitializer")) {
131-
instance.callFunction("hangLimitInitializer", arguments);
133+
if (auto* ex = wasm.getExportOrNull("hangLimitInitializer")) {
134+
instance.callFunction(ex->value, arguments);
132135
}
133136
// call the method
134137
for (Type param : func->params) {

src/tools/js-wrapper.h

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@
2222
namespace wasm {
2323

2424
static std::string generateJSWrapper(Module& wasm) {
25-
PassRunner runner(&wasm);
26-
runner.add("legalize-js-interface");
27-
runner.run();
28-
2925
std::string ret;
3026
ret += "if (typeof console === 'undefined') {\n"
3127
" console = { log: print };\n"
@@ -49,12 +45,26 @@ static std::string generateJSWrapper(Module& wasm) {
4945
" binary = read(args[0], 'binary');\n"
5046
" }\n"
5147
"}\n"
48+
"function literal(x, type) {\n"
49+
" var ret = type + '.const ';\n"
50+
" switch (type) {\n"
51+
" case 'i32': ret += (x | 0); break;\n"
52+
" case 'f32':\n"
53+
" case 'f64': {\n"
54+
" if (x == 0 && (1 / x) < 0) ret += '-';\n"
55+
" ret += x;\n"
56+
" break;\n"
57+
" }\n"
58+
" default: throw 'what?';\n"
59+
" }\n"
60+
" return ret;\n"
61+
"}\n"
5262
"var instance = new WebAssembly.Instance(new WebAssembly.Module(binary), {\n"
5363
" 'fuzzing-support': {\n"
54-
" 'log-i32': function(x) { console.log('i32: ' + x) },\n"
55-
" 'log-i64': function(x, y) { console.log('i64: ' + x + ', ' + y) },\n"
56-
" 'log-f32': function(x) { console.log('f32: ' + x) },\n"
57-
" 'log-f64': function(x) { console.log('f64: ' + x) }\n"
64+
" 'log-i32': function(x) { console.log('[LoggingExternalInterface logging ' + literal(x, 'i32') + ']') },\n"
65+
" 'log-i64': function(x, y) { console.log('[LoggingExternalInterface logging ' + literal(x, 'i32') + ' ' + literal(y, 'i32') + ']') },\n" // legalization: two i32s
66+
" 'log-f32': function(x) { console.log('[LoggingExternalInterface logging ' + literal(x, 'f64') + ']') },\n" // legalization: an f64
67+
" 'log-f64': function(x) { console.log('[LoggingExternalInterface logging ' + literal(x, 'f64') + ']') },\n"
5868
" },\n"
5969
" 'env': {\n"
6070
" 'setTempRet0': function(x) { tempRet0 = x },\n"
@@ -64,40 +74,38 @@ static std::string generateJSWrapper(Module& wasm) {
6474
for (auto& exp : wasm.exports) {
6575
auto* func = wasm.getFunctionOrNull(exp->value);
6676
if (!func) continue; // something exported other than a function
67-
auto bad = false; // check for things we can't support
68-
for (Type param : func->params) {
69-
if (param == i64) bad = true;
70-
}
71-
if (func->result == i64) bad = true;
72-
if (bad) continue;
7377
ret += "if (instance.exports.hangLimitInitializer) instance.exports.hangLimitInitializer();\n";
7478
ret += "try {\n";
75-
ret += std::string(" console.log('calling: ") + exp->name.str + "');\n";
79+
ret += std::string(" console.log('[fuzz-exec] calling $") + exp->name.str + "');\n";
7680
if (func->result != none) {
77-
ret += " console.log(' result: ' + ";
81+
ret += std::string(" console.log('[fuzz-exec] note result: $") + exp->name.str + " => ' + literal(";
82+
} else {
83+
ret += " ";
7884
}
7985
ret += std::string("instance.exports.") + exp->name.str + "(";
8086
bool first = true;
8187
for (Type param : func->params) {
82-
WASM_UNUSED(param);
8388
// zeros in arguments TODO more?
8489
if (first) {
8590
first = false;
8691
} else {
8792
ret += ", ";
8893
}
8994
ret += "0";
95+
if (param == i64) {
96+
ret += ", 0";
97+
}
9098
}
9199
ret += ")";
92100
if (func->result != none) {
93-
ret += ")"; // for console.log
101+
ret += ", '" + std::string(printType(func->result)) + "'))";
102+
// TODO: getTempRet
94103
}
95104
ret += ";\n";
96105
ret += "} catch (e) {\n";
97-
ret += " console.log(' exception: ' + e);\n";
106+
ret += " console.log('exception: ' + e);\n";
98107
ret += "}\n";
99108
}
100-
ret += "console.log('done.')\n";
101109
return ret;
102110
}
103111

0 commit comments

Comments
 (0)