Skip to content

Commit 679c26f

Browse files
authored
wasm-split: Add fuzzer support (#7014)
The support is added but not enabled as this is still finding bugs. The first part here is to add Split testcase handler to the fuzzer, which runs a wasm, then runs it again after splitting it and then linking it at runtime, and checking for different results. The second part is support for linking two modules at runtime in the fuzzer's JS code, that works in tandem with the first part. New options are added to load and link a second wasm, and to pick which exports to run.
1 parent ca3302b commit 679c26f

File tree

2 files changed

+189
-37
lines changed

2 files changed

+189
-37
lines changed

scripts/fuzz_opt.py

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -743,8 +743,8 @@ def run_d8_js(js, args=[], liftoff=True):
743743
FUZZ_SHELL_JS = in_binaryen('scripts', 'fuzz_shell.js')
744744

745745

746-
def run_d8_wasm(wasm, liftoff=True):
747-
return run_d8_js(FUZZ_SHELL_JS, [wasm], liftoff=liftoff)
746+
def run_d8_wasm(wasm, liftoff=True, args=[]):
747+
return run_d8_js(FUZZ_SHELL_JS, [wasm] + args, liftoff=liftoff)
748748

749749

750750
def all_disallowed(features):
@@ -1391,6 +1391,111 @@ def handle(self, wasm):
13911391
compare_between_vms(output, merged_output, 'Merge')
13921392

13931393

1394+
FUNC_NAMES_REGEX = re.compile(r'\n [(]func [$](\S+)')
1395+
1396+
1397+
# Tests wasm-split
1398+
class Split(TestCaseHandler):
1399+
frequency = 1 # TODO: adjust lower when we actually enable this
1400+
1401+
def handle(self, wasm):
1402+
# get the list of function names, some of which we will decide to split
1403+
# out
1404+
wat = run([in_bin('wasm-dis'), wasm] + FEATURE_OPTS)
1405+
all_funcs = re.findall(FUNC_NAMES_REGEX, wat)
1406+
1407+
# get the original output before splitting
1408+
output = run_d8_wasm(wasm)
1409+
output = fix_output(output)
1410+
1411+
# find the names of the exports. we need this because when we split the
1412+
# module then new exports appear to connect the two halves of the
1413+
# original module. we do not want to call all the exports on the new
1414+
# primary module, but only the original ones.
1415+
exports = []
1416+
for line in output.splitlines():
1417+
if FUZZ_EXEC_CALL_PREFIX in line:
1418+
exports.append(get_export_from_call_line(line))
1419+
1420+
# pick which to split out, with a random rate of picking (biased towards
1421+
# 0.5).
1422+
rate = (random.random() + random.random()) / 2
1423+
split_funcs = []
1424+
for func in all_funcs:
1425+
if random.random() < rate:
1426+
split_funcs.append(func)
1427+
1428+
if not split_funcs:
1429+
# nothing to split out
1430+
return
1431+
1432+
# split the wasm into two
1433+
primary = wasm + '.primary.wasm'
1434+
secondary = wasm + '.secondary.wasm'
1435+
1436+
# we require reference types, because that allows us to create our own
1437+
# table. without that we use the existing table, and that may interact
1438+
# with user code in odd ways (it really only works with the particular
1439+
# form of table+segments that LLVM emits, and not with random fuzzer
1440+
# content).
1441+
split_feature_opts = FEATURE_OPTS + ['--enable-reference-types']
1442+
1443+
run([in_bin('wasm-split'), wasm, '--split',
1444+
'--split-funcs', ','.join(split_funcs),
1445+
'--primary-output', primary,
1446+
'--secondary-output', secondary] + split_feature_opts)
1447+
1448+
# sometimes also optimize the split modules
1449+
optimized = False
1450+
1451+
def optimize(name):
1452+
# do not optimize if it would change the ABI
1453+
if CLOSED_WORLD:
1454+
return name
1455+
# TODO: use other optimizations here, but we'd need to be careful of
1456+
# anything that can alter the ABI, and also current
1457+
# limitations of open-world optimizations (see discussion in
1458+
# https://github.com/WebAssembly/binaryen/pull/6660)
1459+
opts = ['-O3']
1460+
new_name = name + '.opt.wasm'
1461+
run([in_bin('wasm-opt'), name, '-o', new_name, '-all'] + opts + split_feature_opts)
1462+
nonlocal optimized
1463+
optimized = True
1464+
return new_name
1465+
1466+
if random.random() < 0.5:
1467+
primary = optimize(primary)
1468+
if random.random() < 0.5:
1469+
secondary = optimize(secondary)
1470+
1471+
# prepare the list of exports to call. the format is
1472+
#
1473+
# exports:A,B,C
1474+
#
1475+
exports_to_call = 'exports:' + ','.join(exports)
1476+
1477+
# get the output from the split modules, linking them using JS
1478+
# TODO run liftoff/turboshaft/etc.
1479+
linked_output = run_d8_wasm(primary, args=[secondary, exports_to_call])
1480+
linked_output = fix_output(linked_output)
1481+
1482+
# see D8.can_compare_to_self: we cannot compare optimized outputs if
1483+
# NaNs are allowed, as the optimizer can modify NaNs differently than
1484+
# the JS engine.
1485+
if not (NANS and optimized):
1486+
compare_between_vms(output, linked_output, 'Split')
1487+
1488+
def can_run_on_feature_opts(self, feature_opts):
1489+
# to run the split wasm we use JS, that is, JS links the exports of one
1490+
# to the imports of the other, etc. since we run in JS, the wasm must be
1491+
# valid for JS.
1492+
if not LEGALIZE:
1493+
return False
1494+
1495+
# see D8.can_run
1496+
return all_disallowed(['shared-everything'])
1497+
1498+
13941499
# Check that the text format round-trips without error.
13951500
class RoundtripText(TestCaseHandler):
13961501
frequency = 0.05
@@ -1413,6 +1518,8 @@ def handle(self, wasm):
14131518
TrapsNeverHappen(),
14141519
CtorEval(),
14151520
Merge(),
1521+
# TODO: enable when stable enough, and adjust |frequency| (see above)
1522+
# Split(),
14161523
RoundtripText()
14171524
]
14181525

