From 048d6eaf43cab6c15f4b09e71b44a94db6645c06 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 09:47:17 -0700 Subject: [PATCH 01/39] go --- scripts/fuzz_opt.py | 409 ++++++++++++++++++++++++-------------------- 1 file changed, 226 insertions(+), 183 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 784476d89c2..6bb7b8b2d64 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -28,6 +28,7 @@ import contextlib import difflib +import pathlib import json import math import os @@ -783,180 +784,183 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): run([in_bin('wasm-opt'), before_wasm] + opts + ['--fuzz-exec']) +# VMs + +class BinaryenInterpreter: + name = 'binaryen interpreter' + + def run(self, wasm): + output = run_bynterp(wasm, ['--fuzz-exec-before']) + if output != IGNORE: + calls = output.count(FUZZ_EXEC_EXPORT_PREFIX) + errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX) + if errors > calls / 2: + # A significant amount of execution on this testcase + # simply trapped, and was not very useful, so mark it + # as ignored. Ideally the fuzzer testcases would be + # improved to reduce this number. + # + # Note that we don't change output=IGNORE as there may + # still be useful testing here (up to 50%), so we only + # note that this is a mostly-ignored run, but we do not + # ignore the parts that are useful. + # + # Note that we set amount to 0.5 because we are run both + # on the before wasm and the after wasm. Those will be + # in sync (because the optimizer does not remove traps) + # and so by setting 0.5 we only increment by 1 for the + # entire iteration. + note_ignored_vm_run('too many errors vs calls', + extra_text=f' ({calls} calls, {errors} errors)', + amount=0.5) + return output + + def can_run(self, wasm): + return True + + def can_compare_to_self(self): + return True + + def can_compare_to_other(self, other): + return True + +class D8: + name = 'd8' + + def run(self, wasm, extra_d8_flags=[]): + return run_vm([shared.V8, get_fuzz_shell_js()] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) + + def can_run(self, wasm): + return all_disallowed(DISALLOWED_FEATURES_IN_V8) + + def can_compare_to_self(self): + # With nans, VM differences can confuse us, so only very simple VMs + # can compare to themselves after opts in that case. + return not NANS + + def can_compare_to_other(self, other): + # Relaxed SIMD allows different behavior between VMs, so only + # allow comparisons to other d8 variants if it is enabled. + if not all_disallowed(['relaxed-simd']) and not other.name.startswith('d8'): + return False + + # If not legalized, the JS will fail immediately, so no point to + # compare to others. + return self.can_compare_to_self() and LEGALIZE + +class D8Liftoff(D8): + name = 'd8_liftoff' + + def run(self, wasm): + return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) + +class D8Turboshaft(D8): + name = 'd8_turboshaft' + + def run(self, wasm): + flags = ['--no-liftoff'] + if random.random() < 0.5: + flags += ['--no-wasm-generic-wrapper'] + return super().run(wasm, extra_d8_flags=flags) + +class Wasm2C: + name = 'wasm2c' + + def __init__(self): + # look for wabt in the path. if it's not here, don't run wasm2c + try: + wabt_bin = shared.which('wasm2c') + wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) + self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') + if not os.path.isdir(self.wasm2c_dir): + print('wabt found, but not wasm2c support dir') + self.wasm2c_dir = None + except Exception as e: + print('warning: no wabt found:', e) + self.wasm2c_dir = None + + def can_run(self, wasm): + if self.wasm2c_dir is None: + return False + # if we legalize for JS, the ABI is not what C wants + if LEGALIZE: + return False + # relatively slow, so run it less frequently + if random.random() < 0.5: + return False + # wasm2c doesn't support most features + return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics']) + + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] + run(compile_cmd) + return run_vm(['./a.out']) + + def can_compare_to_self(self): + # The binaryen optimizer changes NaNs in the ways that wasm + # expects, but that's not quite what C has + return not NANS + + def can_compare_to_other(self, other): + # C won't trap on OOB, and NaNs can differ from wasm VMs + return not OOB and not NANS + +class Wasm2C2Wasm(Wasm2C): + name = 'wasm2c2wasm' + + def __init__(self): + super().__init__() + + self.has_emcc = shared.which('emcc') is not None + + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['emcc', 'main.c', 'wasm.c', + os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), + '-I' + self.wasm2c_dir, + '-lm', + '-s', 'ENVIRONMENT=shell', + '-s', 'ALLOW_MEMORY_GROWTH'] + # disable the signal handler: emcc looks like unix, but wasm has + # no signals + compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0'] + if random.random() < 0.5: + compile_cmd += ['-O' + str(random.randint(1, 3))] + elif random.random() < 0.5: + if random.random() < 0.5: + compile_cmd += ['-Os'] + else: + compile_cmd += ['-Oz'] + # avoid pass-debug on the emcc invocation itself (which runs + # binaryen to optimize the wasm), as the wasm here can be very + # large and it isn't what we are focused on testing here + with no_pass_debug(): + run(compile_cmd) + return run_d8_js(abspath('a.out.js')) + + def can_run(self, wasm): + # quite slow (more steps), so run it less frequently + if random.random() < 0.8: + return False + # prefer not to run if the wasm is very large, as it can OOM + # the JS engine. + return super().can_run(wasm) and self.has_emcc and \ + os.path.getsize(wasm) <= INPUT_SIZE_MEAN + + def can_compare_to_other(self, other): + # NaNs can differ from wasm VMs + return not NANS + + class CompareVMs(TestCaseHandler): frequency = 1 def __init__(self): super().__init__() - class BinaryenInterpreter: - name = 'binaryen interpreter' - - def run(self, wasm): - output = run_bynterp(wasm, ['--fuzz-exec-before']) - if output != IGNORE: - calls = output.count(FUZZ_EXEC_EXPORT_PREFIX) - errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX) - if errors > calls / 2: - # A significant amount of execution on this testcase - # simply trapped, and was not very useful, so mark it - # as ignored. Ideally the fuzzer testcases would be - # improved to reduce this number. - # - # Note that we don't change output=IGNORE as there may - # still be useful testing here (up to 50%), so we only - # note that this is a mostly-ignored run, but we do not - # ignore the parts that are useful. - # - # Note that we set amount to 0.5 because we are run both - # on the before wasm and the after wasm. Those will be - # in sync (because the optimizer does not remove traps) - # and so by setting 0.5 we only increment by 1 for the - # entire iteration. - note_ignored_vm_run('too many errors vs calls', - extra_text=f' ({calls} calls, {errors} errors)', - amount=0.5) - return output - - def can_run(self, wasm): - return True - - def can_compare_to_self(self): - return True - - def can_compare_to_other(self, other): - return True - - class D8: - name = 'd8' - - def run(self, wasm, extra_d8_flags=[]): - return run_vm([shared.V8, get_fuzz_shell_js()] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) - - def can_run(self, wasm): - return all_disallowed(DISALLOWED_FEATURES_IN_V8) - - def can_compare_to_self(self): - # With nans, VM differences can confuse us, so only very simple VMs - # can compare to themselves after opts in that case. - return not NANS - - def can_compare_to_other(self, other): - # Relaxed SIMD allows different behavior between VMs, so only - # allow comparisons to other d8 variants if it is enabled. - if not all_disallowed(['relaxed-simd']) and not other.name.startswith('d8'): - return False - - # If not legalized, the JS will fail immediately, so no point to - # compare to others. - return self.can_compare_to_self() and LEGALIZE - - class D8Liftoff(D8): - name = 'd8_liftoff' - - def run(self, wasm): - return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) - - class D8Turboshaft(D8): - name = 'd8_turboshaft' - - def run(self, wasm): - flags = ['--no-liftoff'] - if random.random() < 0.5: - flags += ['--no-wasm-generic-wrapper'] - return super().run(wasm, extra_d8_flags=flags) - - class Wasm2C: - name = 'wasm2c' - - def __init__(self): - # look for wabt in the path. if it's not here, don't run wasm2c - try: - wabt_bin = shared.which('wasm2c') - wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) - self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') - if not os.path.isdir(self.wasm2c_dir): - print('wabt found, but not wasm2c support dir') - self.wasm2c_dir = None - except Exception as e: - print('warning: no wabt found:', e) - self.wasm2c_dir = None - - def can_run(self, wasm): - if self.wasm2c_dir is None: - return False - # if we legalize for JS, the ABI is not what C wants - if LEGALIZE: - return False - # relatively slow, so run it less frequently - if random.random() < 0.5: - return False - # wasm2c doesn't support most features - return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics']) - - def run(self, wasm): - run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) - run(['wasm2c', wasm, '-o', 'wasm.c']) - compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] - run(compile_cmd) - return run_vm(['./a.out']) - - def can_compare_to_self(self): - # The binaryen optimizer changes NaNs in the ways that wasm - # expects, but that's not quite what C has - return not NANS - - def can_compare_to_other(self, other): - # C won't trap on OOB, and NaNs can differ from wasm VMs - return not OOB and not NANS - - class Wasm2C2Wasm(Wasm2C): - name = 'wasm2c2wasm' - - def __init__(self): - super().__init__() - - self.has_emcc = shared.which('emcc') is not None - - def run(self, wasm): - run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) - run(['wasm2c', wasm, '-o', 'wasm.c']) - compile_cmd = ['emcc', 'main.c', 'wasm.c', - os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), - '-I' + self.wasm2c_dir, - '-lm', - '-s', 'ENVIRONMENT=shell', - '-s', 'ALLOW_MEMORY_GROWTH'] - # disable the signal handler: emcc looks like unix, but wasm has - # no signals - compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0'] - if random.random() < 0.5: - compile_cmd += ['-O' + str(random.randint(1, 3))] - elif random.random() < 0.5: - if random.random() < 0.5: - compile_cmd += ['-Os'] - else: - compile_cmd += ['-Oz'] - # avoid pass-debug on the emcc invocation itself (which runs - # binaryen to optimize the wasm), as the wasm here can be very - # large and it isn't what we are focused on testing here - with no_pass_debug(): - run(compile_cmd) - return run_d8_js(abspath('a.out.js')) - - def can_run(self, wasm): - # quite slow (more steps), so run it less frequently - if random.random() < 0.8: - return False - # prefer not to run if the wasm is very large, as it can OOM - # the JS engine. - return super().can_run(wasm) and self.has_emcc and \ - os.path.getsize(wasm) <= INPUT_SIZE_MEAN - - def can_compare_to_other(self, other): - # NaNs can differ from wasm VMs - return not NANS - # the binaryen interpreter is specifically useful for various things self.bynterpreter = BinaryenInterpreter() @@ -2032,8 +2036,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): @@ -2078,6 +2083,56 @@ 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. +class PreserveImportsExportsJS(TestCaseHandler): + frequency = 1 + + def handle_pair(self, input, before_wasm, after_wasm, opts): + # Pick a js+wasm pair. + js_files = list(pathlib.Path(in_binaryen('test', 'js_wasm')).glob('*.mjs')) + js_file = random.choice(js_files) + 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('features not compatible with js+wasm') + return + + # Generate some random input data. + data = abspath('preserve_input.dat') + make_random_input(random_size(), data) + + # Modify the initial wat. + wasm = abspath('modified.wasm') + processed = run([in_bin('wasm-opt'), data] + FEATURE_OPTS + [ + '-ttf', + '--fuzz-preserve-imports-exports', + '--initial-fuzz=' + wat_file, + '-o', wasm + ]) + + # Pick VMs to test the code before and after optimizations. + vms = [ + D8(), + D8Liftoff(), + D8Turboshaft(), + ] + vm_pre = random.choice(vms) + vm_post = random.choice(vms) + + + + 1/0 + + # 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 @@ -2294,19 +2349,7 @@ def handle(self, wasm): # The global list of all test case handlers testcase_handlers = [ - FuzzExec(), - CompareVMs(), - CheckDeterminism(), - Wasm2JS(), - TrapsNeverHappen(), - CtorEval(), - Merge(), - # Split(), # https://github.com/WebAssembly/binaryen/issues/8510 - RoundtripText(), - ClusterFuzz(), - Two(), - PreserveImportsExports(), - BranchHintPreservation(), + PreserveImportsExportsJS(), ] From df8f1323ad3ea2ef8dc6cdf5d3b77b660d52cf7e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 09:49:11 -0700 Subject: [PATCH 02/39] undo --- scripts/fuzz_opt.py | 341 ++++++++++++++++++++++---------------------- 1 file changed, 169 insertions(+), 172 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 6bb7b8b2d64..57fea2f8c6d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -784,183 +784,180 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): run([in_bin('wasm-opt'), before_wasm] + opts + ['--fuzz-exec']) -# VMs - -class BinaryenInterpreter: - name = 'binaryen interpreter' - - def run(self, wasm): - output = run_bynterp(wasm, ['--fuzz-exec-before']) - if output != IGNORE: - calls = output.count(FUZZ_EXEC_EXPORT_PREFIX) - errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX) - if errors > calls / 2: - # A significant amount of execution on this testcase - # simply trapped, and was not very useful, so mark it - # as ignored. Ideally the fuzzer testcases would be - # improved to reduce this number. - # - # Note that we don't change output=IGNORE as there may - # still be useful testing here (up to 50%), so we only - # note that this is a mostly-ignored run, but we do not - # ignore the parts that are useful. - # - # Note that we set amount to 0.5 because we are run both - # on the before wasm and the after wasm. Those will be - # in sync (because the optimizer does not remove traps) - # and so by setting 0.5 we only increment by 1 for the - # entire iteration. - note_ignored_vm_run('too many errors vs calls', - extra_text=f' ({calls} calls, {errors} errors)', - amount=0.5) - return output - - def can_run(self, wasm): - return True - - def can_compare_to_self(self): - return True - - def can_compare_to_other(self, other): - return True - -class D8: - name = 'd8' - - def run(self, wasm, extra_d8_flags=[]): - return run_vm([shared.V8, get_fuzz_shell_js()] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) - - def can_run(self, wasm): - return all_disallowed(DISALLOWED_FEATURES_IN_V8) - - def can_compare_to_self(self): - # With nans, VM differences can confuse us, so only very simple VMs - # can compare to themselves after opts in that case. - return not NANS - - def can_compare_to_other(self, other): - # Relaxed SIMD allows different behavior between VMs, so only - # allow comparisons to other d8 variants if it is enabled. - if not all_disallowed(['relaxed-simd']) and not other.name.startswith('d8'): - return False - - # If not legalized, the JS will fail immediately, so no point to - # compare to others. - return self.can_compare_to_self() and LEGALIZE - -class D8Liftoff(D8): - name = 'd8_liftoff' - - def run(self, wasm): - return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) - -class D8Turboshaft(D8): - name = 'd8_turboshaft' - - def run(self, wasm): - flags = ['--no-liftoff'] - if random.random() < 0.5: - flags += ['--no-wasm-generic-wrapper'] - return super().run(wasm, extra_d8_flags=flags) - -class Wasm2C: - name = 'wasm2c' - - def __init__(self): - # look for wabt in the path. if it's not here, don't run wasm2c - try: - wabt_bin = shared.which('wasm2c') - wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) - self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') - if not os.path.isdir(self.wasm2c_dir): - print('wabt found, but not wasm2c support dir') - self.wasm2c_dir = None - except Exception as e: - print('warning: no wabt found:', e) - self.wasm2c_dir = None - - def can_run(self, wasm): - if self.wasm2c_dir is None: - return False - # if we legalize for JS, the ABI is not what C wants - if LEGALIZE: - return False - # relatively slow, so run it less frequently - if random.random() < 0.5: - return False - # wasm2c doesn't support most features - return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics']) - - def run(self, wasm): - run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) - run(['wasm2c', wasm, '-o', 'wasm.c']) - compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] - run(compile_cmd) - return run_vm(['./a.out']) - - def can_compare_to_self(self): - # The binaryen optimizer changes NaNs in the ways that wasm - # expects, but that's not quite what C has - return not NANS - - def can_compare_to_other(self, other): - # C won't trap on OOB, and NaNs can differ from wasm VMs - return not OOB and not NANS - -class Wasm2C2Wasm(Wasm2C): - name = 'wasm2c2wasm' - - def __init__(self): - super().__init__() - - self.has_emcc = shared.which('emcc') is not None - - def run(self, wasm): - run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) - run(['wasm2c', wasm, '-o', 'wasm.c']) - compile_cmd = ['emcc', 'main.c', 'wasm.c', - os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), - '-I' + self.wasm2c_dir, - '-lm', - '-s', 'ENVIRONMENT=shell', - '-s', 'ALLOW_MEMORY_GROWTH'] - # disable the signal handler: emcc looks like unix, but wasm has - # no signals - compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0'] - if random.random() < 0.5: - compile_cmd += ['-O' + str(random.randint(1, 3))] - elif random.random() < 0.5: - if random.random() < 0.5: - compile_cmd += ['-Os'] - else: - compile_cmd += ['-Oz'] - # avoid pass-debug on the emcc invocation itself (which runs - # binaryen to optimize the wasm), as the wasm here can be very - # large and it isn't what we are focused on testing here - with no_pass_debug(): - run(compile_cmd) - return run_d8_js(abspath('a.out.js')) - - def can_run(self, wasm): - # quite slow (more steps), so run it less frequently - if random.random() < 0.8: - return False - # prefer not to run if the wasm is very large, as it can OOM - # the JS engine. - return super().can_run(wasm) and self.has_emcc and \ - os.path.getsize(wasm) <= INPUT_SIZE_MEAN - - def can_compare_to_other(self, other): - # NaNs can differ from wasm VMs - return not NANS - - class CompareVMs(TestCaseHandler): frequency = 1 def __init__(self): super().__init__() + class BinaryenInterpreter: + name = 'binaryen interpreter' + + def run(self, wasm): + output = run_bynterp(wasm, ['--fuzz-exec-before']) + if output != IGNORE: + calls = output.count(FUZZ_EXEC_EXPORT_PREFIX) + errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX) + if errors > calls / 2: + # A significant amount of execution on this testcase + # simply trapped, and was not very useful, so mark it + # as ignored. Ideally the fuzzer testcases would be + # improved to reduce this number. + # + # Note that we don't change output=IGNORE as there may + # still be useful testing here (up to 50%), so we only + # note that this is a mostly-ignored run, but we do not + # ignore the parts that are useful. + # + # Note that we set amount to 0.5 because we are run both + # on the before wasm and the after wasm. Those will be + # in sync (because the optimizer does not remove traps) + # and so by setting 0.5 we only increment by 1 for the + # entire iteration. + note_ignored_vm_run('too many errors vs calls', + extra_text=f' ({calls} calls, {errors} errors)', + amount=0.5) + return output + + def can_run(self, wasm): + return True + + def can_compare_to_self(self): + return True + + def can_compare_to_other(self, other): + return True + + class D8: + name = 'd8' + + def run(self, wasm, extra_d8_flags=[]): + return run_vm([shared.V8, get_fuzz_shell_js()] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) + + def can_run(self, wasm): + return all_disallowed(DISALLOWED_FEATURES_IN_V8) + + def can_compare_to_self(self): + # With nans, VM differences can confuse us, so only very simple VMs + # can compare to themselves after opts in that case. + return not NANS + + def can_compare_to_other(self, other): + # Relaxed SIMD allows different behavior between VMs, so only + # allow comparisons to other d8 variants if it is enabled. + if not all_disallowed(['relaxed-simd']) and not other.name.startswith('d8'): + return False + + # If not legalized, the JS will fail immediately, so no point to + # compare to others. + return self.can_compare_to_self() and LEGALIZE + + class D8Liftoff(D8): + name = 'd8_liftoff' + + def run(self, wasm): + return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) + + class D8Turboshaft(D8): + name = 'd8_turboshaft' + + def run(self, wasm): + flags = ['--no-liftoff'] + if random.random() < 0.5: + flags += ['--no-wasm-generic-wrapper'] + return super().run(wasm, extra_d8_flags=flags) + + class Wasm2C: + name = 'wasm2c' + + def __init__(self): + # look for wabt in the path. if it's not here, don't run wasm2c + try: + wabt_bin = shared.which('wasm2c') + wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) + self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') + if not os.path.isdir(self.wasm2c_dir): + print('wabt found, but not wasm2c support dir') + self.wasm2c_dir = None + except Exception as e: + print('warning: no wabt found:', e) + self.wasm2c_dir = None + + def can_run(self, wasm): + if self.wasm2c_dir is None: + return False + # if we legalize for JS, the ABI is not what C wants + if LEGALIZE: + return False + # relatively slow, so run it less frequently + if random.random() < 0.5: + return False + # wasm2c doesn't support most features + return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics']) + + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] + run(compile_cmd) + return run_vm(['./a.out']) + + def can_compare_to_self(self): + # The binaryen optimizer changes NaNs in the ways that wasm + # expects, but that's not quite what C has + return not NANS + + def can_compare_to_other(self, other): + # C won't trap on OOB, and NaNs can differ from wasm VMs + return not OOB and not NANS + + class Wasm2C2Wasm(Wasm2C): + name = 'wasm2c2wasm' + + def __init__(self): + super().__init__() + + self.has_emcc = shared.which('emcc') is not None + + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['emcc', 'main.c', 'wasm.c', + os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), + '-I' + self.wasm2c_dir, + '-lm', + '-s', 'ENVIRONMENT=shell', + '-s', 'ALLOW_MEMORY_GROWTH'] + # disable the signal handler: emcc looks like unix, but wasm has + # no signals + compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0'] + if random.random() < 0.5: + compile_cmd += ['-O' + str(random.randint(1, 3))] + elif random.random() < 0.5: + if random.random() < 0.5: + compile_cmd += ['-Os'] + else: + compile_cmd += ['-Oz'] + # avoid pass-debug on the emcc invocation itself (which runs + # binaryen to optimize the wasm), as the wasm here can be very + # large and it isn't what we are focused on testing here + with no_pass_debug(): + run(compile_cmd) + return run_d8_js(abspath('a.out.js')) + + def can_run(self, wasm): + # quite slow (more steps), so run it less frequently + if random.random() < 0.8: + return False + # prefer not to run if the wasm is very large, as it can OOM + # the JS engine. + return super().can_run(wasm) and self.has_emcc and \ + os.path.getsize(wasm) <= INPUT_SIZE_MEAN + + def can_compare_to_other(self, other): + # NaNs can differ from wasm VMs + return not NANS + # the binaryen interpreter is specifically useful for various things self.bynterpreter = BinaryenInterpreter() @@ -2128,7 +2125,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): vm_pre = random.choice(vms) vm_post = random.choice(vms) - + pre = vm_pre.run( 1/0 From b8bc05427a8f1a4e698669fb6dc5e5807375330b Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 09:58:17 -0700 Subject: [PATCH 03/39] work --- scripts/fuzz_opt.py | 54 ++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 57fea2f8c6d..0d3f9161399 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -693,17 +693,12 @@ def get_v8_extra_flags(): V8_LIFTOFF_ARGS = ['--liftoff'] +V8_NO_LIFTOFF_ARGS = ['--no-liftoff'] -# Default to running with liftoff enabled, because we need to pick either -# liftoff or turbo* for consistency (otherwise running the same command twice -# may have different results due to NaN nondeterminism), and liftoff is faster -# for small things. -def run_d8_js(js, args=[], liftoff=True): +def run_d8_js(js, args=[]): cmd = [shared.V8] + shared.V8_OPTS cmd += get_v8_extra_flags() - if liftoff: - cmd += V8_LIFTOFF_ARGS cmd += [js] if args: cmd += ['--'] + args @@ -741,8 +736,8 @@ def get_fuzz_shell_js(): return JSPI_JS_FILE -def run_d8_wasm(wasm, liftoff=True, args=[]): - return run_d8_js(get_fuzz_shell_js(), [wasm] + args, liftoff=liftoff) +def run_d8_wasm(wasm, args=[]): + return run_d8_js(get_fuzz_shell_js(), [wasm] + args) def all_disallowed(features): @@ -862,7 +857,7 @@ class D8Turboshaft(D8): name = 'd8_turboshaft' def run(self, wasm): - flags = ['--no-liftoff'] + flags = V8_NO_LIFTOFF_ARGS if random.random() < 0.5: flags += ['--no-wasm-generic-wrapper'] return super().run(wasm, extra_d8_flags=flags) @@ -2103,31 +2098,34 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): note_ignored_vm_run('features not compatible with js+wasm') return - # Generate some random input data. - data = abspath('preserve_input.dat') - make_random_input(random_size(), data) - - # Modify the initial wat. - wasm = abspath('modified.wasm') - processed = run([in_bin('wasm-opt'), data] + FEATURE_OPTS + [ + # Modify the initial wat to get the pre-optimizations wasm. + pre_wasm = abspath('pre.wasm') + processed = run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [ '-ttf', '--fuzz-preserve-imports-exports', '--initial-fuzz=' + wat_file, - '-o', wasm + '-o', pre_wasm ]) - # Pick VMs to test the code before and after optimizations. - vms = [ - D8(), - D8Liftoff(), - D8Turboshaft(), - ] - vm_pre = random.choice(vms) - vm_post = random.choice(vms) + # Pick v8 opts. + v8_opts = random.choice([ + [], + V8_LIFTOFF_ARGS, + V8_NO_LIFTOFF_ARGS, + ]) + + # Run before opts. + pre = run_d8_js(js_file, v8_opts + ['--', pre_wasm]) + + # Optimize. + post_wasm = abspath('post.wasm') + run([in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + opts + FEATURE_OPTS) - pre = vm_pre.run( + # Run after opts. + post = run_d8_js(js_file, v8_opts + ['--', post_wasm]) - 1/0 + # Compare + compare(pre, post, 'PreserveImportsExportsJS') # Test that we preserve branch hints properly. The invariant that we test here From 511ac057bd6200ad485a869a82c2cb416d676052 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 10:21:24 -0700 Subject: [PATCH 04/39] undo --- scripts/fuzz_opt.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 0d3f9161399..cd73cfb45c7 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -696,9 +696,15 @@ def get_v8_extra_flags(): V8_NO_LIFTOFF_ARGS = ['--no-liftoff'] -def run_d8_js(js, args=[]): +# Default to running with liftoff enabled, because we need to pick either +# liftoff or turbo* for consistency (otherwise running the same command twice +# may have different results due to NaN nondeterminism), and liftoff is faster +# for small things. +def run_d8_js(js, args=[], liftoff=True): cmd = [shared.V8] + shared.V8_OPTS cmd += get_v8_extra_flags() + if liftoff: + cmd += V8_LIFTOFF_ARGS cmd += [js] if args: cmd += ['--'] + args @@ -736,8 +742,8 @@ def get_fuzz_shell_js(): return JSPI_JS_FILE -def run_d8_wasm(wasm, args=[]): - return run_d8_js(get_fuzz_shell_js(), [wasm] + args) +def run_d8_wasm(wasm, liftoff=True, args=[]): + return run_d8_js(get_fuzz_shell_js(), [wasm] + args, liftoff=liftoff) def all_disallowed(features): @@ -2086,7 +2092,7 @@ class PreserveImportsExportsJS(TestCaseHandler): def handle_pair(self, input, before_wasm, after_wasm, opts): # Pick a js+wasm pair. js_files = list(pathlib.Path(in_binaryen('test', 'js_wasm')).glob('*.mjs')) - js_file = random.choice(js_files) + js_file = str(random.choice(js_files)) wat_file = str(pathlib.Path(js_file).with_suffix('.wat')) # Verify the wat works with our features From ea9d16de3abfa49da9c2880e46fc2f90cb6bba5b Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 10:25:24 -0700 Subject: [PATCH 05/39] wrk --- scripts/fuzz_opt.py | 364 ++++++++++++++++++++++---------------------- 1 file changed, 185 insertions(+), 179 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index cd73cfb45c7..9b9ac7c0aa7 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -785,180 +785,186 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): run([in_bin('wasm-opt'), before_wasm] + opts + ['--fuzz-exec']) +# VMs + +class BinaryenInterpreter: + name = 'binaryen interpreter' + + def run(self, wasm): + output = run_bynterp(wasm, ['--fuzz-exec-before']) + if output != IGNORE: + calls = output.count(FUZZ_EXEC_EXPORT_PREFIX) + errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX) + if errors > calls / 2: + # A significant amount of execution on this testcase + # simply trapped, and was not very useful, so mark it + # as ignored. Ideally the fuzzer testcases would be + # improved to reduce this number. + # + # Note that we don't change output=IGNORE as there may + # still be useful testing here (up to 50%), so we only + # note that this is a mostly-ignored run, but we do not + # ignore the parts that are useful. + # + # Note that we set amount to 0.5 because we are run both + # on the before wasm and the after wasm. Those will be + # in sync (because the optimizer does not remove traps) + # and so by setting 0.5 we only increment by 1 for the + # entire iteration. + note_ignored_vm_run('too many errors vs calls', + extra_text=f' ({calls} calls, {errors} errors)', + amount=0.5) + return output + + def can_run(self, wasm): + return True + + def can_compare_to_self(self): + return True + + def can_compare_to_other(self, other): + return True + +class D8: + name = 'd8' + + def run_js(self, js, wasm, extra_d8_flags=[]): + return run_vm([shared.V8, js] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) + + def run(self, wasm, extra_d8_flags=[]): + return self.run(js=get_fuzz_shell_js(), wasm=wasm, extra_d8_flags=extra_d8_flags) + + def can_run(self, wasm): + return all_disallowed(DISALLOWED_FEATURES_IN_V8) + + def can_compare_to_self(self): + # With nans, VM differences can confuse us, so only very simple VMs + # can compare to themselves after opts in that case. + return not NANS + + def can_compare_to_other(self, other): + # Relaxed SIMD allows different behavior between VMs, so only + # allow comparisons to other d8 variants if it is enabled. + if not all_disallowed(['relaxed-simd']) and not other.name.startswith('d8'): + return False + + # If not legalized, the JS will fail immediately, so no point to + # compare to others. + return self.can_compare_to_self() and LEGALIZE + +class D8Liftoff(D8): + name = 'd8_liftoff' + + def run(self, wasm): + return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) + +class D8Turboshaft(D8): + name = 'd8_turboshaft' + + def run(self, wasm): + flags = V8_NO_LIFTOFF_ARGS + if random.random() < 0.5: + flags += ['--no-wasm-generic-wrapper'] + return super().run(wasm, extra_d8_flags=flags) + +class Wasm2C: + name = 'wasm2c' + + def __init__(self): + # look for wabt in the path. if it's not here, don't run wasm2c + try: + wabt_bin = shared.which('wasm2c') + wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) + self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') + if not os.path.isdir(self.wasm2c_dir): + print('wabt found, but not wasm2c support dir') + self.wasm2c_dir = None + except Exception as e: + print('warning: no wabt found:', e) + self.wasm2c_dir = None + + def can_run(self, wasm): + if self.wasm2c_dir is None: + return False + # if we legalize for JS, the ABI is not what C wants + if LEGALIZE: + return False + # relatively slow, so run it less frequently + if random.random() < 0.5: + return False + # wasm2c doesn't support most features + return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics']) + + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] + run(compile_cmd) + return run_vm(['./a.out']) + + def can_compare_to_self(self): + # The binaryen optimizer changes NaNs in the ways that wasm + # expects, but that's not quite what C has + return not NANS + + def can_compare_to_other(self, other): + # C won't trap on OOB, and NaNs can differ from wasm VMs + return not OOB and not NANS + +class Wasm2C2Wasm(Wasm2C): + name = 'wasm2c2wasm' + + def __init__(self): + super().__init__() + + self.has_emcc = shared.which('emcc') is not None + + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['emcc', 'main.c', 'wasm.c', + os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), + '-I' + self.wasm2c_dir, + '-lm', + '-s', 'ENVIRONMENT=shell', + '-s', 'ALLOW_MEMORY_GROWTH'] + # disable the signal handler: emcc looks like unix, but wasm has + # no signals + compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0'] + if random.random() < 0.5: + compile_cmd += ['-O' + str(random.randint(1, 3))] + elif random.random() < 0.5: + if random.random() < 0.5: + compile_cmd += ['-Os'] + else: + compile_cmd += ['-Oz'] + # avoid pass-debug on the emcc invocation itself (which runs + # binaryen to optimize the wasm), as the wasm here can be very + # large and it isn't what we are focused on testing here + with no_pass_debug(): + run(compile_cmd) + return run_d8_js(abspath('a.out.js')) + + def can_run(self, wasm): + # quite slow (more steps), so run it less frequently + if random.random() < 0.8: + return False + # prefer not to run if the wasm is very large, as it can OOM + # the JS engine. + return super().can_run(wasm) and self.has_emcc and \ + os.path.getsize(wasm) <= INPUT_SIZE_MEAN + + def can_compare_to_other(self, other): + # NaNs can differ from wasm VMs + return not NANS + + class CompareVMs(TestCaseHandler): frequency = 1 def __init__(self): super().__init__() - class BinaryenInterpreter: - name = 'binaryen interpreter' - - def run(self, wasm): - output = run_bynterp(wasm, ['--fuzz-exec-before']) - if output != IGNORE: - calls = output.count(FUZZ_EXEC_EXPORT_PREFIX) - errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX) - if errors > calls / 2: - # A significant amount of execution on this testcase - # simply trapped, and was not very useful, so mark it - # as ignored. Ideally the fuzzer testcases would be - # improved to reduce this number. - # - # Note that we don't change output=IGNORE as there may - # still be useful testing here (up to 50%), so we only - # note that this is a mostly-ignored run, but we do not - # ignore the parts that are useful. - # - # Note that we set amount to 0.5 because we are run both - # on the before wasm and the after wasm. Those will be - # in sync (because the optimizer does not remove traps) - # and so by setting 0.5 we only increment by 1 for the - # entire iteration. - note_ignored_vm_run('too many errors vs calls', - extra_text=f' ({calls} calls, {errors} errors)', - amount=0.5) - return output - - def can_run(self, wasm): - return True - - def can_compare_to_self(self): - return True - - def can_compare_to_other(self, other): - return True - - class D8: - name = 'd8' - - def run(self, wasm, extra_d8_flags=[]): - return run_vm([shared.V8, get_fuzz_shell_js()] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) - - def can_run(self, wasm): - return all_disallowed(DISALLOWED_FEATURES_IN_V8) - - def can_compare_to_self(self): - # With nans, VM differences can confuse us, so only very simple VMs - # can compare to themselves after opts in that case. - return not NANS - - def can_compare_to_other(self, other): - # Relaxed SIMD allows different behavior between VMs, so only - # allow comparisons to other d8 variants if it is enabled. - if not all_disallowed(['relaxed-simd']) and not other.name.startswith('d8'): - return False - - # If not legalized, the JS will fail immediately, so no point to - # compare to others. - return self.can_compare_to_self() and LEGALIZE - - class D8Liftoff(D8): - name = 'd8_liftoff' - - def run(self, wasm): - return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) - - class D8Turboshaft(D8): - name = 'd8_turboshaft' - - def run(self, wasm): - flags = V8_NO_LIFTOFF_ARGS - if random.random() < 0.5: - flags += ['--no-wasm-generic-wrapper'] - return super().run(wasm, extra_d8_flags=flags) - - class Wasm2C: - name = 'wasm2c' - - def __init__(self): - # look for wabt in the path. if it's not here, don't run wasm2c - try: - wabt_bin = shared.which('wasm2c') - wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) - self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') - if not os.path.isdir(self.wasm2c_dir): - print('wabt found, but not wasm2c support dir') - self.wasm2c_dir = None - except Exception as e: - print('warning: no wabt found:', e) - self.wasm2c_dir = None - - def can_run(self, wasm): - if self.wasm2c_dir is None: - return False - # if we legalize for JS, the ABI is not what C wants - if LEGALIZE: - return False - # relatively slow, so run it less frequently - if random.random() < 0.5: - return False - # wasm2c doesn't support most features - return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics']) - - def run(self, wasm): - run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) - run(['wasm2c', wasm, '-o', 'wasm.c']) - compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] - run(compile_cmd) - return run_vm(['./a.out']) - - def can_compare_to_self(self): - # The binaryen optimizer changes NaNs in the ways that wasm - # expects, but that's not quite what C has - return not NANS - - def can_compare_to_other(self, other): - # C won't trap on OOB, and NaNs can differ from wasm VMs - return not OOB and not NANS - - class Wasm2C2Wasm(Wasm2C): - name = 'wasm2c2wasm' - - def __init__(self): - super().__init__() - - self.has_emcc = shared.which('emcc') is not None - - def run(self, wasm): - run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) - run(['wasm2c', wasm, '-o', 'wasm.c']) - compile_cmd = ['emcc', 'main.c', 'wasm.c', - os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), - '-I' + self.wasm2c_dir, - '-lm', - '-s', 'ENVIRONMENT=shell', - '-s', 'ALLOW_MEMORY_GROWTH'] - # disable the signal handler: emcc looks like unix, but wasm has - # no signals - compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0'] - if random.random() < 0.5: - compile_cmd += ['-O' + str(random.randint(1, 3))] - elif random.random() < 0.5: - if random.random() < 0.5: - compile_cmd += ['-Os'] - else: - compile_cmd += ['-Oz'] - # avoid pass-debug on the emcc invocation itself (which runs - # binaryen to optimize the wasm), as the wasm here can be very - # large and it isn't what we are focused on testing here - with no_pass_debug(): - run(compile_cmd) - return run_d8_js(abspath('a.out.js')) - - def can_run(self, wasm): - # quite slow (more steps), so run it less frequently - if random.random() < 0.8: - return False - # prefer not to run if the wasm is very large, as it can OOM - # the JS engine. - return super().can_run(wasm) and self.has_emcc and \ - os.path.getsize(wasm) <= INPUT_SIZE_MEAN - - def can_compare_to_other(self, other): - # NaNs can differ from wasm VMs - return not NANS - # the binaryen interpreter is specifically useful for various things self.bynterpreter = BinaryenInterpreter() @@ -2113,22 +2119,22 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): '-o', pre_wasm ]) - # Pick v8 opts. - v8_opts = random.choice([ - [], - V8_LIFTOFF_ARGS, - V8_NO_LIFTOFF_ARGS, - ]) - - # Run before opts. - pre = run_d8_js(js_file, v8_opts + ['--', pre_wasm]) + # Pick vm and run before we optimize the wasm. + vms = [ + D8(), + D8Liftoff(), + D8Turboshaft(), + ] + pre_vm = random.choice(vm) + pre = pre_vm.run_js(js_file, pre_wasm) # Optimize. post_wasm = abspath('post.wasm') run([in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + opts + FEATURE_OPTS) - # Run after opts. - post = run_d8_js(js_file, v8_opts + ['--', post_wasm]) + # Run after opts, in a random vm. + post_vm = random.choice(vm) + post = post_vm.run_js(js_file, post_wasm) # Compare compare(pre, post, 'PreserveImportsExportsJS') From 35c3f898077eed2d33c9b7a7a7253bb610753545 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 10:27:16 -0700 Subject: [PATCH 06/39] wrk --- scripts/fuzz_opt.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 9b9ac7c0aa7..e85fc9b2f41 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2125,7 +2125,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): D8Liftoff(), D8Turboshaft(), ] - pre_vm = random.choice(vm) + pre_vm = random.choice(vms) pre = pre_vm.run_js(js_file, pre_wasm) # Optimize. @@ -2133,12 +2133,17 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): run([in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + opts + FEATURE_OPTS) # Run after opts, in a random vm. - post_vm = random.choice(vm) + post_vm = random.choice(vms) post = post_vm.run_js(js_file, post_wasm) # Compare compare(pre, post, 'PreserveImportsExportsJS') + 1/0 + + 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 From 7afab7106b36fa895ac39a86dfa73eba08eef5bf Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 10:40:16 -0700 Subject: [PATCH 07/39] go --- scripts/fuzz_opt.py | 2 - src/tools/fuzzing/fuzzing.cpp | 8 ++- test/js_wasm/counter.mjs | 34 +++++++++++ test/js_wasm/counter.wat | 107 ++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 test/js_wasm/counter.mjs create mode 100644 test/js_wasm/counter.wat diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index e85fc9b2f41..633bd98ce12 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2139,8 +2139,6 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # Compare compare(pre, post, 'PreserveImportsExportsJS') - 1/0 - def can_run_on_wasm(self, wasm): return all_disallowed(DISALLOWED_FEATURES_IN_V8) diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index 222d68698ae..af206ab2488 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -2381,8 +2381,12 @@ void TranslateToFuzzReader::modifyInitialFunctions() { } // Remove a start function - the fuzzing harness expects code to run only - // from exports. - wasm.start = Name(); + // from exports. When preserving imports and exports, however, we need to + // keep any start method, as it may be important to keep the contract between + // the wasm and the outside. + if (!preserveImportsAndExports) { + wasm.start = Name(); + } } void TranslateToFuzzReader::dropToLog(Function* func) { diff --git a/test/js_wasm/counter.mjs b/test/js_wasm/counter.mjs new file mode 100644 index 00000000000..04e217a1198 --- /dev/null +++ b/test/js_wasm/counter.mjs @@ -0,0 +1,34 @@ +// https://github.com/WebAssembly/custom-descriptors/blob/main/proposals/custom-descriptors/Overview.md + +// counter.mjs + +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]); // XXX modified to read the wasm filename + +let { module, instance } = + await WebAssembly.instantiate(buffer, imports, compileOptions); + +let Counter = constructors.Counter; + +let count = new Counter(0); + +console.log(count.get()); +count.inc(); +console.log(count.get()); + +console.log(count instanceof Counter); diff --git a/test/js_wasm/counter.wat b/test/js_wasm/counter.wat new file mode 100644 index 00000000000..fe5af51c905 --- /dev/null +++ b/test/js_wasm/counter.wat @@ -0,0 +1,107 @@ +;; https://github.com/WebAssembly/custom-descriptors/blob/main/proposals/custom-descriptors/Overview.md + +;; counter.wasm + +(module + (rec + (type $counter (descriptor $counter.vtable) (struct (field $val (mut i32)))) + (type $counter.vtable (describes $counter) (struct + (field $proto (ref extern)) + (field $get (ref $get_t)) + (field $inc (ref $inc_t)) + )) + (type $get_t (func (param (ref null $counter)) (result i32))) + (type $inc_t (func (param (ref null $counter)))) + ) + (type $new_t (func (param i32) (result (ref $counter)))) + + ;; Types for prototype configuration + (type $prototypes (array (mut externref))) + (type $functions (array (mut funcref))) + (type $data (array (mut i8))) + (type $configureAll (func (param (ref null $prototypes)) + (param (ref null $functions)) + (param (ref null $data)) + (param externref))) + + (import "protos" "counter.proto" (global $counter.proto (ref extern))) + + ;; The object where configured constructors will be installed. + (import "env" "constructors" (global $constructors externref)) + + (import "wasm:js-prototypes" "configureAll" + (func $configureAll (type $configureAll))) + + ;; Segments used to create arrays passed to $configureAll + (elem $prototypes externref + (global.get $counter.proto) + ) + (elem $functions funcref + (ref.func $counter.new) + (ref.func $counter.get) + (ref.func $counter.inc) + ) + ;; \01 one protoconfig + ;; \01 one constructorconfig + ;; \07 length of name "Counter" + ;; Counter constructor name + ;; \00 no static methods + ;; \02 two methodconfigs + ;; \00 method (not getter or setter) + ;; \03 length of name "get" + ;; get method name + ;; \00 method (not getter or setter) + ;; \03 length of name "inc" + ;; inc method name + ;; \7f no parent prototype (-1 s32) + (data $data "\01\01\07Counter\00\02\00\03get\00\03inc\7f") + + (global $counter.vtable (ref (exact $counter.vtable)) + (struct.new $counter.vtable + (global.get $counter.proto) + (ref.func $counter.get) + (ref.func $counter.inc) + ) + ) + + (func $counter.get (type $get_t) (param (ref null $counter)) (result i32) + (struct.get $counter $val (local.get 0)) + ) + + (func $counter.inc (type $inc_t) (param (ref null $counter)) + (struct.set $counter $val + (local.get 0) + (i32.add + (struct.get $counter $val (local.get 0)) + (i32.const 1) + ) + ) + ) + + (func $counter.new (type $new_t) (param i32) (result (ref $counter)) + (struct.new_desc $counter + (local.get 0) + (global.get $counter.vtable) + ) + ) + + (func $start + (call $configureAll + (array.new_elem $prototypes $prototypes + (i32.const 0) + (i32.const 1) + ) + (array.new_elem $functions $functions + (i32.const 0) + (i32.const 3) + ) + (array.new_data $data $data + (i32.const 0) + (i32.const 23) + ) + (global.get $constructors) + ) + ) + + (start $start) +) From 7487cc25508aa17a07f2cdab12581322967240cb Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 11:26:26 -0700 Subject: [PATCH 08/39] go --- scripts/fuzz_opt.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 633bd98ce12..0f245d99192 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2091,7 +2091,9 @@ def get_relevant_lines(wat): # 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. +# the wasm. This simulates how an external fuzzer could use binaryen to modify +# its known-working testcases. +# TODO: Reduction how? class PreserveImportsExportsJS(TestCaseHandler): frequency = 1 @@ -2126,7 +2128,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): D8Turboshaft(), ] pre_vm = random.choice(vms) - pre = pre_vm.run_js(js_file, pre_wasm) + pre = self.do_run(pre_vm, js_file, pre_wasm) # Optimize. post_wasm = abspath('post.wasm') @@ -2134,11 +2136,24 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # Run after opts, in a random vm. post_vm = random.choice(vms) - post = post_vm.run_js(js_file, post_wasm) + post = self.do_run(post_vm, js_file, post_wasm) # Compare compare(pre, post, 'PreserveImportsExportsJS') + # Run a VM on a js+wasm combination, handling exceptions. It is possible the + # testcase simply traps, in which case we mark it as such, and test that it + # traps both before and after. + def do_run(self, vm, js, wasm): + try: + return vm.run_js(js, wasm) + except Exception as e: + # We run this twice (before and after opts), so increment 0.5 each + # time. + note_ignored_vm_run('PreserveImportsExportsJS trap', + amount=0.5) + return 'JS exception' # TODO: can we keep some output? + def can_run_on_wasm(self, wasm): return all_disallowed(DISALLOWED_FEATURES_IN_V8) From affcb37dd7594895d4bbc7df931a4e3dce32da56 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 12:08:34 -0700 Subject: [PATCH 09/39] wrk --- scripts/fuzz_opt.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 0f245d99192..51faa03932b 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2098,6 +2098,8 @@ class PreserveImportsExportsJS(TestCaseHandler): frequency = 1 def handle_pair(self, input, before_wasm, after_wasm, opts): + # TODO: We could allow less than INPUT_SIZE_MIN for 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)) @@ -2132,7 +2134,16 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # Optimize. post_wasm = abspath('post.wasm') - run([in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + opts + FEATURE_OPTS) + cmd = [in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + opts + FEATURE_OPTS + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if proc.returncode: + if 'Invalid configureAll' in proc.stderr: + 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) From d3100a80215a73b6951d89be3fec34d721fa8718 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 12:10:59 -0700 Subject: [PATCH 10/39] wrk --- scripts/fuzz_opt.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 51faa03932b..0e68ccb04f2 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2105,15 +2105,6 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): js_file = str(random.choice(js_files)) 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('features not compatible with js+wasm') - return - # Modify the initial wat to get the pre-optimizations wasm. pre_wasm = abspath('pre.wasm') processed = run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [ @@ -2166,6 +2157,14 @@ def do_run(self, vm, js, wasm): return 'JS exception' # TODO: can we keep some output? def can_run_on_wasm(self, wasm): + # Verify the wat works with our features + try: + run([in_bin('wasm-opt'), wasm] + FEATURE_OPTS, + stderr=subprocess.PIPE, + silent=True) + except Exception: + return False + return all_disallowed(DISALLOWED_FEATURES_IN_V8) From 8c0b563cf9cbad04385b529b9e6891eea64d75f4 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 12:13:19 -0700 Subject: [PATCH 11/39] Revert "wrk" This reverts commit d3100a80215a73b6951d89be3fec34d721fa8718. --- scripts/fuzz_opt.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 0e68ccb04f2..51faa03932b 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2105,6 +2105,15 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): js_file = str(random.choice(js_files)) 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('features not compatible with js+wasm') + return + # Modify the initial wat to get the pre-optimizations wasm. pre_wasm = abspath('pre.wasm') processed = run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [ @@ -2157,14 +2166,6 @@ def do_run(self, vm, js, wasm): return 'JS exception' # TODO: can we keep some output? def can_run_on_wasm(self, wasm): - # Verify the wat works with our features - try: - run([in_bin('wasm-opt'), wasm] + FEATURE_OPTS, - stderr=subprocess.PIPE, - silent=True) - except Exception: - return False - return all_disallowed(DISALLOWED_FEATURES_IN_V8) From b2dd1540f925122e7aa4d56c3ab32c6fe41dc4a8 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 12:28:38 -0700 Subject: [PATCH 12/39] wrk --- scripts/fuzz_opt.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 51faa03932b..be574053bfb 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -689,6 +689,9 @@ def get_v8_extra_flags(): if random.random() < 0.5: flags += ['--wasm-assert-types'] + if random.random() < 0.5: + flags += ['--no-wasm-generic-wrapper'] + return flags @@ -828,11 +831,13 @@ def can_compare_to_other(self, other): class D8: name = 'd8' - def run_js(self, js, wasm, extra_d8_flags=[]): - return run_vm([shared.V8, js] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) + extra_d8_flags = [] - def run(self, wasm, extra_d8_flags=[]): - return self.run(js=get_fuzz_shell_js(), wasm=wasm, extra_d8_flags=extra_d8_flags) + def run_js(self, js, wasm): + return run_vm([shared.V8, js] + shared.V8_OPTS + get_v8_extra_flags() + self.extra_d8_flags + ['--', wasm]) + + def run(self, wasm): + return self.run(js=get_fuzz_shell_js(), wasm=wasm) def can_run(self, wasm): return all_disallowed(DISALLOWED_FEATURES_IN_V8) @@ -855,17 +860,14 @@ def can_compare_to_other(self, other): class D8Liftoff(D8): name = 'd8_liftoff' - def run(self, wasm): - return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) + extra_d8_flags = V8_LIFTOFF_ARGS + class D8Turboshaft(D8): name = 'd8_turboshaft' - def run(self, wasm): - flags = V8_NO_LIFTOFF_ARGS - if random.random() < 0.5: - flags += ['--no-wasm-generic-wrapper'] - return super().run(wasm, extra_d8_flags=flags) + extra_d8_flags = V8_NO_LIFTOFF_ARGS + class Wasm2C: name = 'wasm2c' From 8dc8547db7ad1a5b05c05a46561b85439ddd83e0 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 13:35:17 -0700 Subject: [PATCH 13/39] wrk --- scripts/fuzz_opt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index be574053bfb..815cc5e118d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2095,7 +2095,11 @@ def get_relevant_lines(wat): # 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. -# TODO: Reduction how? +# +# This reads wasm+js combinations from the test/js_wasm directory, so as new +# testcases are added there, this will fuzz them. +# +# TODO: Add a good way to reduce these testcases. class PreserveImportsExportsJS(TestCaseHandler): frequency = 1 From 966f784c677830092c5e221e46fa0a962173e0e4 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 15:31:48 -0700 Subject: [PATCH 14/39] testing --- scripts/fuzz_opt.py | 1 + test/js_wasm/exhaustive.mjs | 58 +++++++++++++ test/js_wasm/exhaustive.wat | 163 ++++++++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 test/js_wasm/exhaustive.mjs create mode 100644 test/js_wasm/exhaustive.wat diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 815cc5e118d..56242338741 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2109,6 +2109,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # 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 diff --git a/test/js_wasm/exhaustive.mjs b/test/js_wasm/exhaustive.mjs new file mode 100644 index 00000000000..6cc8d765a9b --- /dev/null +++ b/test/js_wasm/exhaustive.mjs @@ -0,0 +1,58 @@ +// exhaustive.mjs + +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 diff --git a/test/js_wasm/exhaustive.wat b/test/js_wasm/exhaustive.wat new file mode 100644 index 00000000000..68861143ce8 --- /dev/null +++ b/test/js_wasm/exhaustive.wat @@ -0,0 +1,163 @@ +(module + (rec + (type $Base (sub (descriptor $Base.vtable) (struct (field $val (mut i32))))) + (type $Base.vtable (sub (describes $Base) (struct + (field $proto (ref extern)) + (field $getValue (ref $getValue_t)) + (field $setValue (ref $setValue_t)) + ))) + (type $getValue_t (func (param (ref null $Base)) (result i32))) + (type $setValue_t (func (param (ref null $Base)) (param i32))) + + (type $Derived (sub $Base (descriptor $Derived.vtable) (struct (field $val (mut i32)) (field $extra i32)))) + (type $Derived.vtable (sub $Base.vtable (describes $Derived) (struct + (field $proto (ref extern)) + (field $getValue (ref $getValue_t)) + (field $setValue (ref $setValue_t)) + (field $getExtra (ref $getExtra_t)) + ))) + (type $getExtra_t (func (param (ref null $Derived)) (result i32))) + (type $staticMethod_t (func (result i32))) + ) + + (type $newBase_t (func (param i32) (result (ref $Base)))) + (type $newDerived_t (func (param i32 i32) (result (ref $Derived)))) + + ;; Types for prototype configuration + (type $prototypes (array (mut externref))) + (type $functions (array (mut funcref))) + (type $data (array (mut i8))) + (type $configureAll (func (param (ref null $prototypes)) + (param (ref null $functions)) + (param (ref null $data)) + (param externref))) + + (import "protos" "Base.proto" (global $Base.proto (ref extern))) + (import "protos" "Derived.proto" (global $Derived.proto (ref extern))) + + (import "env" "constructors" (global $constructors externref)) + + (import "wasm:js-prototypes" "configureAll" + (func $configureAll (type $configureAll))) + + (elem $prototypes externref + (global.get $Base.proto) + (global.get $Derived.proto) + ) + + (elem $functions funcref + (ref.func $Base.new) + (ref.func $Base.getValue) + (ref.func $Base.getValue) + (ref.func $Base.setValue) + (ref.func $Derived.new) + (ref.func $Derived.staticMethod) + (ref.func $Derived.getExtra) + ) + + ;; \02 - 2 protoconfigs + ;; Base: + ;; \01 - 1 constructorconfig + ;; \04Base - "Base" + ;; \00 - 0 static methods + ;; \03 - 3 methodconfigs + ;; \00\08getValue - method "getValue" + ;; \01\05value - getter "value" + ;; \02\05value - setter "value" + ;; \7f - parentidx -1 + ;; Derived: + ;; \01 - 1 constructorconfig + ;; \07Derived - "Derived" + ;; \01 - 1 static method + ;; \00\0cstaticMethod + ;; \01 - 1 methodconfig + ;; \00\08getExtra + ;; \00 - parentidx 0 (Base) + (data $data "\02\01\04Base\00\03\00\08getValue\01\05value\02\05value\7f\01\07Derived\01\00\0cstaticMethod\01\00\08getExtra\00") + + (global $Base.vtable (export "Base.vtable") (ref (exact $Base.vtable)) + (struct.new $Base.vtable + (global.get $Base.proto) + (ref.func $Base.getValue) + (ref.func $Base.setValue) + ) + ) + + (global $Derived.vtable (export "Derived.vtable") (ref (exact $Derived.vtable)) + (struct.new $Derived.vtable + (global.get $Derived.proto) + (ref.func $Base.getValue) + (ref.func $Base.setValue) + (ref.func $Derived.getExtra) + ) + ) + + (func $Base.new (type $newBase_t) (param $val i32) (result (ref $Base)) + (struct.new_desc $Base + (local.get $val) + (global.get $Base.vtable) + ) + ) + + (func $Base.getValue (type $getValue_t) (param $this (ref null $Base)) (result i32) + (struct.get $Base $val (local.get $this)) + ) + + (func $Base.setValue (type $setValue_t) (param $this (ref null $Base)) (param $val i32) + (struct.set $Base $val (local.get $this) (local.get $val)) + ) + + (func $Derived.new (type $newDerived_t) (param $val i32) (param $extra i32) (result (ref $Derived)) + (struct.new_desc $Derived + (local.get $val) + (local.get $extra) + (global.get $Derived.vtable) + ) + ) + + (func $Derived.getExtra (type $getExtra_t) (param $this (ref null $Derived)) (result i32) + (struct.get $Derived $extra (local.get $this)) + ) + + (func $Derived.staticMethod (type $staticMethod_t) (result i32) + (i32.const 42) + ) + + (func $start + (call $configureAll + (array.new_elem $prototypes $prototypes (i32.const 0) (i32.const 2)) + (array.new_elem $functions $functions (i32.const 0) (i32.const 7)) + (array.new_data $data $data (i32.const 0) (i32.const 70)) + (global.get $constructors) + ) + ) + + (start $start) + + ;; Additional tests for descriptor instructions + (func (export "get_base_val") (param $b (ref $Base)) (result i32) + (call $Base.getValue (local.get $b)) + ) + + (func (export "checkDesc") (param $b (ref $Base)) (result i32) + (block $derived (result (ref $Derived)) + (block $base (result (ref $Base)) + (br_on_cast_desc_eq $base (ref $Base) (ref $Base) (local.get $b) (global.get $Base.vtable)) + (br_on_cast_desc_eq $derived (ref $Base) (ref $Derived) (local.get $b) (global.get $Derived.vtable)) + (return (i32.const 0)) + ) + (drop) + (return (i32.const 1)) + ) + (drop) + (return (i32.const 2)) + ) + + (func (export "isDerived") (param $b (ref $Base)) (result i32) + (if (result i32) + (ref.test (ref $Derived) (local.get $b)) + (then (i32.const 1)) + (else (i32.const 0)) + ) + ) +) From 1c13ed0eeb1da8ebb4cdcb8fa4f8077b9e2a87fe Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 16:04:41 -0700 Subject: [PATCH 15/39] moar --- test/js_wasm/interop_extra.mjs | 94 ++++++++++++++++++ test/js_wasm/interop_extra.wat | 169 +++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 test/js_wasm/interop_extra.mjs create mode 100644 test/js_wasm/interop_extra.wat diff --git a/test/js_wasm/interop_extra.mjs b/test/js_wasm/interop_extra.mjs new file mode 100644 index 00000000000..c89e7c0f883 --- /dev/null +++ b/test/js_wasm/interop_extra.mjs @@ -0,0 +1,94 @@ +// interop_extra.mjs + +let protoFactory = new Proxy({}, { + get(target, prop, receiver) { + // Always return a fresh, empty object. + return {}; + } +}); + +let constructors = {}; + +let imports = { + "protos": protoFactory, + "env": { + constructors, + exact_func: (x) => x + 100, + }, +}; + +let compileOptions = { builtins: ["js-prototypes"] }; + +let buffer = readbuffer(arguments[0]); + +let { module, instance } = + await WebAssembly.instantiate(buffer, imports, compileOptions); + +// Test exact function import +console.log("call_exact(5):", instance.exports.call_exact(5)); + +// Test A (no constructor, just methods on prototype) +let a = instance.exports.newA(10); +console.log("a.getA():", a.getA()); +console.log("Object.getPrototypeOf(a) exists:", !!Object.getPrototypeOf(a)); + +// Test B (inherits from A) +let B = constructors.B; +let b = new B(20, 30); +console.log("b.getA():", b.getA()); +console.log("b.getB():", b.getB()); +console.log("b instanceof B:", b instanceof B); +console.log("b instanceof constructors.B:", b instanceof constructors.B); + +// Test C (inherits from B) +let C = constructors.C; +let c = new C(40, 50, 60); +console.log("c.getA():", c.getA()); +console.log("c.getB():", c.getB()); +console.log("c.getC():", c.getC()); +console.log("c instanceof C:", c instanceof C); +console.log("c instanceof B:", c instanceof B); +console.log("C.s1():", C.s1()); +console.log("C.s2():", C.s2()); + +// Test Meta-descriptor +let Meta = constructors.Meta; +let m = new Meta(70); +console.log("m.getM():", m.getM()); + +let mDesc = instance.exports.get_meta_desc(m); +console.log("mDesc.getVal():", mDesc.getVal()); +console.log("mDesc instanceof Object:", mDesc instanceof Object); +// The descriptor itself has a prototype configured! +console.log("mDesc.getVal inherited:", !!mDesc.getVal); + +// Test NoProto (invalid prototype source in descriptor) +let noProto = instance.exports.newNoProto(80); +try { + console.log("Object.getPrototypeOf(noProto):", Object.getPrototypeOf(noProto)); +} catch (e) { + console.log("Object.getPrototypeOf(noProto) threw:", e.name); +} + +// Test cast instructions +let bVtable = instance.exports.get_B_vtable(); +try { + let castedB = instance.exports.test_cast_desc_eq(b, bVtable); + console.log("test_cast_desc_eq(b, bVtable) succeeded:", !!castedB); +} catch (e) { + console.log("test_cast_desc_eq(b, bVtable) failed:", e.name); +} + +try { + instance.exports.test_cast_desc_eq(a, bVtable); + console.log("test_cast_desc_eq(a, bVtable) succeeded unexpectedly"); +} catch (e) { + console.log("test_cast_desc_eq(a, bVtable) failed as expected:", e.name); +} + +console.log("test_br_on_cast_desc_eq_fail(b, bVtable):", instance.exports.test_br_on_cast_desc_eq_fail(b, bVtable)); +console.log("test_br_on_cast_desc_eq_fail(a, bVtable):", instance.exports.test_br_on_cast_desc_eq_fail(a, bVtable)); + +// Test newDefault +let def = instance.exports.newDefault(); +console.log("newDefault exists:", !!def); diff --git a/test/js_wasm/interop_extra.wat b/test/js_wasm/interop_extra.wat new file mode 100644 index 00000000000..6819fcb4064 --- /dev/null +++ b/test/js_wasm/interop_extra.wat @@ -0,0 +1,169 @@ +(module + (rec + ;; A -> B -> C inheritance chain + (type $A (sub (descriptor $A.desc) (struct (field $a (mut i32))))) + (type $A.desc (sub (describes $A) (struct (field $proto (ref extern)) (field $getA (ref $getA_t))))) + (type $getA_t (func (param (ref null $A)) (result i32))) + + (type $B (sub $A (descriptor $B.desc) (struct (field $a (mut i32)) (field $b (mut i32))))) + (type $B.desc (sub $A.desc (describes $B) (struct (field $proto (ref extern)) (field $getA (ref $getA_t)) (field $getB (ref $getB_t))))) + (type $getB_t (func (param (ref null $B)) (result i32))) + + (type $C (sub $B (descriptor $C.desc) (struct (field $a (mut i32)) (field $b (mut i32)) (field $c (mut i32))))) + (type $C.desc (sub $B.desc (describes $C) (struct (field $proto (ref extern)) (field $getA (ref $getA_t)) (field $getB (ref $getB_t)) (field $getC (ref $getC_t))))) + (type $getC_t (func (param (ref null $C)) (result i32))) + + ;; Type with meta-descriptor (descriptor for the descriptor) + (type $Meta (sub (descriptor $Meta.desc) (struct (field $m (mut i32))))) + (type $Meta.desc (sub (describes $Meta) (descriptor $Meta.meta-desc) (struct (field $proto (ref extern)) (field $val i32) (field $getM (ref $getM_t))))) + (type $getM_t (func (param (ref null $Meta)) (result i32))) + (type $Meta.meta-desc (sub (describes $Meta.desc) (struct (field $proto (ref extern)) (field $metaVal i32) (field $getVal (ref $getVal_t))))) + (type $getVal_t (func (param (ref null $Meta.desc)) (result i32))) + + ;; Type with invalid prototype source (first field is i32, not externref) + (type $NoProto (sub (descriptor $NoProto.desc) (struct (field $x i32)))) + (type $NoProto.desc (sub (describes $NoProto) (struct (field $val i32)))) + + ;; Test struct.new_default_desc + (type $Default (sub (descriptor $Default.desc) (struct (field i32)))) + (type $Default.desc (sub (describes $Default) (struct (field (ref extern))))) + ) + + ;; Non-recursive types should be outside the rec group for builtin compatibility + (type $exact_f_t (func (param i32) (result i32))) + (type $newB_t (func (param i32 i32) (result (ref $B)))) + (type $newC_t (func (param i32 i32 i32) (result (ref $C)))) + (type $newMeta_t (func (param i32) (result (ref $Meta)))) + + ;; Types for configureAll + (type $prototypes (array (mut externref))) + (type $functions (array (mut funcref))) + (type $data (array (mut i8))) + (type $configureAll (func (param (ref null $prototypes)) + (param (ref null $functions)) + (param (ref null $data)) + (param externref))) + + (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll))) + (import "env" "constructors" (global $constructors externref)) + (import "protos" "A.proto" (global $A.proto (ref extern))) + (import "protos" "B.proto" (global $B.proto (ref extern))) + (import "protos" "C.proto" (global $C.proto (ref extern))) + (import "protos" "Meta.proto" (global $Meta.proto (ref extern))) + (import "protos" "MetaDesc.proto" (global $MetaDesc.proto (ref extern))) + + ;; Exact function import test + (import "env" "exact_func" (func $exact_func (exact (type $exact_f_t)))) + + (elem $prototypes externref + (global.get $A.proto) + (global.get $B.proto) + (global.get $C.proto) + (global.get $Meta.proto) + (global.get $MetaDesc.proto) + ) + + (elem $functions funcref + (ref.func $A.getA) ;; 0: method for A + (ref.func $B.new) ;; 1: constructor for B + (ref.func $B.getB) ;; 2: method for B + (ref.func $C.new) ;; 3: constructor for C + (ref.func $static1) ;; 4: static 1 for C + (ref.func $static2) ;; 5: static 2 for C + (ref.func $C.getC) ;; 6: method for C + (ref.func $Meta.new) ;; 7: constructor for Meta + (ref.func $Meta.getM) ;; 8: method for Meta + (ref.func $MetaDesc.getVal) ;; 9: method for MetaDesc + ) + + ;; \05 (5 protoconfigs) + ;; 1. A: \00 (0 constructors) \01 (1 method) \00\04getA \7f (parent -1) + ;; 2. B: \01 (1 constructor) \01B \00 (0 static) \01 (1 method) \00\04getB \00 (parent 0) + ;; 3. C: \01 (1 constructor) \01C \02 (2 static) \00\02s1 \00\02s2 \01 (1 method) \00\04getC \01 (parent 1) + ;; 4. Meta: \01 (1 constructor) \04Meta \00 (0 static) \01 (1 method) \00\04getM \7f (parent -1) + ;; 5. MetaDesc: \00 (0 constructors) \01 (1 method) \00\06getVal \7f (parent -1) + (data $data "\05\00\01\00\04getA\7f\01\01B\00\01\00\04getB\00\01\01C\02\00\02s1\00\02s2\01\00\04getC\01\01\04Meta\00\01\00\04getM\7f\00\01\00\06getVal\7f") + + (global $A.vtable (ref (exact $A.desc)) + (struct.new $A.desc (global.get $A.proto) (ref.func $A.getA)) + ) + (global $B.vtable (ref (exact $B.desc)) + (struct.new $B.desc (global.get $B.proto) (ref.func $A.getA) (ref.func $B.getB)) + ) + (global $C.vtable (ref (exact $C.desc)) + (struct.new $C.desc (global.get $C.proto) (ref.func $A.getA) (ref.func $B.getB) (ref.func $C.getC)) + ) + + (global $Meta.meta-vtable (ref (exact $Meta.meta-desc)) + (struct.new $Meta.meta-desc (global.get $MetaDesc.proto) (i32.const 42) (ref.func $MetaDesc.getVal)) + ) + (global $Meta.vtable (ref (exact $Meta.desc)) + (struct.new_desc $Meta.desc (global.get $Meta.proto) (i32.const 100) (ref.func $Meta.getM) (global.get $Meta.meta-vtable)) + ) + + (global $NoProto.vtable (ref (exact $NoProto.desc)) + (struct.new $NoProto.desc (i32.const 123)) + ) + + (func $A.getA (type $getA_t) (param $this (ref null $A)) (result i32) (struct.get $A $a (local.get $this))) + (func $B.getB (type $getB_t) (param $this (ref null $B)) (result i32) (struct.get $B $b (local.get $this))) + (func $C.getC (type $getC_t) (param $this (ref null $C)) (result i32) (struct.get $C $c (local.get $this))) + (func $Meta.getM (type $getM_t) (param $this (ref null $Meta)) (result i32) (struct.get $Meta $m (local.get $this))) + (func $MetaDesc.getVal (type $getVal_t) (param $this (ref null $Meta.desc)) (result i32) (struct.get $Meta.desc $val (local.get $this))) + (func $static1 (result i32) (i32.const 1)) + (func $static2 (result i32) (i32.const 2)) + + (func $A.new (export "newA") (param $a i32) (result (ref $A)) + (struct.new_desc $A (local.get $a) (global.get $A.vtable)) + ) + (func $B.new (type $newB_t) (param $a i32) (param $b i32) (result (ref $B)) + (struct.new_desc $B (local.get $a) (local.get $b) (global.get $B.vtable)) + ) + (func $C.new (type $newC_t) (param $a i32) (param $b i32) (param $c i32) (result (ref $C)) + (struct.new_desc $C (local.get $a) (local.get $b) (local.get $c) (global.get $C.vtable)) + ) + (func $Meta.new (type $newMeta_t) (param $m i32) (result (ref $Meta)) + (struct.new_desc $Meta (local.get $m) (global.get $Meta.vtable)) + ) + (func $NoProto.new (export "newNoProto") (param $x i32) (result (ref $NoProto)) + (struct.new_desc $NoProto (local.get $x) (global.get $NoProto.vtable)) + ) + + (func $start + (call $configureAll + (array.new_elem $prototypes $prototypes (i32.const 0) (i32.const 5)) + (array.new_elem $functions $functions (i32.const 0) (i32.const 10)) + (array.new_data $data $data (i32.const 0) (i32.const 68)) + (global.get $constructors) + ) + ) + (start $start) + + (func (export "get_meta_desc") (param $m (ref $Meta)) (result (ref $Meta.desc)) + (ref.get_desc $Meta (local.get $m)) + ) + + (func (export "test_cast_desc_eq") (param $a (ref $A)) (param $desc (ref (exact $B.desc))) (result (ref null $B)) + (ref.cast_desc_eq (ref null $B) (local.get $a) (local.get $desc)) + ) + + (func (export "test_br_on_cast_desc_eq_fail") (param $a (ref $A)) (param $desc (ref (exact $B.desc))) (result i32) + (block $fail (result (ref $A)) + (drop (br_on_cast_desc_eq_fail $fail (ref $A) (ref $B) (local.get $a) (local.get $desc))) + (return (i32.const 1)) + ) + (drop) + (return (i32.const 0)) + ) + + (func (export "get_B_vtable") (result (ref (exact $B.desc))) (global.get $B.vtable)) + + (global $Default.vtable (ref (exact $Default.desc)) (struct.new $Default.desc (global.get $A.proto))) + (func (export "newDefault") (result (ref $Default)) + (struct.new_default_desc $Default (global.get $Default.vtable)) + ) + + (func (export "call_exact") (param i32) (result i32) + (call $exact_func (local.get 0)) + ) +) From 2aa1b9a7eb6d08c54106cdc0d67a25a0c428e591 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 16:56:36 -0700 Subject: [PATCH 16/39] testing --- scripts/fuzz_opt.py | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 56242338741..d29ec84d01f 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -607,7 +607,7 @@ def note_ignored_vm_run(reason, extra_text='', amount=1): # Run a VM command, and filter out known issues. -def run_vm(cmd): +def run_vm(cmd, checked=True): def filter_known_issues(output): known_issues = [ # can be caused by flatten, ssa, etc. passes @@ -650,7 +650,11 @@ def filter_known_issues(output): try: # some known issues do not cause the entire process to fail - return filter_known_issues(run(cmd)) + if checked: + ret = run(cmd) + else: + ret = run_unchecked(cmd) + return filter_known_issues(ret) except subprocess.CalledProcessError: # other known issues do make it fail, so re-run without checking for # success and see if we should ignore it @@ -833,8 +837,8 @@ class D8: extra_d8_flags = [] - def run_js(self, js, wasm): - return run_vm([shared.V8, js] + shared.V8_OPTS + get_v8_extra_flags() + self.extra_d8_flags + ['--', wasm]) + def run_js(self, js, wasm, checked=True): + return run_vm([shared.V8, js] + shared.V8_OPTS + get_v8_extra_flags() + self.extra_d8_flags + ['--', wasm], checked=checked) def run(self, wasm): return self.run(js=get_fuzz_shell_js(), wasm=wasm) @@ -2163,14 +2167,26 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # testcase simply traps, in which case we mark it as such, and test that it # traps both before and after. def do_run(self, vm, js, wasm): - try: - return vm.run_js(js, wasm) - except Exception as e: - # We run this twice (before and after opts), so increment 0.5 each - # time. - note_ignored_vm_run('PreserveImportsExportsJS trap', - amount=0.5) - return 'JS exception' # TODO: can we keep some output? + # TODO: filter error stuff, both to error on identical errors before and + # after, and to remove stack traces that add random diffs. + out = vm.run_js(js, wasm, checked=False) + cleaned = [] + for line in out.splitlines(): + if ': RuntimeError:' 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. + line = 'TRAP' + elif 'at wasm://' in line: + # This is part of an error like + # + # at wasm://wasm/12345678:wasm-function[42]:0x123 + # + line = '(stack trace)' + cleaned.append(line) + return '\n'.join(cleaned) def can_run_on_wasm(self, wasm): return all_disallowed(DISALLOWED_FEATURES_IN_V8) From 9bb9056fdca5cb06bcb338da07aa011a9950f13b Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 16:57:50 -0700 Subject: [PATCH 17/39] testing --- scripts/fuzz_opt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index d29ec84d01f..5040c4c195d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2172,7 +2172,7 @@ 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: + if ': RuntimeError:' in line or ': TypeError:' in line: # This is part of an error like # # wasm-function[2]:0x273: RuntimeError: unreachable From d9a463ca282089fb885636b8d092175408618d46 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 17:00:21 -0700 Subject: [PATCH 18/39] testing --- scripts/fuzz_opt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 5040c4c195d..4c2e4c722e5 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2185,6 +2185,10 @@ def do_run(self, vm, js, wasm): # at wasm://wasm/12345678:wasm-function[42]:0x123 # line = '(stack trace)' + elif '()' in line: + # Anonymous parts of stack traces sometimes differ, due to + # inlining. + line = '' cleaned.append(line) return '\n'.join(cleaned) From 89c9d7d3e65211beda82611b6d7955f7e9d27000 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 17:01:10 -0700 Subject: [PATCH 19/39] testing --- scripts/fuzz_opt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 4c2e4c722e5..d11ddc10cff 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2188,7 +2188,7 @@ def do_run(self, vm, js, wasm): elif '()' in line: # Anonymous parts of stack traces sometimes differ, due to # inlining. - line = '' + continue cleaned.append(line) return '\n'.join(cleaned) From 12360627d52c69115ddcd8f6e21a80fc086d3c0a Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 17:04:14 -0700 Subject: [PATCH 20/39] testing --- scripts/fuzz_opt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index d11ddc10cff..cfee2a8176d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2172,12 +2172,14 @@ 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: + 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 ignore the binary location, which opts can change. We + # must also remove the specific trap, as Binaryen can change + # that. line = 'TRAP' elif 'at wasm://' in line: # This is part of an error like From 5b47101b89ef2318bc5044a501b91cbae350e874 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 07:46:16 -0700 Subject: [PATCH 21/39] testing --- scripts/fuzz_opt.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index cfee2a8176d..36ed3b79a88 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2181,15 +2181,13 @@ def do_run(self, vm, js, wasm): # must also remove the specific trap, as Binaryen can change # that. line = 'TRAP' - elif 'at wasm://' in line: - # This is part of an error like + elif 'at wasm://' in line or '()' in line: + # This is part of a stack trace like # # at wasm://wasm/12345678:wasm-function[42]:0x123 + # at () # - line = '(stack trace)' - elif '()' in line: - # Anonymous parts of stack traces sometimes differ, due to - # inlining. + # Ignore it, as traces differ based on optimizations. continue cleaned.append(line) return '\n'.join(cleaned) From 1d7965672d8bab0614004d3030c5611e555c358a Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 08:38:51 -0700 Subject: [PATCH 22/39] testing --- scripts/fuzz_opt.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 36ed3b79a88..d9e70ea4c94 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2108,7 +2108,14 @@ class PreserveImportsExportsJS(TestCaseHandler): frequency = 1 def handle_pair(self, input, before_wasm, after_wasm, opts): - # TODO: We could allow less than INPUT_SIZE_MIN for size(input) + # 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')) @@ -2131,7 +2138,8 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): '-ttf', '--fuzz-preserve-imports-exports', '--initial-fuzz=' + wat_file, - '-o', pre_wasm + '-o', pre_wasm, + '-g', ]) # Pick vm and run before we optimize the wasm. @@ -2167,8 +2175,6 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # testcase simply traps, in which case we mark it as such, and test that it # traps both before and after. def do_run(self, vm, js, wasm): - # TODO: filter error stuff, both to error on identical errors before and - # after, and to remove stack traces that add random diffs. out = vm.run_js(js, wasm, checked=False) cleaned = [] for line in out.splitlines(): From 33013c307add350bc267c6759fe97f0b5eeea440 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 08:39:48 -0700 Subject: [PATCH 23/39] testing --- scripts/fuzz_opt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index d9e70ea4c94..538b50465c4 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2187,7 +2187,7 @@ def do_run(self, vm, js, wasm): # must also remove the specific trap, as Binaryen can change # that. line = 'TRAP' - elif 'at wasm://' in line or '()' in line: + elif 'wasm://' in line or '()' in line: # This is part of a stack trace like # # at wasm://wasm/12345678:wasm-function[42]:0x123 From 327e374c2f578988f6d81064854f41c9d519c94f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 11:28:54 -0700 Subject: [PATCH 24/39] undo --- scripts/fuzz_opt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 538b50465c4..eb652d127eb 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -693,6 +693,7 @@ def get_v8_extra_flags(): if random.random() < 0.5: flags += ['--wasm-assert-types'] + # Some other options make sense to use sometimes. if random.random() < 0.5: flags += ['--no-wasm-generic-wrapper'] @@ -791,9 +792,9 @@ class FuzzExec(TestCaseHandler): def handle_pair(self, input, before_wasm, after_wasm, opts): run([in_bin('wasm-opt'), before_wasm] + opts + ['--fuzz-exec']) - # VMs + class BinaryenInterpreter: name = 'binaryen interpreter' @@ -832,6 +833,7 @@ def can_compare_to_self(self): def can_compare_to_other(self, other): return True + class D8: name = 'd8' @@ -861,6 +863,7 @@ def can_compare_to_other(self, other): # compare to others. return self.can_compare_to_self() and LEGALIZE + class D8Liftoff(D8): name = 'd8_liftoff' @@ -917,6 +920,7 @@ def can_compare_to_other(self, other): # C won't trap on OOB, and NaNs can differ from wasm VMs return not OOB and not NANS + class Wasm2C2Wasm(Wasm2C): name = 'wasm2c2wasm' From a2ee6fb09ccef2c457695bf3a4c19ca9cdb33ca9 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 11:29:08 -0700 Subject: [PATCH 25/39] testing --- scripts/fuzz_opt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index eb652d127eb..d6d8410dc76 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -843,7 +843,7 @@ def run_js(self, js, wasm, checked=True): return run_vm([shared.V8, js] + shared.V8_OPTS + get_v8_extra_flags() + self.extra_d8_flags + ['--', wasm], checked=checked) def run(self, wasm): - return self.run(js=get_fuzz_shell_js(), wasm=wasm) + return self.run_js(js=get_fuzz_shell_js(), wasm=wasm) def can_run(self, wasm): return all_disallowed(DISALLOWED_FEATURES_IN_V8) From e13fba820fe18c15ef0570bdac735f96fdf2cd3d Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 12:54:21 -0700 Subject: [PATCH 26/39] restore --- scripts/fuzz_opt.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index d6d8410dc76..b9303d406be 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2422,7 +2422,20 @@ def handle(self, wasm): # The global list of all test case handlers testcase_handlers = [ + FuzzExec(), + CompareVMs(), + CheckDeterminism(), + Wasm2JS(), + TrapsNeverHappen(), + CtorEval(), + Merge(), + # Split(), # https://github.com/WebAssembly/binaryen/issues/8510 + RoundtripText(), + ClusterFuzz(), + Two(), + PreserveImportsExportsRandom(), PreserveImportsExportsJS(), + BranchHintPreservation(), ] From 709eee76331535cf524d1642ad5c147dcc79bcf6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 12:58:45 -0700 Subject: [PATCH 27/39] clean --- scripts/fuzz_opt.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index b9303d406be..5e80a1e5d19 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2102,7 +2102,7 @@ def get_relevant_lines(wat): # 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. +# 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. @@ -2146,7 +2146,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): '-g', ]) - # Pick vm and run before we optimize the wasm. + # Pick a vm and run before we optimize the wasm. vms = [ D8(), D8Liftoff(), @@ -2161,6 +2161,9 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 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 @@ -2175,9 +2178,6 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # Compare compare(pre, post, 'PreserveImportsExportsJS') - # Run a VM on a js+wasm combination, handling exceptions. It is possible the - # testcase simply traps, in which case we mark it as such, and test that it - # traps both before and after. def do_run(self, vm, js, wasm): out = vm.run_js(js, wasm, checked=False) cleaned = [] From fd2556b405e6e944a7572ad6ae49e152e7e0ccef Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 13:41:39 -0700 Subject: [PATCH 28/39] clean --- scripts/fuzz_opt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 5e80a1e5d19..39ed2b7124c 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2133,7 +2133,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): stderr=subprocess.PIPE, silent=True) except Exception: - note_ignored_vm_run('features not compatible with js+wasm') + note_ignored_vm_run('PreserveImportsExportsJS: features not compatible with js+wasm') return # Modify the initial wat to get the pre-optimizations wasm. @@ -2164,7 +2164,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # 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') + note_ignored_vm_run('PreserveImportsExportsJS: bad configureAll') return # Anything else is a problem. @@ -2180,6 +2180,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): 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: From bae026a2033b607fbbb865fdc72f6ce98658d231 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 14:32:08 -0700 Subject: [PATCH 29/39] testing --- scripts/fuzz_opt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 39ed2b7124c..bf54e78c4f4 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2158,6 +2158,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # 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, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if proc.returncode: if 'Invalid configureAll' in proc.stderr: From 08d20be9160a77d0f847efa5b110c0385f761229 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 10 Apr 2026 07:29:59 -0700 Subject: [PATCH 30/39] verify --- scripts/fuzz_opt.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index bf54e78c4f4..27a1fe7e5e1 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2136,6 +2136,12 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): 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') processed = run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [ From f50bd44533e580f6910ce294a8f1ccc9c3906c99 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 10 Apr 2026 08:29:52 -0700 Subject: [PATCH 31/39] fuzz --- scripts/fuzz_opt.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 27a1fe7e5e1..8703756f16d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2107,7 +2107,8 @@ def get_relevant_lines(wat): # This reads wasm+js combinations from the test/js_wasm directory, so as new # testcases are added there, this will fuzz them. # -# TODO: Add a good way to reduce these testcases. +# Note that bugs found by this fuzzer tend to require the following during +# reducing: BINARYEN_TRUST_GIVEN_WASM=1 in the env. TODO: simplify this class PreserveImportsExportsJS(TestCaseHandler): frequency = 1 @@ -2152,6 +2153,13 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): '-g', ]) + # 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(), From 345864c81c7ae46a9fbfecb061e62eb6c19d7a22 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 10 Apr 2026 08:48:09 -0700 Subject: [PATCH 32/39] fuzz --- scripts/fuzz_opt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 8703756f16d..b38047ae75d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2107,8 +2107,8 @@ def get_relevant_lines(wat): # 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 tend to require the following during -# reducing: BINARYEN_TRUST_GIVEN_WASM=1 in the env. TODO: simplify this +# 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 @@ -2159,6 +2159,10 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): if os.environ.get('BINARYEN_TRUST_GIVEN_WASM'): print('using given wasm', before_wasm) pre_wasm = before_wasm + else: + # Otherwise, overwrite before_wasm, so that if we end up reducing + # this, the fuzzer knows where to start. + shutil.copyfile(pre_wasm, before_wasm) # Pick a vm and run before we optimize the wasm. vms = [ From ddd50982388a5578ef7cefa12ef01565e1f1cf7f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 10 Apr 2026 09:47:29 -0700 Subject: [PATCH 33/39] rename --- scripts/test/fuzzing.py | 4 ++++ test/js_wasm/{exhaustive.mjs => js_interop_cases.mjs} | 0 test/js_wasm/{exhaustive.wat => js_interop_cases.wat} | 0 test/js_wasm/{interop_extra.mjs => js_interop_corners.mjs} | 0 test/js_wasm/{interop_extra.wat => js_interop_corners.wat} | 0 test/js_wasm/{counter.mjs => js_interop_counter.mjs} | 0 test/js_wasm/{counter.wat => js_interop_counter.wat} | 0 7 files changed, 4 insertions(+) rename test/js_wasm/{exhaustive.mjs => js_interop_cases.mjs} (100%) rename test/js_wasm/{exhaustive.wat => js_interop_cases.wat} (100%) rename test/js_wasm/{interop_extra.mjs => js_interop_corners.mjs} (100%) rename test/js_wasm/{interop_extra.wat => js_interop_corners.wat} (100%) rename test/js_wasm/{counter.mjs => js_interop_counter.mjs} (100%) rename test/js_wasm/{counter.wat => js_interop_counter.wat} (100%) diff --git a/scripts/test/fuzzing.py b/scripts/test/fuzzing.py index 29e4cf0e4ac..8a23262280d 100644 --- a/scripts/test/fuzzing.py +++ b/scripts/test/fuzzing.py @@ -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', ] diff --git a/test/js_wasm/exhaustive.mjs b/test/js_wasm/js_interop_cases.mjs similarity index 100% rename from test/js_wasm/exhaustive.mjs rename to test/js_wasm/js_interop_cases.mjs diff --git a/test/js_wasm/exhaustive.wat b/test/js_wasm/js_interop_cases.wat similarity index 100% rename from test/js_wasm/exhaustive.wat rename to test/js_wasm/js_interop_cases.wat diff --git a/test/js_wasm/interop_extra.mjs b/test/js_wasm/js_interop_corners.mjs similarity index 100% rename from test/js_wasm/interop_extra.mjs rename to test/js_wasm/js_interop_corners.mjs diff --git a/test/js_wasm/interop_extra.wat b/test/js_wasm/js_interop_corners.wat similarity index 100% rename from test/js_wasm/interop_extra.wat rename to test/js_wasm/js_interop_corners.wat diff --git a/test/js_wasm/counter.mjs b/test/js_wasm/js_interop_counter.mjs similarity index 100% rename from test/js_wasm/counter.mjs rename to test/js_wasm/js_interop_counter.mjs diff --git a/test/js_wasm/counter.wat b/test/js_wasm/js_interop_counter.wat similarity index 100% rename from test/js_wasm/counter.wat rename to test/js_wasm/js_interop_counter.wat From da781b2a1df6d62cfe1ee81be1949a6dd1517cff Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 10 Apr 2026 09:55:34 -0700 Subject: [PATCH 34/39] fix --- scripts/fuzz_opt.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index b38047ae75d..8a65c497374 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2113,6 +2113,17 @@ class PreserveImportsExportsJS(TestCaseHandler): frequency = 1 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) + + 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, @@ -2153,16 +2164,16 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): '-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 - else: - # Otherwise, overwrite before_wasm, so that if we end up reducing - # this, the fuzzer knows where to start. - shutil.copyfile(pre_wasm, before_wasm) # Pick a vm and run before we optimize the wasm. vms = [ From 83b302a3a2c52de2ce5f7f45af02e0224d6b397f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 10 Apr 2026 12:18:08 -0700 Subject: [PATCH 35/39] clean --- test/js_wasm/js_interop_cases.mjs | 2 -- test/js_wasm/js_interop_corners.mjs | 2 -- test/js_wasm/js_interop_corners.wat | 2 +- test/js_wasm/js_interop_counter.mjs | 2 -- test/js_wasm/js_interop_counter.wat | 2 -- 5 files changed, 1 insertion(+), 9 deletions(-) diff --git a/test/js_wasm/js_interop_cases.mjs b/test/js_wasm/js_interop_cases.mjs index 6cc8d765a9b..c54758b2b98 100644 --- a/test/js_wasm/js_interop_cases.mjs +++ b/test/js_wasm/js_interop_cases.mjs @@ -1,5 +1,3 @@ -// exhaustive.mjs - let protoFactory = new Proxy({}, { get(target, prop, receiver) { // Always return a fresh, empty object. diff --git a/test/js_wasm/js_interop_corners.mjs b/test/js_wasm/js_interop_corners.mjs index c89e7c0f883..75ed2feeb27 100644 --- a/test/js_wasm/js_interop_corners.mjs +++ b/test/js_wasm/js_interop_corners.mjs @@ -1,5 +1,3 @@ -// interop_extra.mjs - let protoFactory = new Proxy({}, { get(target, prop, receiver) { // Always return a fresh, empty object. diff --git a/test/js_wasm/js_interop_corners.wat b/test/js_wasm/js_interop_corners.wat index 6819fcb4064..330100dfc0e 100644 --- a/test/js_wasm/js_interop_corners.wat +++ b/test/js_wasm/js_interop_corners.wat @@ -29,7 +29,6 @@ (type $Default.desc (sub (describes $Default) (struct (field (ref extern))))) ) - ;; Non-recursive types should be outside the rec group for builtin compatibility (type $exact_f_t (func (param i32) (result i32))) (type $newB_t (func (param i32 i32) (result (ref $B)))) (type $newC_t (func (param i32 i32 i32) (result (ref $C)))) @@ -159,6 +158,7 @@ (func (export "get_B_vtable") (result (ref (exact $B.desc))) (global.get $B.vtable)) (global $Default.vtable (ref (exact $Default.desc)) (struct.new $Default.desc (global.get $A.proto))) + (func (export "newDefault") (result (ref $Default)) (struct.new_default_desc $Default (global.get $Default.vtable)) ) diff --git a/test/js_wasm/js_interop_counter.mjs b/test/js_wasm/js_interop_counter.mjs index 04e217a1198..3a646aa8f0a 100644 --- a/test/js_wasm/js_interop_counter.mjs +++ b/test/js_wasm/js_interop_counter.mjs @@ -1,7 +1,5 @@ // https://github.com/WebAssembly/custom-descriptors/blob/main/proposals/custom-descriptors/Overview.md -// counter.mjs - let protoFactory = new Proxy({}, { get(target, prop, receiver) { // Always return a fresh, empty object. diff --git a/test/js_wasm/js_interop_counter.wat b/test/js_wasm/js_interop_counter.wat index fe5af51c905..781816829ef 100644 --- a/test/js_wasm/js_interop_counter.wat +++ b/test/js_wasm/js_interop_counter.wat @@ -1,7 +1,5 @@ ;; https://github.com/WebAssembly/custom-descriptors/blob/main/proposals/custom-descriptors/Overview.md -;; counter.wasm - (module (rec (type $counter (descriptor $counter.vtable) (struct (field $val (mut i32)))) From fcbcfe87bfdce4fb0b3ab4d0518ac3abacc2fe13 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 10 Apr 2026 14:31:34 -0700 Subject: [PATCH 36/39] oops --- scripts/fuzz_opt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 8a65c497374..e2669981167 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2122,6 +2122,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # 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 From 299d254ed47817e0cbf1d8d440a918f25e324f65 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 10 Apr 2026 14:32:03 -0700 Subject: [PATCH 37/39] lint --- scripts/fuzz_opt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index e2669981167..746226e7a8e 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2157,7 +2157,7 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts): # Modify the initial wat to get the pre-optimizations wasm. pre_wasm = abspath('pre.wasm') - processed = run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [ + run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [ '-ttf', '--fuzz-preserve-imports-exports', '--initial-fuzz=' + wat_file, From f11871cc5cb5ce664f3da07fd3f5f58923a951e9 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 10 Apr 2026 14:38:16 -0700 Subject: [PATCH 38/39] sort imports --- scripts/fuzz_opt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 746226e7a8e..b7d7f5c5b7d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -28,10 +28,10 @@ import contextlib import difflib -import pathlib import json import math import os +import pathlib import random import re import shutil From 0c1b9cda9d0525e59c1c3249501b30cd265170d4 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 10 Apr 2026 14:39:24 -0700 Subject: [PATCH 39/39] ruff --- scripts/fuzz_opt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index b7d7f5c5b7d..bf0892be681 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2189,7 +2189,7 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts): 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, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + 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.