Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
048d6ea
go
kripken Apr 8, 2026
df8f132
undo
kripken Apr 8, 2026
b8bc054
work
kripken Apr 8, 2026
511ac05
undo
kripken Apr 8, 2026
ea9d16d
wrk
kripken Apr 8, 2026
35c3f89
wrk
kripken Apr 8, 2026
7afab71
go
kripken Apr 8, 2026
7487cc2
go
kripken Apr 8, 2026
affcb37
wrk
kripken Apr 8, 2026
d3100a8
wrk
kripken Apr 8, 2026
8c0b563
Revert "wrk"
kripken Apr 8, 2026
b2dd154
wrk
kripken Apr 8, 2026
8dc8547
wrk
kripken Apr 8, 2026
966f784
testing
kripken Apr 8, 2026
1c13ed0
moar
kripken Apr 8, 2026
2aa1b9a
testing
kripken Apr 8, 2026
9bb9056
testing
kripken Apr 8, 2026
d9a463c
testing
kripken Apr 9, 2026
89c9d7d
testing
kripken Apr 9, 2026
1236062
testing
kripken Apr 9, 2026
5b47101
testing
kripken Apr 9, 2026
1d79656
testing
kripken Apr 9, 2026
33013c3
testing
kripken Apr 9, 2026
d3f1186
Merge remote-tracking branch 'origin/main' into fuzz.preserve.js
kripken Apr 9, 2026
327e374
undo
kripken Apr 9, 2026
a2ee6fb
testing
kripken Apr 9, 2026
8065efe
Merge remote-tracking branch 'origin/main' into fuzz.preserve.js
kripken Apr 9, 2026
e13fba8
restore
kripken Apr 9, 2026
709eee7
clean
kripken Apr 9, 2026
fd2556b
clean
kripken Apr 9, 2026
bae026a
testing
kripken Apr 9, 2026
08d20be
verify
kripken Apr 10, 2026
f50bd44
fuzz
kripken Apr 10, 2026
345864c
fuzz
kripken Apr 10, 2026
ddd5098
rename
kripken Apr 10, 2026
da781b2
fix
kripken Apr 10, 2026
de3bc88
Merge remote-tracking branch 'origin/main' into fuzz.preserve.js
kripken Apr 10, 2026
83b302a
clean
kripken Apr 10, 2026
fcbcfe8
oops
kripken Apr 10, 2026
299d254
lint
kripken Apr 10, 2026
f11871c
sort imports
kripken Apr 10, 2026
0c1b9cd
ruff
kripken Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 147 additions & 3 deletions scripts/fuzz_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import json
import math
import os
import pathlib
import random
import re
import shutil
Expand Down Expand Up @@ -2049,8 +2050,9 @@ def compare_to_merged_output(self, output, merged_output):
compare(output, merged_output, 'Two-Merged')


# Test --fuzz-preserve-imports-exports, which never modifies imports or exports.
class PreserveImportsExports(TestCaseHandler):
# Test --fuzz-preserve-imports-exports on random inputs. This should never
# modify imports or exports.
class PreserveImportsExportsRandom(TestCaseHandler):
frequency = 0.1

def handle(self, wasm):
Expand Down Expand Up @@ -2095,6 +2097,147 @@ def get_relevant_lines(wat):
compare(get_relevant_lines(original), get_relevant_lines(processed), 'Preserve')


# Test --fuzz-preserve-imports-exports on a realistic js+wasm input. Unlike
# PreserveImportsExportsRandom which starts with a random file and modifies it,
# this starts with a fixed js+wasm testcase, known to work and to have
# interesting operations on the js/wasm boundary, and then randomly modifies
# the wasm. This simulates how an external fuzzer could use binaryen to modify
# its known-working testcases (parallel to how we test ClusterFuzz here).
#
# This reads wasm+js combinations from the test/js_wasm directory, so as new
# testcases are added there, this will fuzz them.
#
# Note that bugs found by this fuzzer require BINARYEN_TRUST_GIVEN_WASM=1 in the
# env for reduction. TODO: simplify this
class PreserveImportsExportsJS(TestCaseHandler):
frequency = 1
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do this less frequently given how few starting configurations it has?


