Skip to content

Commit 2f6f42c

Browse files
authored
Fuzz combinations of two modules (#7144)
The new Two fuzz testcase handler generates two wasm files and then runs them in V8, linking them using JS. It then optimizes at least one of the two and runs it again, and checks for differences. This is similar to the Split fuzzer for wasm-split, but the two wasm files are arbitrary and not the result of splitting. Also lower CtorEval priority a little. It is not very important.
1 parent 725f76d commit 2f6f42c

File tree

1 file changed

+80
-2
lines changed

1 file changed

+80
-2
lines changed

scripts/fuzz_opt.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,7 +1360,7 @@ def can_run_on_wasm(self, wasm):
13601360

13611361
# Tests wasm-ctor-eval
13621362
class CtorEval(TestCaseHandler):
1363-
frequency = 0.2
1363+
frequency = 0.1
13641364

13651365
def handle(self, wasm):
13661366
# get the expected execution results.
@@ -1477,7 +1477,9 @@ def can_run_on_wasm(self, wasm):
14771477
FUNC_NAMES_REGEX = re.compile(r'\n [(]func [$](\S+)')
14781478

14791479

1480-
# Tests wasm-split
1480+
# Tests wasm-split. This also tests that fuzz_shell.js properly executes 2 wasm
1481+
# files, which adds coverage for ClusterFuzz (which sometimes runs two wasm
1482+
# files in that way).
14811483
class Split(TestCaseHandler):
14821484
frequency = 1 # TODO: adjust lower when we actually enable this
14831485

@@ -1670,6 +1672,78 @@ def ensure(self):
16701672
tar.close()
16711673

16721674

1675+
# Tests linking two wasm files at runtime, and that optimizations do not break
1676+
# anything. This is similar to Split(), but rather than split a wasm file into
1677+
# two and link them at runtime, this starts with two separate wasm files.
1678+
class Two(TestCaseHandler):
1679+
frequency = 0.2
1680+
1681+
def handle(self, wasm):
1682+
# Generate a second wasm file, unless we were given one (useful during
1683+
# reduction).
1684+
second_wasm = abspath('second.wasm')
1685+
given = os.environ.get('BINARYEN_SECOND_WASM')
1686+
if given:
1687+
# TODO: should we de-nan this etc. as with the primary?
1688+
shutil.copyfile(given, second_wasm)
1689+
else:
1690+
second_input = abspath('second_input.dat')
1691+
make_random_input(random_size(), second_input)
1692+
args = [second_input, '-ttf', '-o', second_wasm]
1693+
run([in_bin('wasm-opt')] + args + GEN_ARGS + FEATURE_OPTS)
1694+
1695+
# The binaryen interpreter only supports a single file, so we run them
1696+
# from JS using fuzz_shell.js's support for two files.
1697+
#
1698+
# Note that we *cannot* run each wasm file separately and compare those
1699+
# to the combined output, as fuzz_shell.js intentionally allows calls
1700+
# *between* the wasm files, through JS APIs like call-export*. So all we
1701+
# do here is see the combined, linked behavior, and then later below we
1702+
# see that that behavior remains even after optimizations.
1703+
output = run_d8_wasm(wasm, args=[second_wasm])
1704+
1705+
if output == IGNORE:
1706+
# There is no point to continue since we can't compare this output
1707+
# to anything.
1708+
return
1709+
1710+
if output.strip() == 'exception thrown: failed to instantiate module':
1711+
# We may fail to instantiate the modules for valid reasons, such as
1712+
# an active segment being out of bounds. There is no point to
1713+
# continue in such cases, as no exports are called.
1714+
return
1715+
1716+
# Make sure that fuzz_shell.js actually executed all exports from both
1717+
# wasm files.
1718+
exports = get_exports(wasm, ['func']) + get_exports(second_wasm, ['func'])
1719+
assert output.count(FUZZ_EXEC_CALL_PREFIX) == len(exports)
1720+
1721+
output = fix_output(output)
1722+
1723+
# Optimize at least one of the two.
1724+
wasms = [wasm, second_wasm]
1725+
for i in range(random.randint(1, 2)):
1726+
wasm_index = random.randint(0, 1)
1727+
name = wasms[wasm_index]
1728+
new_name = name + f'.opt{i}.wasm'
1729+
opts = get_random_opts()
1730+
run([in_bin('wasm-opt'), name, '-o', new_name] + opts + FEATURE_OPTS)
1731+
wasms[wasm_index] = new_name
1732+
1733+
# Run again, and compare the output
1734+
optimized_output = run_d8_wasm(wasms[0], args=[wasms[1]])
1735+
optimized_output = fix_output(optimized_output)
1736+
1737+
compare(output, optimized_output, 'Two')
1738+
1739+
def can_run_on_wasm(self, wasm):
1740+
# We cannot optimize wasm files we are going to link in closed world
1741+
# mode. We also cannot run shared-everything code in d8 yet. We also
1742+
# cannot compare if there are NaNs (as optimizations can lead to
1743+
# different outputs).
1744+
return not CLOSED_WORLD and all_disallowed(['shared-everything']) and not NANS
1745+
1746+
16731747
# The global list of all test case handlers
16741748
testcase_handlers = [
16751749
FuzzExec(),
@@ -1683,6 +1757,7 @@ def ensure(self):
16831757
# Split(),
16841758
RoundtripText(),
16851759
ClusterFuzz(),
1760+
Two(),
16861761
]
16871762

16881763

@@ -2136,6 +2211,9 @@ def get_random_opts():
21362211
# bash %(reduce_sh)s
21372212
#
21382213
# You may also need to add --timeout 5 or such if the testcase is a slow one.
2214+
#
2215+
# If the testcase handler uses a second wasm file, you may be able to reduce it
2216+
# using BINARYEN_SECOND_WASM.
21392217
#
21402218
''' % {'wasm_opt': in_bin('wasm-opt'),
21412219
'bin': shared.options.binaryen_bin,

0 commit comments

Comments
 (0)