Skip to content

Commit 1f65c57

Browse files
authored
New fuzzer: PreserveImportsExportsJS (#8592)
This starts from wasm+js testcases and then modifies the wasm in a way that preserves imports and exports, so the wasm+js can still be run. This is very different from our usual approach of starting with only wasm, then bashing it into the shape that our general js code can handle. The main benefit here is testing of more interesting wasm+js interactions, specifically for the JS Interop proposal. Three wasm+js combinations are added in this PR that test features from that proposal.
1 parent ce7f869 commit 1f65c57

File tree

8 files changed

+765
-3
lines changed

8 files changed

+765
-3
lines changed

scripts/fuzz_opt.py

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import json
3232
import math
3333
import os
34+
import pathlib
3435
import random
3536
import re
3637
import shutil
@@ -2049,8 +2050,9 @@ def compare_to_merged_output(self, output, merged_output):
20492050
compare(output, merged_output, 'Two-Merged')
20502051

20512052

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):
20542056
frequency = 0.1
20552057

20562058
def handle(self, wasm):
@@ -2095,6 +2097,147 @@ def get_relevant_lines(wat):
20952097
compare(get_relevant_lines(original), get_relevant_lines(processed), 'Preserve')
20962098

20972099

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+
20982241
# Test that we preserve branch hints properly. The invariant that we test here
20992242
# is that, given correct branch hints (that is, the input wasm's branch hints
21002243
# are always correct: a branch is taken iff the hint is that it is taken), then
@@ -2322,7 +2465,8 @@ def handle(self, wasm):
23222465
RoundtripText(),
23232466
ClusterFuzz(),
23242467
Two(),
2325-
PreserveImportsExports(),
2468+
PreserveImportsExportsRandom(),
2469+
PreserveImportsExportsJS(),
23262470
BranchHintPreservation(),
23272471
]
23282472

scripts/test/fuzzing.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@
117117
'waitqueue.wast',
118118
# TODO: fix handling of the non-utf8 names here
119119
'name-high-bytes.wast',
120+
# JS interop testcases have complex js-wasm interactions
121+
'js_interop_counter.wat',
122+
'js_interop_cases.wat',
123+
'js_interop_corners.wat',
120124
]
121125

122126

test/js_wasm/js_interop_cases.mjs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
let protoFactory = new Proxy({}, {
2+
get(target, prop, receiver) {
3+
// Always return a fresh, empty object.
4+
return {};
5+
}
6+
});
7+
8+
let constructors = {};
9+
10+
let imports = {
11+
"protos": protoFactory,
12+
"env": { constructors },
13+
};
14+
15+
let compileOptions = { builtins: ["js-prototypes"] };
16+
17+
let buffer = readbuffer(arguments[0]);
18+
19+
let { module, instance } =
20+
await WebAssembly.instantiate(buffer, imports, compileOptions);
21+
22+
let Base = constructors.Base;
23+
let Derived = constructors.Derived;
24+
25+
// Test Base
26+
console.log("Testing Base...");
27+
let b = new Base(10);
28+
console.log("b.getValue():", b.getValue()); // 10
29+
console.log("b.value getter:", b.value); // 10
30+
b.value = 20;
31+
console.log("b.value after setter:", b.getValue()); // 20
32+
console.log("b instanceof Base:", b instanceof Base); // true
33+
console.log("b instanceof Derived:", b instanceof Derived); // false
34+
35+
// Test Derived
36+
console.log("\nTesting Derived...");
37+
let d = new Derived(100, 500);
38+
console.log("d.getValue() (inherited):", d.getValue()); // 100
39+
console.log("d.getExtra():", d.getExtra()); // 500
40+
console.log("d.value getter (inherited):", d.value); // 100
41+
d.value = 150;
42+
console.log("d.value after setter (inherited):", d.getValue()); // 150
43+
console.log("d instanceof Derived:", d instanceof Derived); // true
44+
console.log("d instanceof Base (inheritance):", d instanceof Base); // true
45+
console.log("Derived.staticMethod():", Derived.staticMethod()); // 42
46+
47+
// Test Wasm-side descriptor checks
48+
console.log("\nTesting Wasm-side descriptor checks...");
49+
console.log("checkDesc(b):", instance.exports.checkDesc(b)); // 1
50+
console.log("checkDesc(d):", instance.exports.checkDesc(d)); // 2
51+
console.log("isDerived(b):", instance.exports.isDerived(b)); // 0
52+
console.log("isDerived(d):", instance.exports.isDerived(d)); // 1
53+
54+
// Test cross-checks
55+
console.log("\nTesting cross-checks...");
56+
console.log("get_base_val(d):", instance.exports.get_base_val(d)); // 150

0 commit comments

Comments
 (0)