def handle_pair(self, input, before_wasm, after_wasm, opts):
try:
self.do_handle_pair(input, before_wasm, after_wasm, opts)
except Exception as e:
if not os.environ.get('BINARYEN_TRUST_GIVEN_WASM'):
# We errored, and we were not given a wasm file to trust as we
# reduce, so this is the first time we hit an error. Save the
# pre wasm file, the one we began with, as `before_wasm`, so
# that the reducer will make us proceed exactly from there.
shutil.copyfile(self.pre_wasm, before_wasm)
raise e

def do_handle_pair(self, input, before_wasm, after_wasm, opts):
# Some of the time use a custom input. The normal inputs the fuzzer
# generates are in range INPUT_SIZE_MIN-INPUT_SIZE_MAX, which is good
# for new testcases, but the more changes we make to js+wasm testcases,
# the more chance we have to break things entirely (the js/wasm boundary
# is fragile). It is useful to also fuzz smaller sizes.
if random.random() < 0.25:
size = random.randint(0, INPUT_SIZE_MIN * 2)
make_random_input(size, input)

# Pick a js+wasm pair.
js_files = list(pathlib.Path(in_binaryen('test', 'js_wasm')).glob('*.mjs'))
js_file = str(random.choice(js_files))
print(f'js file: {js_file}')
wat_file = str(pathlib.Path(js_file).with_suffix('.wat'))

# Verify the wat works with our features
try:
run([in_bin('wasm-opt'), wat_file] + FEATURE_OPTS,
stderr=subprocess.PIPE,
silent=True)
except Exception:
note_ignored_vm_run('PreserveImportsExportsJS: features not compatible with js+wasm')
return

# Make sure the testcase runs by itself - there should be no invalid
# testcases.
original_wasm = 'orig.wasm'
run([in_bin('wasm-opt'), wat_file, '-o', original_wasm] + FEATURE_OPTS)
D8().run_js(js_file, original_wasm)

# Modify the initial wat to get the pre-optimizations wasm.
pre_wasm = abspath('pre.wasm')
run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [
'-ttf',
'--fuzz-preserve-imports-exports',
'--initial-fuzz=' + wat_file,
'-o', pre_wasm,
'-g',
])

# We successfully generated pre_wasm; stash it for possible reduction
# purposes later.
self.pre_wasm = pre_wasm

# If we were given a wasm file, use that instead of all the above. We
# do this now, after creating pre_wasm, because we still need to consume
# all the randomness normally.
if os.environ.get('BINARYEN_TRUST_GIVEN_WASM'):
print('using given wasm', before_wasm)
pre_wasm = before_wasm

# Pick a vm and run before we optimize the wasm.
vms = [
D8(),
D8Liftoff(),
D8Turboshaft(),
]
pre_vm = random.choice(vms)
pre = self.do_run(pre_vm, js_file, pre_wasm)

