|
31 | 31 | import json |
32 | 32 | import math |
33 | 33 | import os |
| 34 | +import pathlib |
34 | 35 | import random |
35 | 36 | import re |
36 | 37 | import shutil |
@@ -2049,8 +2050,9 @@ def compare_to_merged_output(self, output, merged_output): |
2049 | 2050 | compare(output, merged_output, 'Two-Merged') |
2050 | 2051 |
|
2051 | 2052 |
|
2052 | | -# Test --fuzz-preserve-imports-exports, which never modifies imports or exports. |
2053 | | -class PreserveImportsExports(TestCaseHandler): |
| 2053 | +# Test --fuzz-preserve-imports-exports on random inputs. This should never |
| 2054 | +# modify imports or exports. |
| 2055 | +class PreserveImportsExportsRandom(TestCaseHandler): |
2054 | 2056 | frequency = 0.1 |
2055 | 2057 |
|
2056 | 2058 | def handle(self, wasm): |
@@ -2095,6 +2097,147 @@ def get_relevant_lines(wat): |
2095 | 2097 | compare(get_relevant_lines(original), get_relevant_lines(processed), 'Preserve') |
2096 | 2098 |
|
2097 | 2099 |
|
| 2100 | +# Test --fuzz-preserve-imports-exports on a realistic js+wasm input. Unlike |
| 2101 | +# PreserveImportsExportsRandom which starts with a random file and modifies it, |
| 2102 | +# this starts with a fixed js+wasm testcase, known to work and to have |
| 2103 | +# interesting operations on the js/wasm boundary, and then randomly modifies |
| 2104 | +# the wasm. This simulates how an external fuzzer could use binaryen to modify |
| 2105 | +# its known-working testcases (parallel to how we test ClusterFuzz here). |
| 2106 | +# |
| 2107 | +# This reads wasm+js combinations from the test/js_wasm directory, so as new |
| 2108 | +# testcases are added there, this will fuzz them. |
| 2109 | +# |
| 2110 | +# Note that bugs found by this fuzzer require BINARYEN_TRUST_GIVEN_WASM=1 in the |
| 2111 | +# env for reduction. TODO: simplify this |
| 2112 | +class PreserveImportsExportsJS(TestCaseHandler): |
| 2113 | + frequency = 1 |
| 2114 | + |
| 2115 | + def handle_pair(self, input, before_wasm, after_wasm, opts): |
| 2116 | + try: |
| 2117 | + self.do_handle_pair(input, before_wasm, after_wasm, opts) |
| 2118 | + except Exception as e: |
| 2119 | + if not os.environ.get('BINARYEN_TRUST_GIVEN_WASM'): |
| 2120 | + # We errored, and we were not given a wasm file to trust as we |
| 2121 | + # reduce, so this is the first time we hit an error. Save the |
| 2122 | + # pre wasm file, the one we began with, as `before_wasm`, so |
| 2123 | + # that the reducer will make us proceed exactly from there. |
| 2124 | + shutil.copyfile(self.pre_wasm, before_wasm) |
| 2125 | + raise e |
| 2126 | + |
| 2127 | + def do_handle_pair(self, input, before_wasm, after_wasm, opts): |
| 2128 | + # Some of the time use a custom input. The normal inputs the fuzzer |
| 2129 | + # generates are in range INPUT_SIZE_MIN-INPUT_SIZE_MAX, which is good |
| 2130 | + # for new testcases, but the more changes we make to js+wasm testcases, |
| 2131 | + # the more chance we have to break things entirely (the js/wasm boundary |
| 2132 | + # is fragile). It is useful to also fuzz smaller sizes. |
| 2133 | + if random.random() < 0.25: |
| 2134 | + size = random.randint(0, INPUT_SIZE_MIN * 2) |
| 2135 | + make_random_input(size, input) |
| 2136 | + |
| 2137 | + # Pick a js+wasm pair. |
| 2138 | + js_files = list(pathlib.Path(in_binaryen('test', 'js_wasm')).glob('*.mjs')) |
| 2139 | + js_file = str(random.choice(js_files)) |
| 2140 | + print(f'js file: {js_file}') |
| 2141 | + wat_file = str(pathlib.Path(js_file).with_suffix('.wat')) |
| 2142 | + |
| 2143 | + # Verify the wat works with our features |
| 2144 | + try: |
| 2145 | + run([in_bin('wasm-opt'), wat_file] + FEATURE_OPTS, |
| 2146 | + stderr=subprocess.PIPE, |
| 2147 | + silent=True) |
| 2148 | + except Exception: |
| 2149 | + note_ignored_vm_run('PreserveImportsExportsJS: features not compatible with js+wasm') |
| 2150 | + return |
| 2151 | + |
| 2152 | + # Make sure the testcase runs by itself - there should be no invalid |
| 2153 | + # testcases. |
| 2154 | + original_wasm = 'orig.wasm' |
| 2155 | + run([in_bin('wasm-opt'), wat_file, '-o', original_wasm] + FEATURE_OPTS) |
| 2156 | + D8().run_js(js_file, original_wasm) |
| 2157 | + |
| 2158 | + # Modify the initial wat to get the pre-optimizations wasm. |
| 2159 | + pre_wasm = abspath('pre.wasm') |
| 2160 | + run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [ |
| 2161 | + '-ttf', |
| 2162 | + '--fuzz-preserve-imports-exports', |
| 2163 | + '--initial-fuzz=' + wat_file, |
| 2164 | + '-o', pre_wasm, |
| 2165 | + '-g', |
| 2166 | + ]) |
| 2167 | + |
| 2168 | + # We successfully generated pre_wasm; stash it for possible reduction |
| 2169 | + # purposes later. |
| 2170 | + self.pre_wasm = pre_wasm |
| 2171 | + |
| 2172 | + # If we were given a wasm file, use that instead of all the above. We |
| 2173 | + # do this now, after creating pre_wasm, because we still need to consume |
| 2174 | + # all the randomness normally. |
| 2175 | + if os.environ.get('BINARYEN_TRUST_GIVEN_WASM'): |
| 2176 | + print('using given wasm', before_wasm) |
| 2177 | + pre_wasm = before_wasm |
| 2178 | + |
| 2179 | + # Pick a vm and run before we optimize the wasm. |
| 2180 | + vms = [ |
| 2181 | + D8(), |
| 2182 | + D8Liftoff(), |
| 2183 | + D8Turboshaft(), |
| 2184 | + ] |
| 2185 | + pre_vm = random.choice(vms) |
| 2186 | + pre = self.do_run(pre_vm, js_file, pre_wasm) |
| 2187 | + |
| 2188 | + # Optimize. |
| 2189 | + post_wasm = abspath('post.wasm') |
| 2190 | + cmd = [in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + opts + FEATURE_OPTS |
| 2191 | + print(' '.join(cmd)) |
| 2192 | + proc = subprocess.run(cmd, capture_output=True, text=True) |
| 2193 | + if proc.returncode: |
| 2194 | + if 'Invalid configureAll' in proc.stderr: |
| 2195 | + # We have a hard error on unfamiliar configureAll patterns atm. |
| 2196 | + # Mutation of configureAll will easily break that pattern, so we |
| 2197 | + # must ignore such cases. |
| 2198 | + note_ignored_vm_run('PreserveImportsExportsJS: bad configureAll') |
| 2199 | + return |
| 2200 | + |
| 2201 | + # Anything else is a problem. |
| 2202 | + print(proc.stderr) |
| 2203 | + raise Exception('opts failed') |
| 2204 | + |
| 2205 | + # Run after opts, in a random vm. |
| 2206 | + post_vm = random.choice(vms) |
| 2207 | + post = self.do_run(post_vm, js_file, post_wasm) |
| 2208 | + |
| 2209 | + # Compare |
| 2210 | + compare(pre, post, 'PreserveImportsExportsJS') |
| 2211 | + |
| 2212 | + def do_run(self, vm, js, wasm): |
| 2213 | + out = vm.run_js(js, wasm, checked=False) |
| 2214 | + |
| 2215 | + cleaned = [] |
| 2216 | + for line in out.splitlines(): |
| 2217 | + if 'RuntimeError:' in line or 'TypeError:' in line: |
| 2218 | + # This is part of an error like |
| 2219 | + # |
| 2220 | + # wasm-function[2]:0x273: RuntimeError: unreachable |
| 2221 | + # |
| 2222 | + # We must ignore the binary location, which opts can change. We |
| 2223 | + # must also remove the specific trap, as Binaryen can change |
| 2224 | + # that. |
| 2225 | + line = 'TRAP' |
| 2226 | + elif 'wasm://' in line or '(<anonymous>)' in line: |
| 2227 | + # This is part of a stack trace like |
| 2228 | + # |
| 2229 | + # at wasm://wasm/12345678:wasm-function[42]:0x123 |
| 2230 | + # at (<anonymous>) |
| 2231 | + # |
| 2232 | + # Ignore it, as traces differ based on optimizations. |
| 2233 | + continue |
| 2234 | + cleaned.append(line) |
| 2235 | + return '\n'.join(cleaned) |
| 2236 | + |
| 2237 | + def can_run_on_wasm(self, wasm): |
| 2238 | + return all_disallowed(DISALLOWED_FEATURES_IN_V8) |
| 2239 | + |
| 2240 | + |
2098 | 2241 | # Test that we preserve branch hints properly. The invariant that we test here |
2099 | 2242 | # is that, given correct branch hints (that is, the input wasm's branch hints |
2100 | 2243 | # are always correct: a branch is taken iff the hint is that it is taken), then |
@@ -2322,7 +2465,8 @@ def handle(self, wasm): |
2322 | 2465 | RoundtripText(), |
2323 | 2466 | ClusterFuzz(), |
2324 | 2467 | Two(), |
2325 | | - PreserveImportsExports(), |
| 2468 | + PreserveImportsExportsRandom(), |
| 2469 | + PreserveImportsExportsJS(), |
2326 | 2470 | BranchHintPreservation(), |
2327 | 2471 | ] |
2328 | 2472 |
|
|
0 commit comments