Skip to content

Commit 2fc0797

Browse files
authored
Fuzzer: Run Two() using the Binaryen interpreter (#7954)
Using our interpreter in addition to V8 allows our assertions to also be checked, potentially finding more issues. Also, we can run our interpreter on more code (e.g. V8 comparisons are unreliable if there are NANs, due to V8's nondeterminism differing from ours). To make this work, fix execution-results to match fuzz_shell.js's behavior: First instantiate both modules when running two of them, and then run their exports (this makes it easy to see if we error during instantiation).
1 parent 7a94261 commit 2fc0797

2 files changed

Lines changed: 72 additions & 48 deletions

File tree

scripts/fuzz_opt.py

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,7 +1667,7 @@ def handle(self, wasm):
16671667
run([in_bin('wasm-opt'), abspath('a.wast')] + FEATURE_OPTS)
16681668

16691669

1670-
# The error shown when a module fails to instantiate.
1670+
# The error shown in V8 when a module fails to instantiate.
16711671
INSTANTIATE_ERROR = 'exception thrown: failed to instantiate module'
16721672

16731673

@@ -1787,7 +1787,9 @@ def ensure(self):
17871787
# anything. This is similar to Split(), but rather than split a wasm file into
17881788
# two and link them at runtime, this starts with two separate wasm files.
17891789
class Two(TestCaseHandler):
1790-
frequency = 0.2
1790+
# Run at relatively high priority, as this is the main place we check cross-
1791+
# module interactions.
1792+
frequency = 1
17911793

17921794
def handle(self, wasm):
17931795
# Generate a second wasm file, unless we were given one (useful during
@@ -1798,56 +1800,48 @@ def handle(self, wasm):
17981800
# TODO: should we de-nan this etc. as with the primary?
17991801
shutil.copyfile(given, second_wasm)
18001802
else:
1803+
# generate a second wasm file to merge. pick a smaller size when
1804+
# the main wasm file is smaller, so reduction shrinks this too.
1805+
wasm_size = os.stat(wasm).st_size
1806+
second_size = min(wasm_size, random_size())
1807+
18011808
second_input = abspath('second_input.dat')
1802-
make_random_input(random_size(), second_input)
1809+
make_random_input(second_size, second_input)
18031810
args = [second_input, '-ttf', '-o', second_wasm]
18041811
# Most of the time, use the first wasm as an import to the second.
18051812
if random.random() < 0.8:
18061813
args += ['--fuzz-import=' + wasm]
18071814
run([in_bin('wasm-opt')] + args + GEN_ARGS + FEATURE_OPTS)
18081815

1809-
# The binaryen interpreter only supports a single file, so we run them
1810-
# from JS using fuzz_shell.js's support for two files.
1816+
# Run the wasm.
18111817
#
18121818
# Note that we *cannot* run each wasm file separately and compare those
18131819
# to the combined output, as fuzz_shell.js intentionally allows calls
18141820
# *between* the wasm files, through JS APIs like call-export*. So all we
18151821
# do here is see the combined, linked behavior, and then later below we
18161822
# see that that behavior remains even after optimizations.
1817-
output = run_d8_wasm(wasm, args=[second_wasm])
1818-
1819-
if output == IGNORE:
1820-
# There is no point to continue since we can't compare this output
1821-
# to anything.
1822-
return
1823+
output = run_bynterp(wasm, args=['--fuzz-exec-before', f'--fuzz-exec-second={second_wasm}'])
18231824

1824-
if output.startswith(INSTANTIATE_ERROR):
1825+
# Check if we trapped during instantiation.
1826+
if traps_in_instantiation(output):
18251827
# We may fail to instantiate the modules for valid reasons, such as
18261828
# an active segment being out of bounds. There is no point to
18271829
# continue in such cases, as no exports are called.
1828-
1829-
# But, check 'primary' is not in the V8 error. That might indicate a
1830-
# problem in the imports of --fuzz-import. To do this, run the d8
1831-
# command directly, without the usual filtering of run_d8_wasm.
1832-
cmd = [shared.V8] + shared.V8_OPTS + get_v8_extra_flags() + [
1833-
get_fuzz_shell_js(),
1834-
'--',
1835-
wasm,
1836-
second_wasm
1837-
]
1838-
out = run(cmd)
1839-
assert '"primary"' not in out, out
1840-
18411830
note_ignored_vm_run('Two instantiate error')
18421831
return
18431832

1844-
# Make sure that fuzz_shell.js actually executed all exports from both
1833+
if output == IGNORE:
1834+
# There is no point to continue since we can't compare this output
1835+
# to anything.
1836+
return
1837+
1838+
# Make sure that we actually executed all exports from both
18451839
# wasm files.
18461840
exports = get_exports(wasm, ['func']) + get_exports(second_wasm, ['func'])
18471841
calls_in_output = output.count(FUZZ_EXEC_CALL_PREFIX)
18481842
if calls_in_output == 0:
18491843
print(f'warning: no calls in output. output:\n{output}')
1850-
assert calls_in_output == len(exports)
1844+
assert calls_in_output == len(exports), exports
18511845

18521846
output = fix_output(output)
18531847

@@ -1862,22 +1856,39 @@ def handle(self, wasm):
18621856
wasms[wasm_index] = new_name
18631857

18641858
# Run again, and compare the output
1865-
optimized_output = run_d8_wasm(wasms[0], args=[wasms[1]])
1859+
optimized_output = run_bynterp(wasms[0], args=['--fuzz-exec-before', f'--fuzz-exec-second={wasms[1]}'])
18661860
optimized_output = fix_output(optimized_output)
18671861

18681862
compare(output, optimized_output, 'Two')
18691863

1864+
# If we can, also test in V8. We also cannot compare if there are NaNs
1865+
# (as optimizations can lead to different outputs), and we must
1866+
# disallow some features.
1867+
# TODO: relax some of these
1868+
if NANS or not all_disallowed(['shared-everything', 'strings', 'stack-switching']):
1869+
return
1870+
1871+
output = run_d8_wasm(wasm, args=[second_wasm])
1872+
1873+
if output == IGNORE:
1874+
return
1875+
1876+
# We ruled out things we must ignore, like host limitations, and also
1877+
# exited earlier on a deterministic instantiation error, so there should
1878+
# be no such error in V8.
1879+
assert not output.startswith(INSTANTIATE_ERROR)
1880+
1881+
output = fix_output(output)
1882+
1883+
optimized_output = run_d8_wasm(wasms[0], args=[wasms[1]])
1884+
optimized_output = fix_output(optimized_output)
1885+
1886+
compare(output, optimized_output, 'Two-V8')
1887+
18701888
def can_run_on_wasm(self, wasm):
18711889
# We cannot optimize wasm files we are going to link in closed world
1872-
# mode. We also cannot run shared-everything code in d8 yet. We also
1873-
# cannot compare if there are NaNs (as optimizations can lead to
1874-
# different outputs).
1875-
# TODO: relax some of these
1876-
if CLOSED_WORLD:
1877-
return False
1878-
if NANS:
1879-
return False
1880-
return all_disallowed(['shared-everything', 'strings', 'stack-switching'])
1890+
# mode.
1891+
return not CLOSED_WORLD
18811892

18821893

18831894
# Test --fuzz-preserve-imports-exports, which never modifies imports or exports.

src/tools/execution-results.h

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -306,20 +306,31 @@ struct ExecutionResults {
306306
// link with it (like fuzz_shell's second module).
307307
void get(Module& wasm, Module* second = nullptr) {
308308
try {
309-
// Run the first module.
309+
// Instantiate the first module.
310310
LoggingExternalInterface interface(loggings, wasm);
311311
auto instance = std::make_shared<ModuleRunner>(wasm, &interface);
312-
runModule(wasm, *instance, interface);
312+
instantiate(*instance, interface);
313313

314+
// Instantiate the second, if there is one (we instantiate both before
315+
// running anything, so that we match the behavior of fuzz_shell.js).
316+
std::map<Name, std::shared_ptr<ModuleRunner>> linkedInstances;
317+
std::unique_ptr<LoggingExternalInterface> secondInterface;
318+
std::shared_ptr<ModuleRunner> secondInstance;
314319
if (second) {
315-
// Link and run the second module.
316-
std::map<Name, std::shared_ptr<ModuleRunner>> linkedInstances;
320+
// Link and instantiate the second module.
317321
linkedInstances["primary"] = instance;
318-
LoggingExternalInterface secondInterface(
322+
secondInterface = std::make_unique<LoggingExternalInterface>(
319323
loggings, *second, linkedInstances);
320-
auto secondInstance = std::make_shared<ModuleRunner>(
321-
*second, &secondInterface, linkedInstances);
322-
runModule(*second, *secondInstance, secondInterface);
324+
secondInstance = std::make_shared<ModuleRunner>(
325+
*second, secondInterface.get(), linkedInstances);
326+
instantiate(*secondInstance, *secondInterface);
327+
}
328+
329+
// Run.
330+
callExports(wasm, *instance);
331+
if (second) {
332+
std::cout << "[fuzz-exec] running second module\n";
333+
callExports(*second, *secondInstance);
323334
}
324335
} catch (const TrapException&) {
325336
// May throw in instance creation (init of offsets).
@@ -331,14 +342,16 @@ struct ExecutionResults {
331342
}
332343
}
333344

334-
void runModule(Module& wasm,
335-
ModuleRunner& instance,
336-
LoggingExternalInterface& interface) {
345+
void instantiate(ModuleRunner& instance,
346+
LoggingExternalInterface& interface) {
337347
// This is not an optimization: we want to execute anything, even relaxed
338348
// SIMD instructions.
339349
instance.setRelaxedBehavior(ModuleRunner::RelaxedBehavior::Execute);
340350
instance.instantiate();
341351
interface.setModuleRunner(&instance);
352+
}
353+
354+
void callExports(Module& wasm, ModuleRunner& instance) {
342355
// execute all exported methods (that are therefore preserved through
343356
// opts)
344357
for (auto& exp : wasm.exports) {

0 commit comments

Comments
 (0)