Skip to content

Commit be3135c

Browse files
authored
Bysyncify: Fuzzing (#2192)
Gets fuzzing support for Bysyncify working. * Add the python to run the fuzzing on bysyncify. * Add a JS script to load and run a testcase with bysyncify support. The code has all the runtime support for sleep/resume etc., which it does on calls to imports at random in a deterministic manner. * Export memory from fuzzer so JS can access it. * Fix tiny builder bug with makeExport.
1 parent ab34a77 commit be3135c

7 files changed

Lines changed: 273 additions & 22 deletions

File tree

scripts/fuzz_opt.py

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
# utilities
5353

5454

55+
def in_binaryen(*args):
56+
return os.path.join(options.binaryen_root, *args)
57+
58+
5559
def in_bin(tool):
5660
return os.path.join(options.binaryen_root, 'bin', tool)
5761

@@ -139,8 +143,17 @@ def run_vm(cmd):
139143
raise
140144

141145

142-
def run_bynterp(wasm):
143-
return fix_output(run_vm([in_bin('wasm-opt'), wasm, '--fuzz-exec-before'] + FEATURE_OPTS))
146+
def run_bynterp(wasm, args):
147+
# increase the interpreter stack depth, to test more things
148+
os.environ['BINARYEN_MAX_INTERPRETER_DEPTH'] = '1000'
149+
try:
150+
return run_vm([in_bin('wasm-opt'), wasm] + FEATURE_OPTS + args)
151+
finally:
152+
del os.environ['BINARYEN_MAX_INTERPRETER_DEPTH']
153+
154+
155+
def run_d8(wasm):
156+
return run_vm(['d8', in_binaryen('scripts', 'fuzz_shell.js'), '--', wasm])
144157

145158

146159
# Each test case handler receives two wasm files, one before and one after some changes
@@ -166,7 +179,7 @@ def handle_pair(self, before_wasm, after_wasm, opts):
166179

167180
def run_vms(self, js, wasm):
168181
results = []
169-
results.append(run_bynterp(wasm))
182+
results.append(fix_output(run_bynterp(wasm, ['--fuzz-exec-before'])))
170183
results.append(fix_output(run_vm(['d8', js] + V8_OPTS + ['--', wasm])))
171184

172185
# append to add results from VMs
@@ -200,7 +213,7 @@ def compare_vs(self, before, after):
200213
class FuzzExec(TestCaseHandler):
201214
def handle_pair(self, before_wasm, after_wasm, opts):
202215
# fuzz binaryen interpreter itself. separate invocation so result is easily fuzzable
203-
run([in_bin('wasm-opt'), before_wasm, '--fuzz-exec', '--fuzz-binary'] + opts)
216+
run_bynterp(before_wasm, ['--fuzz-exec', '--fuzz-binary'])
204217

205218

206219
# Check for determinism - the same command must have the same output
@@ -241,19 +254,44 @@ def run(self, wasm):
241254

242255

243256
class Bysyncify(TestCaseHandler):
244-
def handle(self, wasm):
245-
# run normally and run in an async manner, and compare
246-
before = run([in_bin('wasm-opt'), wasm, '--fuzz-exec'])
257+
def handle_pair(self, before_wasm, after_wasm, opts):
258+
# we must legalize in order to run in JS
259+
run([in_bin('wasm-opt'), before_wasm, '--legalize-js-interface', '-o', before_wasm])
260+
run([in_bin('wasm-opt'), after_wasm, '--legalize-js-interface', '-o', after_wasm])
261+
before = fix_output(run_d8(before_wasm))
262+
after = fix_output(run_d8(after_wasm))
263+
247264
# TODO: also something that actually does async sleeps in the code, say
248265
# on the logging commands?
249266
# --remove-unused-module-elements removes the bysyncify intrinsics, which are not valid to call
250-
cmd = [in_bin('wasm-opt'), wasm, '--bysyncify', '--remove-unused-module-elements', '-o', 'by.wasm']
251-
if random.random() < 0.5:
252-
cmd += ['--optimize-level=3'] # TODO: more
253-
run(cmd)
254-
after = run([in_bin('wasm-opt'), 'by.wasm', '--fuzz-exec'])
255-
after = '\n'.join([line for line in after.splitlines() if '[fuzz-exec] calling $bysyncify' not in line])
256-
compare(before, after, 'Bysyncify')
267+
268+
def do_bysyncify(wasm):
269+
cmd = [in_bin('wasm-opt'), wasm, '--bysyncify', '-o', 't.wasm']
270+
if random.random() < 0.5:
271+
cmd += ['--optimize-level=%d' % random.randint(1, 3)]
272+
if random.random() < 0.5:
273+
cmd += ['--shrink-level=%d' % random.randint(1, 2)]
274+
run(cmd)
275+
out = run_d8('t.wasm')
276+
# emit some status logging from bysyncify
277+
print(out.splitlines()[-1])
278+
# ignore the output from the new bysyncify API calls - the ones with asserts will trap, too
279+
for ignore in ['[fuzz-exec] calling $bysyncify_start_unwind\nexception!\n',
280+
'[fuzz-exec] calling $bysyncify_start_unwind\n',
281+
'[fuzz-exec] calling $bysyncify_start_rewind\nexception!\n',
282+
'[fuzz-exec] calling $bysyncify_start_rewind\n',
283+
'[fuzz-exec] calling $bysyncify_stop_rewind\n',
284+
'[fuzz-exec] calling $bysyncify_stop_unwind\n']:
285+
out = out.replace(ignore, '')
286+
out = '\n'.join([l for l in out.splitlines() if 'bysyncify: ' not in l])
287+
return fix_output(out)
288+
289+
before_bysyncify = do_bysyncify(before_wasm)
290+
after_bysyncify = do_bysyncify(after_wasm)
291+
292+
compare(before, after, 'Bysyncify (before/after)')
293+
compare(before, before_bysyncify, 'Bysyncify (before/before_bysyncify)')
294+
compare(before, after_bysyncify, 'Bysyncify (before/after_bysyncify)')
257295

258296

259297
# The global list of all test case handlers
@@ -262,19 +300,19 @@ def handle(self, wasm):
262300
FuzzExec(),
263301
CheckDeterminism(),
264302
Wasm2JS(),
265-
# TODO Bysyncify(),
303+
Bysyncify(),
266304
]
267305