# Optimize.
post_wasm = abspath('post.wasm')
cmd = [in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + opts + FEATURE_OPTS
print(' '.join(cmd))
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode:
if 'Invalid configureAll' in proc.stderr:
# We have a hard error on unfamiliar configureAll patterns atm.
# Mutation of configureAll will easily break that pattern, so we
# must ignore such cases.
note_ignored_vm_run('PreserveImportsExportsJS: bad configureAll')
return

# Anything else is a problem.
print(proc.stderr)
raise Exception('opts failed')

# Run after opts, in a random vm.
post_vm = random.choice(vms)
post = self.do_run(post_vm, js_file, post_wasm)

# Compare
compare(pre, post, 'PreserveImportsExportsJS')

def do_run(self, vm, js, wasm):
out = vm.run_js(js, wasm, checked=False)

cleaned = []
for line in out.splitlines():
if 'RuntimeError:' in line or 'TypeError:' in line:
# This is part of an error like
#
# wasm-function[2]:0x273: RuntimeError: unreachable
#
# We must ignore the binary location, which opts can change. We
# must also remove the specific trap, as Binaryen can change
# that.
line = 'TRAP'
elif 'wasm://' in line or '(<anonymous>)' in line:
# This is part of a stack trace like
#
# at wasm://wasm/12345678:wasm-function[42]:0x123
# at (<anonymous>)
#
# Ignore it, as traces differ based on optimizations.
continue
cleaned.append(line)
return '\n'.join(cleaned)

def can_run_on_wasm(self, wasm):
return all_disallowed(DISALLOWED_FEATURES_IN_V8)


# Test that we preserve branch hints properly. The invariant that we test here
# is that, given correct branch hints (that is, the input wasm's branch hints
# are always correct: a branch is taken iff the hint is that it is taken), then
Expand Down Expand Up @@ -2322,7 +2465,8 @@ def handle(self, wasm):
RoundtripText(),
ClusterFuzz(),
Two(),
PreserveImportsExports(),
PreserveImportsExportsRandom(),
PreserveImportsExportsJS(),
BranchHintPreservation(),
]

Expand Down
4 changes: 4 additions & 0 deletions scripts/test/fuzzing.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@
'waitqueue.wast',
# TODO: fix handling of the non-utf8 names here
'name-high-bytes.wast',
# JS interop testcases have complex js-wasm interactions
'js_interop_counter.wat',
'js_interop_cases.wat',
'js_interop_corners.wat',
]


Expand Down
56 changes: 56 additions & 0 deletions test/js_wasm/js_interop_cases.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
let protoFactory = new Proxy({}, {
get(target, prop, receiver) {
// Always return a fresh, empty object.
return {};
}
});

let constructors = {};

let imports = {
"protos": protoFactory,
"env": { constructors },
};

let compileOptions = { builtins: ["js-prototypes"] };

let buffer = readbuffer(arguments[0]);

let { module, instance } =
await WebAssembly.instantiate(buffer, imports, compileOptions);

let Base = constructors.Base;
let Derived = constructors.Derived;

// Test Base
console.log("Testing Base...");
let b = new Base(10);
console.log("b.getValue():", b.getValue()); // 10
console.log("b.value getter:", b.value); // 10
b.value = 20;
console.log("b.value after setter:", b.getValue()); // 20
console.log("b instanceof Base:", b instanceof Base); // true
console.log("b instanceof Derived:", b instanceof Derived); // false

// Test Derived
console.log("\nTesting Derived...");
let d = new Derived(100, 500);
console.log("d.getValue() (inherited):", d.getValue()); // 100
console.log("d.getExtra():", d.getExtra()); // 500
console.log("d.value getter (inherited):", d.value); // 100
d.value = 150;
console.log("d.value after setter (inherited):", d.getValue()); // 150
console.log("d instanceof Derived:", d instanceof Derived); // true
console.log("d instanceof Base (inheritance):", d instanceof Base); // true
console.log("Derived.staticMethod():", Derived.staticMethod()); // 42

// Test Wasm-side descriptor checks
console.log("\nTesting Wasm-side descriptor checks...");
console.log("checkDesc(b):", instance.exports.checkDesc(b)); // 1
console.log("checkDesc(d):", instance.exports.checkDesc(d)); // 2
console.log("isDerived(b):", instance.exports.isDerived(b)); // 0
console.log("isDerived(d):", instance.exports.isDerived(d)); // 1

// Test cross-checks
console.log("\nTesting cross-checks...");
console.log("get_base_val(d):", instance.exports.get_base_val(d)); // 150
Loading
Loading