From c50d2f52c7cad274322fc2db37fbe9dcbb1a112b Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 08:56:10 -0700 Subject: [PATCH 1/2] move --- scripts/fuzz_opt.py | 333 ++++++++++++++++++++++---------------------- 1 file changed, 167 insertions(+), 166 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 2f32eac59fb..58b8940bcee 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -786,6 +786,173 @@ 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' + + 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'] + 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 @@ -793,172 +960,6 @@ class CompareVMs(TestCaseHandler): 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'] - 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() From 3f0dc3d52acfa4e21440b1bce276bd17651fdb23 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 10:25:19 -0700 Subject: [PATCH 2/2] format --- scripts/fuzz_opt.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 58b8940bcee..83d351d67f1 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -788,6 +788,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): # VMs + class BinaryenInterpreter: name = 'binaryen interpreter' @@ -826,6 +827,7 @@ def can_compare_to_self(self): def can_compare_to_other(self, other): return True + class D8: name = 'd8' @@ -850,12 +852,14 @@ def can_compare_to_other(self, other): # 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' @@ -863,6 +867,7 @@ def run(self, wasm): flags = ['--no-liftoff'] return super().run(wasm, extra_d8_flags=flags) + class Wasm2C: name = 'wasm2c' @@ -907,6 +912,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' @@ -954,6 +960,7 @@ def can_compare_to_other(self, other): # NaNs can differ from wasm VMs return not NANS + class CompareVMs(TestCaseHandler): frequency = 1