268306

269307
# Do one test, given an input file for -ttf and some optimizations to run
270-
def test_one(infile, opts):
308+
def test_one(random_input, opts):
271309
randomize_pass_debug()
272310

273311
bytes = 0
274312

275313
# fuzz vms
276314
# gather VM outputs on input file
277-
run([in_bin('wasm-opt'), infile, '-ttf', '-o', 'a.wasm'] + FUZZ_OPTS + FEATURE_OPTS)
315+
run([in_bin('wasm-opt'), random_input, '-ttf', '-o', 'a.wasm'] + FUZZ_OPTS + FEATURE_OPTS)
278316
wasm_size = os.stat('a.wasm').st_size
279317
bytes += wasm_size
280318
print('pre js size :', os.stat('a.js').st_size, ' wasm size:', wasm_size)

scripts/fuzz_shell.js

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Shell integration.
2+
if (typeof console === 'undefined') {
3+
console = { log: print };
4+
}
5+
var tempRet0;
6+
var binary;
7+
if (typeof process === 'object' && typeof require === 'function' /* node.js detection */) {
8+
var args = process.argv.slice(2);
9+
binary = require('fs').readFileSync(args[0]);
10+
if (!binary.buffer) binary = new Uint8Array(binary);
11+
} else {
12+
var args;
13+
if (typeof scriptArgs != 'undefined') {
14+
args = scriptArgs;
15+
} else if (typeof arguments != 'undefined') {
16+
args = arguments;
17+
}
18+
if (typeof readbuffer === 'function') {
19+
binary = new Uint8Array(readbuffer(args[0]));
20+
} else {
21+
binary = read(args[0], 'binary');
22+
}
23+
}
24+
25+
// Utilities.
26+
function assert(x, y) {
27+
if (!x) throw (y || 'assertion failed');// + new Error().stack;
28+
}
29+
30+
// Deterministic randomness.
31+
var detrand = (function() {
32+
var hash = 5381; // TODO DET_RAND_SEED;
33+
var x = 0;
34+
return function() {
35+
hash = (((hash << 5) + hash) ^ (x & 0xff)) >>> 0;
36+
x = (x + 1) % 256;
37+
return (hash % 256) / 256;
38+
};
39+
})();
40+
41+
// Bysyncify integration.
42+
var Bysyncify = {
43+
sleeping: false,
44+
sleepingFunction: null,
45+
sleeps: 0,
46+
maxDepth: 0,
47+
DATA_ADDR: 4,
48+
DATA_MAX: 65536,
49+
savedMemory: null,
50+
instrumentImports: function(imports) {
51+
var ret = {};
52+
for (var module in imports) {
53+
ret[module] = {};
54+
for (var i in imports[module]) {
55+
if (typeof imports[module][i] === 'function') {
56+
(function(module, i) {
57+
ret[module][i] = function() {
58+
if (!Bysyncify.sleeping) {
59+
// Sleep if bysyncify support is present, and at a certain
60+
// probability.
61+
if (exports.bysyncify_start_unwind &&
62+
detrand() < 0.5) {
63+
// We are called in order to start a sleep/unwind.
64+
console.log('bysyncify: sleep in ' + i + '...');
65+
Bysyncify.sleepingFunction = i;
66+
Bysyncify.sleeps++;
67+
var depth = new Error().stack.split('\n').length - 6;
68+
Bysyncify.maxDepth = Math.max(Bysyncify.maxDepth, depth);
69+
// Save the memory we use for data, so after we restore it later, the
70+
// sleep/resume appears to have had no change to memory.
71+
Bysyncify.savedMemory = new Int32Array(view.subarray(Bysyncify.DATA_ADDR >> 2, Bysyncify.DATA_MAX >> 2));
72+
// Unwinding.
73+
// Fill in the data structure. The first value has the stack location,
74+
// which for simplicity we can start right after the data structure itself.
75+
view[Bysyncify.DATA_ADDR >> 2] = Bysyncify.DATA_ADDR + 8;
76+
// The end of the stack will not be reached here anyhow.
77+
view[Bysyncify.DATA_ADDR + 4 >> 2] = Bysyncify.DATA_MAX;
78+
exports.bysyncify_start_unwind(Bysyncify.DATA_ADDR);
79+
Bysyncify.sleeping = true;
80+
} else {
81+
// Don't sleep, normal execution.
82+
return imports[module][i].apply(null, arguments);
83+
}
84+
} else {
85+
// We are called as part of a resume/rewind. Stop sleeping.
86+
console.log('bysyncify: resume in ' + i + '...');
87+
assert(Bysyncify.sleepingFunction === i);
88+
exports.bysyncify_stop_rewind();
89+
// The stack should have been all used up, and so returned to the original state.
90+
assert(view[Bysyncify.DATA_ADDR >> 2] == Bysyncify.DATA_ADDR + 8);
91+
assert(view[Bysyncify.DATA_ADDR + 4 >> 2] == Bysyncify.DATA_MAX);
92+
Bysyncify.sleeping = false;
93+
// Restore the memory to the state from before we slept.
94+
view.set(Bysyncify.savedMemory, Bysyncify.DATA_ADDR >> 2);
95+
return imports[module][i].apply(null, arguments);
96+
}
97+
};
98+
})(module, i);
99+
} else {
100+
ret[module][i] = imports[module][i];
101+
}
102+
}
103+
}
104+
// Add ignored.print, which is ignored by bysyncify, and allows debugging of bysyncified code.
105+
ret['ignored'] = { 'print': function(x, y) { console.log(x, y) } };
106+
return ret;
107+
},
108+
instrumentExports: function(exports) {
109+
var ret = {};
110+
for (var e in exports) {
111+
if (typeof exports[e] === 'function' &&
112+
!e.startsWith('bysyncify_')) {
113+
(function(e) {
114+
ret[e] = function() {
115+
while (1) {
116+
var ret = exports[e].apply(null, arguments);
117+
// If we are sleeping, then the stack was unwound; rewind it.
118+
if (Bysyncify.sleeping) {
119+
console.log('bysyncify: stop unwind; rewind');
120+
assert(!ret, 'results during sleep are meaningless, just 0');
121+
//console.log('bysyncify: after unwind', view[Bysyncify.DATA_ADDR >> 2], view[Bysyncify.DATA_ADDR + 4 >> 2]);
122+
try {
123+
exports.bysyncify_stop_unwind();
124+
exports.bysyncify_start_rewind(Bysyncify.DATA_ADDR);
125+
} catch (e) {
126+
console.log('error in unwind/rewind switch', e);
127+
}
128+
continue;
129+
}
130+
return ret;
131+
}
132+
};
133+
})(e);
134+
} else {
135+
ret[e] = exports[e];
136+
}
137+
}
138+
return ret;
139+
},
140+
check: function() {
141+
assert(!Bysyncify.sleeping);
142+
},
143+
finish: function() {
144+
if (Bysyncify.sleeps > 0) {
145+
print('bysyncify:', 'sleeps:', Bysyncify.sleeps, 'max depth:', Bysyncify.maxDepth);
146+
}
147+
},
148+
};
149+
150+
// Fuzz integration.
151+
function logValue(x, y) {
152+
if (typeof y !== 'undefined') {
153+
console.log('[LoggingExternalInterface logging ' + x + ' ' + y + ']');
154+
} else {
155+
console.log('[LoggingExternalInterface logging ' + x + ']');
156+
}
157+
}
158+
159+
// Set up the imports.
160+
var imports = {
161+
'fuzzing-support': {
162+
'log-i32': logValue,
163+
'log-i64': logValue,
164+
'log-f32': logValue,
165+
'log-f64': logValue,
166+
},
167+
'env': {
168+
'setTempRet0': function(x) { tempRet0 = x },
169+
'getTempRet0': function() { return tempRet0 },
170+
},
171+
};
172+
173+
imports = Bysyncify.instrumentImports(imports);
174+
175+
// Create the wasm.
176+
var instance = new WebAssembly.Instance(new WebAssembly.Module(binary), imports);
177+
178+
// Handle the exports.
179+
var exports = instance.exports;
180+
exports = Bysyncify.instrumentExports(exports);
181+
var view = new Int32Array(exports.memory.buffer);
182+
183+
// Run the wasm.
184+
var sortedExports = [];
185+
for (var e in exports) {
186+
sortedExports.push(e);
187+
}
188+
sortedExports.sort();
189+
sortedExports = sortedExports.filter(function(e) {
190+
// Filter special intrinsic functions.
191+
return !e.startsWith('bysyncify_');
192+
});
193+
sortedExports.forEach(function(e) {
194+
Bysyncify.check();
195+
if (typeof exports[e] !== 'function') return;
196+
try {
197+
console.log('[fuzz-exec] calling $' + e);
198+
var result = exports[e]();
199+
if (typeof result !== 'undefined') {
200+
console.log('[fuzz-exec] note result: $' + e + ' => ' + result);
201+
}
202+
} catch (e) {
203+
console.log('exception!');// + [e, e.stack]);
204+
}
205+
});
206+
207+
// Finish up
208+
Bysyncify.finish();
209+