scripts/fuzz_shell.js

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,54 @@
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);
1+
// Shell integration: find argv and set up readBinary().
2+
var argv;
3+
var readBinary;
4+
if (typeof process === 'object' && typeof require === 'function') {
5+
// Node.js.
6+
argv = process.argv.slice(2);
7+
readBinary = function(name) {
8+
var data = require('fs').readFileSync(name);
9+
if (!data.buffer) data = new Uint8Array(data);
10+
return data;
11+
};
1112
} else {
12-
var args;
13+
// A shell like D8.
1314
if (typeof scriptArgs != 'undefined') {
14-
args = scriptArgs;
15+
argv = scriptArgs;
1516
} 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');
17+
argv = arguments;
2218
}
19+
readBinary = function(name) {
20+
if (typeof readbuffer === 'function') {
21+
return new Uint8Array(readbuffer(name));
22+
} else {
23+
return read(name, 'binary');
24+
}
25+
};
26+
}
27+
28+
// We are given the binary to run as a parameter.
29+
var binary = readBinary(argv[0]);
30+
31+
// Normally we call all the exports of the given wasm file. But, if we are
32+
// passed a final parameter in the form of "exports:X,Y,Z" then we call
33+
// specifically the exports X, Y, and Z.
34+
var exportsToCall;
35+
if (argv[argv.length - 1].startsWith('exports:')) {
36+
exportsToCall = argv[argv.length - 1].substr('exports:'.length).split(',');
37+
argv.pop();
38+
}
39+
40+
// If a second parameter is given, it is a second binary that we will link in
41+
// with it.
42+
var secondBinary;
43+
if (argv[1]) {
44+
secondBinary = readBinary(argv[1]);
2345
}
2446

2547
// Utilities.
2648
function assert(x, y) {
2749
if (!x) throw (y || 'assertion failed');// + new Error().stack;
2850
}
2951

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-
4152
// Print out a value in a way that works well for fuzzing.
4253
function printed(x, y) {
4354
if (typeof y !== 'undefined') {
@@ -124,6 +135,7 @@ function logValue(x, y) {
124135
}
125136

126137
// Set up the imports.
138+
var tempRet0;
127139
var imports = {
128140
'fuzzing-support': {
129141
'log-i32': logValue,
@@ -151,6 +163,24 @@ if (typeof WebAssembly.Tag !== 'undefined') {
151163
};
152164
}
153165

166+
// If a second binary will be linked in then set up the imports for
167+
// placeholders. Any import like (import "placeholder" "0" (func .. will be
168+
// provided by the secondary module, and must be called using an indirection.
169+
if (secondBinary) {
170+
imports['placeholder'] = new Proxy({}, {
171+
get(target, prop, receiver) {
172+
// Return a function that throws. We could do an indirect call using the
173+
// exported table, but as we immediately link in the secondary module,
174+
// these stubs will not be called (they are written to the table, and the
175+
// secondary module overwrites them). We do need to return something so
176+
// the primary module links without erroring, though.
177+
return () => {
178+
throw 'proxy stub should not be called';
179+
}
180+
}
181+
});
182+
}
183+
154184
// Create the wasm.
155185
var module = new WebAssembly.Module(binary);
156186

@@ -165,17 +195,32 @@ try {
165195
// Handle the exports.
166196
var exports = instance.exports;
167197

168-
var view;
198+
// Link in a second module, if one was provided.
199+
if (secondBinary) {
200+
var secondModule = new WebAssembly.Module(secondBinary);
169201

170-
// Recreate the view. This is important both initially and after a growth.
171-
function refreshView() {
172-
if (exports.memory) {
173-
view = new Int32Array(exports.memory.buffer);
202+
// The secondary module just needs to import the primary one: all original
203+
// imports it might have needed were exported from there.
204+
var secondImports = {'primary': exports};
205+
var secondInstance;
206+
try {
207+
secondInstance = new WebAssembly.Instance(secondModule, secondImports);
208+
} catch (e) {
209+
console.log('exception thrown: failed to instantiate second module');
210+
quit();
174211
}
175212
}
176213

177214
// Run the wasm.
178-
for (var e in exports) {
215+
if (!exportsToCall) {
216+
// We were not told specific exports, so call them all.
217+
exportsToCall = [];
218+
for (var e in exports) {
219+
exportsToCall.push(e);
220+
}
221+
}
222+
223+
for (var e of exportsToCall) {
179224
if (typeof exports[e] !== 'function') {
180225
continue;
181226
}

0 commit comments

Comments
 (0)