Skip to content

Commit 8070c92

Browse files
authored
Fuzzer: Merge and optimize even with closed world in Two() (#7963)
The key idea here is that we can take two separate modules, merge them, and then optimize in the most aggressive manner (even closed world) internally, as any optimization change after the merge is a bug. (We must still avoid optimizing the separate modules in closed world, which happens later in this fuzzer.) Compare the full merged output to the pre-merged, for maximal coverage. This requires fixing up export names after the merge, which is a little annoying, but seems worth it.
1 parent 0edd3cf commit 8070c92

File tree

1 file changed

+97
-10
lines changed

1 file changed

+97
-10
lines changed

scripts/fuzz_opt.py

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,9 +1783,9 @@ def ensure(self):
17831783
tar.close()
17841784

17851785

1786-
# Tests linking two wasm files at runtime, and that optimizations do not break
1787-
# anything. This is similar to Split(), but rather than split a wasm file into
1788-
# two and link them at runtime, this starts with two separate wasm files.
1786+
# Generates two wasm and tests interesting interactions between them. This is a
1787+
# little similar to Split(), but rather than split one wasm file into two and
1788+
# test that, we start with two.
17891789
#
17901790
# Fuzzing failures here is a little trickier, as there are two wasm files.
17911791
# You can reduce the primary file by finding the secondary one in the log
@@ -1812,7 +1812,7 @@ def ensure(self):
18121812
class Two(TestCaseHandler):
18131813
# Run at relatively high priority, as this is the main place we check cross-
18141814
# module interactions.
1815-
frequency = 1
1815+
frequency = 1 # TODO: We may want even higher priority here
18161816

18171817
def handle(self, wasm):
18181818
# Generate a second wasm file. (For fuzzing, we may be given one, but we
@@ -1865,9 +1865,50 @@ def handle(self, wasm):
18651865
print(f'warning: no calls in output. output:\n{output}')
18661866
assert calls_in_output == len(exports), exports
18671867

1868+
# Merge the files and run them that way. The result should be the same,
1869+
# even if we optimize. TODO: merge (no pun intended) the rest of Merge
1870+
# into here.
1871+
merged = abspath('merged.wasm')
1872+
run([in_bin('wasm-merge'), wasm, 'primary', second_wasm, 'secondary',
1873+
'-o', merged, '--rename-export-conflicts', '-all'])
1874+
1875+
# Usually also optimize the merged module. Optimizations are very
1876+
# interesting here, because after merging we can safely do even closed-
1877+
# world optimizations, making very aggressive changes that should still
1878+
# behave the same as before merging.
1879+
if random.random() < 0.8:
1880+
merged_opt = abspath('merged.opt.wasm')
1881+
opts = get_random_opts()
1882+
run([in_bin('wasm-opt'), merged, '-o', merged_opt, '-all'] + opts)
1883+
merged = merged_opt
1884+
1885+
if not wasm_notices_export_changes(merged):
1886+
# wasm-merge combines exports, which can alter their indexes and
1887+
# lead to noticeable differences if the wasm is sensitive to such
1888+
# things. We only compare the output if that is not an issue.
1889+
merged_output = run_bynterp(merged, args=['--fuzz-exec-before', '-all'])
1890+
1891+
if merged_output == IGNORE:
1892+
# The original output was ok, but after merging it becomes
1893+
# something we must ignore. This can happen when we optimize, if
1894+
# the optimizer reorders a normal trap (say a null exception)
1895+
# with a host limit trap (say an allocation limit). Nothing to
1896+
# do here, but verify we did optimize, as otherwise this is
1897+
# inexplicable.
1898+
assert merged == abspath('merged.opt.wasm')
1899+
else:
1900+
self.compare_to_merged_output(output, merged_output)
1901+
1902+
# The rest of the testing here depends on being to optimize the
1903+
# two modules independently, which closed-world can break.
1904+
if CLOSED_WORLD:
1905+
return
1906+
1907+
# Fix up the normal output for later comparisons.
18681908
output = fix_output(output)
18691909

1870-
# Optimize at least one of the two.
1910+
# We can optimize and compare the results. Optimize at least one of
1911+
# the two.
18711912
wasms = [wasm, second_wasm]
18721913
for i in range(random.randint(1, 2)):
18731914
wasm_index = random.randint(0, 1)
@@ -1881,7 +1922,7 @@ def handle(self, wasm):
18811922
optimized_output = run_bynterp(wasms[0], args=['--fuzz-exec-before', f'--fuzz-exec-second={wasms[1]}'])
18821923
optimized_output = fix_output(optimized_output)
18831924

1884-
compare(output, optimized_output, 'Two')
1925+
compare(output, optimized_output, 'Two-Opt')
18851926

18861927
# If we can, also test in V8. We also cannot compare if there are NaNs
18871928
# (as optimizations can lead to different outputs), and we must
@@ -1907,10 +1948,56 @@ def handle(self, wasm):
19071948

19081949
compare(output, optimized_output, 'Two-V8')
19091950

1910-
def can_run_on_wasm(self, wasm):
1911-
# We cannot optimize wasm files we are going to link in closed world
1912-
# mode.
1913-
return not CLOSED_WORLD
1951+
def compare_to_merged_output(self, output, merged_output):
1952+
# Comparing the original output from two files to the output after
1953+
# merging them is not trivial. First, remove the extra logging that
1954+
# --fuzz-exec-second adds.
1955+
output = output.replace('[fuzz-exec] running second module\n', '')
1956+
1957+
# Fix up both outputs.
1958+
output = fix_output(output)
1959+
merged_output = fix_output(merged_output)
1960+
1961+
# Finally, align the export names. We merged with
1962+
# --rename-export-conflicts, so that all exports remain exported,
1963+
# allowing a full comparison, but we do need to handle the different
1964+
# names. We do so by matching the export names in the logging.
1965+
output_lines = output.splitlines()
1966+
merged_output_lines = merged_output.splitlines()
1967+
1968+
if len(output_lines) != len(merged_output_lines):
1969+
# The line counts don't even match. Just compare them, which will
1970+
# emit a nice error for that.
1971+
compare(output, merged_output, 'Two-Counts')
1972+
assert False, 'we should have errored on the line counts'
1973+
1974+
for i in range(len(output_lines)):
1975+
a = output_lines[i]
1976+
b = merged_output_lines[i]
1977+
if a == b:
1978+
continue
1979+
if a.startswith(FUZZ_EXEC_CALL_PREFIX):
1980+
# Fix up
1981+
# [fuzz-exec] calling foo/bar
1982+
# for different foo/bar. Just copy the original.
1983+
assert b.startswith(FUZZ_EXEC_CALL_PREFIX)
1984+
merged_output_lines[i] = output_lines[i]
1985+
elif a.startswith(FUZZ_EXEC_NOTE_RESULT):
1986+
# Fix up
1987+
# [fuzz-exec] note result: foo/bar => 42
1988+
# for different foo/bar. We do not want to copy the result here,
1989+
# which might differ (that would be a bug we want to find).
1990+
assert b.startswith(FUZZ_EXEC_NOTE_RESULT)
1991+
assert a.count(' => ') == 1
1992+
assert b.count(' => ') == 1
1993+
a_prefix, a_result = a.split(' => ')
1994+
b_prefix, b_result = b.split(' => ')
1995+
# Copy a's prefix with b's result.
1996+
merged_output_lines[i] = a_prefix + ' => ' + b_result
1997+
1998+
merged_output = '\n'.join(merged_output_lines)
1999+
2000+
compare(output, merged_output, 'Two-Merged')
19142001

19152002

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

0 commit comments

Comments
 (0)