src/tools/fuzzing.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,8 @@ class TranslateToFuzzReader {
375375
hasher->type = ensureFunctionType(getSig(hasher), &wasm)->name;
376376
wasm.addExport(
377377
builder.makeExport(hasher->name, hasher->name, ExternalKind::Function));
378+
// Export memory so JS fuzzing can use it
379+
wasm.addExport(builder.makeExport("memory", "0", ExternalKind::Memory));
378380
}
379381

380382
void setupTable() {

src/wasm-builder.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class Builder {
8484
auto* export_ = new Export();
8585
export_->name = name;
8686
export_->value = value;
87-
export_->kind = ExternalKind::Function;
87+
export_->kind = kind;
8888
return export_;
8989
}
9090

test/passes/translate-to-fuzz_all-features.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
(global $hangLimit (mut i32) (i32.const 10))
2121
(event $event$0 (attr 0) (param i32 f32 i32 f64 i32))
2222
(export "hashMemory" (func $hashMemory))
23+
(export "memory" (memory $0))
2324
(export "func_5" (func $func_5))
2425
(export "hangLimitInitializer" (func $hangLimitInitializer))
2526
(func $hashMemory (; 4 ;) (type $FUNCSIG$i) (result i32)

test/passes/translate-to-fuzz_no-fuzz-nans_all-features.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
(global $hangLimit (mut i32) (i32.const 10))
2020
(event $event$0 (attr 0) (param i32 f32 i32 f64 i32))
2121
(export "hashMemory" (func $hashMemory))
22+
(export "memory" (memory $0))
2223
(export "hangLimitInitializer" (func $hangLimitInitializer))
2324
(func $hashMemory (; 4 ;) (type $FUNCSIG$i) (result i32)
2425
(local $0 i32)

0 commit comments

Comments
 (0)