Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
d2615d9
go
kripken May 13, 2026
f8d9cf8
fmrt
kripken May 13, 2026
a8fff04
fix
kripken May 13, 2026
d1a7722
fix
kripken May 13, 2026
dc8b327
fix
kripken May 13, 2026
eb6b0a7
fix
kripken May 13, 2026
7b55ead
fix
kripken May 13, 2026
c02bb91
fix
kripken May 13, 2026
a52568a
fix
kripken May 13, 2026
b6c1171
fix
kripken May 13, 2026
7b4a11b
fix
kripken May 13, 2026
4dde53b
fix
kripken May 13, 2026
684b058
fix
kripken May 14, 2026
5f08a1f
fix
kripken May 14, 2026
4a1a9a0
fix
kripken May 14, 2026
b5bed35
fix
kripken May 14, 2026
c62d304
go
kripken May 14, 2026
07c2def
fix
kripken May 14, 2026
1e1b500
fix
kripken May 14, 2026
46204f8
fix
kripken May 14, 2026
9d2c970
fix
kripken May 14, 2026
a34be63
Merge remote-tracking branch 'origin/main' into fuzz.start
kripken May 20, 2026
2edd71a
10
kripken May 20, 2026
11880d4
filter imports earlier
kripken May 20, 2026
d416b5c
part
kripken May 22, 2026
2bdf9ed
Merge remote-tracking branch 'origin/main' into fuzz.start.2
kripken May 22, 2026
9b636db
go
kripken May 22, 2026
859cef6
test
kripken May 22, 2026
54d0406
no
kripken May 22, 2026
7adb577
yes
kripken May 22, 2026
0149b4f
format
kripken May 22, 2026
cf02595
work
kripken May 22, 2026
2f29a8b
better
kripken May 22, 2026
c6e6beb
fix
kripken May 22, 2026
fe425ec
fix
kripken May 22, 2026
15c27ae
fix
kripken May 22, 2026
565855e
fix
kripken May 22, 2026
e494802
update
kripken May 22, 2026
d4232de
fix
kripken May 22, 2026
879befa
fix
kripken May 22, 2026
61e4561
fix
kripken May 22, 2026
b46210d
fix
kripken May 22, 2026
d1f9278
fix
kripken May 22, 2026
7456512
fix
kripken May 22, 2026
96bb9f2
fix
kripken May 22, 2026
c3086e6
fix
kripken May 22, 2026
b21c27c
fix
kripken May 22, 2026
989038a
fix
kripken May 26, 2026
438ed70
Merge remote-tracking branch 'origin/main' into fuzz.start.2
kripken May 26, 2026
33194d3
fix
kripken May 27, 2026
00732e0
Merge remote-tracking branch 'origin/main' into fuzz.start.2
kripken May 27, 2026
2845a66
go
kripken May 27, 2026
11408bb
frm
kripken May 27, 2026
dd8184a
fix
kripken May 28, 2026
80503ae
go
kripken May 28, 2026
e8529ac
updater
kripken May 28, 2026
6838d0f
go
kripken May 28, 2026
85149dd
m
kripken May 28, 2026
2947d38
go
kripken May 28, 2026
db71798
fix
kripken May 28, 2026
f420f24
finish
kripken May 28, 2026
ad600b7
Merge remote-tracking branch 'origin/main' into fuzz.start.2
kripken May 28, 2026
cd51971
fix
kripken May 28, 2026
a579620
fix
kripken May 28, 2026
4cd5091
fix
kripken May 28, 2026
8edcadc
done
kripken May 28, 2026
f1e15af
ruff
kripken May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/auto_update_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def update_example_tests():
subprocess.check_call(cmd)
print('run...', output_file)
proc = subprocess.run([output_file], capture_output=True)
assert proc.returncode == 0, [proc.returncode, proc.stderror, proc.stdout]
assert proc.returncode == 0, [proc.returncode, proc.stderr, proc.stdout]
actual = proc.stdout
with open(expected, 'wb') as o:
o.write(actual)
Expand Down
106 changes: 78 additions & 28 deletions scripts/fuzz_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,14 @@ def fix_double(x):
# Tag names may change due to opts, so canonicalize them.
out = re.sub(r' tag\$\d+', ' tag', out)

# If we failed to instantiate, remove everything after it. VMs may print
# additional error info that cannot be compared between VMs, like a JS
# stack trace for where we tried to instantiate.
if INSTANTIATE_ERROR in out:
after = out.find(INSTANTIATE_ERROR)
after = out.find('\n', after)
out = out[:after + 1]

lines = out.splitlines()
for i in range(len(lines)):
line = lines[i]
Expand All @@ -576,6 +584,9 @@ def fix_double(x):
# developer can see it.
print(line)
lines[i] = None
elif 'V8 is running with experimental features enabled. Stability and security will suffer.' in line:
# Ignore some boilerplate from VMs
lines[i] = None
elif EXCEPTION_PREFIX in line:
# exceptions may differ when optimizing, but an exception should
# occur, so ignore their types (also js engines print them out
Expand Down Expand Up @@ -657,10 +668,20 @@ def filter_known_issues(output):
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
# Other known issues do make it fail, so re-run without checking for
# success and see if we should ignore it
if filter_known_issues(run_unchecked(cmd)) == IGNORE:
raw = run_unchecked(cmd)
filtered = filter_known_issues(raw)
if filtered == IGNORE:
return IGNORE

# If we trap during instantiation, we do not need to ignore this run
# (we can see that all VMs have the same behavior), but we do need to
# not raise an error here.
if INSTANTIATE_ERROR in raw:
return raw

# Otherwise, raise an error.
raise


Expand Down Expand Up @@ -1091,7 +1112,15 @@ def handle_pair(self, input, before_wasm, after_wasm, opts):
# later make sense (if we don't do this, the wasm may have i64 exports).
# after applying other necessary fixes, we'll recreate the after wasm
# from scratch.
run([in_bin('wasm-opt'), before_wasm, '--legalize-and-prune-js-interface', '-o', before_wasm_temp] + FEATURE_OPTS)
run([
in_bin('wasm-opt'),
before_wasm,
'--legalize-and-prune-js-interface',
'-o', before_wasm_temp,
# Remove the start function for now, as this can lead to traps
# during start which this fuzzer doesn't handle yet. TODO
'--remove-start',
] + FEATURE_OPTS)
compare_before_to_after = random.random() < 0.5
compare_to_interpreter = compare_before_to_after and random.random() < 0.5
if compare_before_to_after:
Expand All @@ -1110,9 +1139,11 @@ def handle_pair(self, input, before_wasm, after_wasm, opts):
run([in_bin('wasm-opt'), before_wasm_temp, '-o', before_wasm_temp] + simplification_passes + FEATURE_OPTS)
# now that the before wasm is fixed up, generate a proper after wasm
run([in_bin('wasm-opt'), before_wasm_temp, '-o', after_wasm_temp] + opts + FEATURE_OPTS)
# always check for compiler crashes

# run before and after
before = self.run(before_wasm_temp)
after = self.run(after_wasm_temp)

if NANS:
# with NaNs we can't compare the output, as a reinterpret through
# memory might end up different in JS than wasm
Expand Down Expand Up @@ -1490,8 +1521,13 @@ def traps_in_instantiation(output):
if trap_index == -1:
# In "fixed" output, traps are replaced with *exception*.
trap_index = output.find('*exception*')
if trap_index == -1:
return False
# An exception can occur during the start function.
exception_index = output.find(EXCEPTION_PREFIX)
# Look at the first of a trap or an exception.
if exception_index >= 0 and (trap_index == -1 or exception_index < trap_index):
trap_index = exception_index
if trap_index == -1:
return False
export_index = output.find(FUZZ_EXEC_EXPORT_PREFIX)
if export_index == -1:
return True
Expand All @@ -1514,7 +1550,25 @@ def handle(self, wasm):
second_input = abspath('second_input.dat')
make_random_input(second_size, second_input)
second_wasm = abspath('second.wasm')
run([in_bin('wasm-opt'), second_input, '-ttf', '-o', second_wasm] + GEN_ARGS + FEATURE_OPTS)

# Always remove the second module's start function. Before merge, we
# have this:
#
# * call first's start
# * call first's exports
# * call second's start
# * call second's exports
#
# After merge, the middle two lines are swapped, since the starts are
# merged, changing the behavior.
second_args = [
in_bin('wasm-opt'),
second_input,
'-ttf',
'-o', second_wasm,
'--remove-start',
]
run(second_args + GEN_ARGS + FEATURE_OPTS)

# the second wasm file must not have an export that can influence our
# execution. the JS exports have that behavior, as when "table-set" is
Expand All @@ -1533,12 +1587,12 @@ def handle(self, wasm):
# second.wasm, but that is ok.
filter_exports(second_wasm, second_wasm, filtered, keep_defaults=False)

# sometimes also optimize the second module
# Sometimes also optimize the second module
if random.random() < 0.5:
opts = get_random_opts()
run([in_bin('wasm-opt'), second_wasm, '-o', second_wasm, '-all'] + FEATURE_OPTS + opts)

# merge the wasm files. note that we must pass -all, as even if the two
# Merge the wasm files. note that we must pass -all, as even if the two
# inputs are MVP, the output may have multiple tables and multiple
# memories (and we must also do that in the commands later down).
#
Expand Down Expand Up @@ -1574,20 +1628,10 @@ def handle(self, wasm):
if merged_output == IGNORE:
return

# If the second module traps in instantiation, then the merged module
# must do so as well, regardless of what the first module does. (In
# contrast, if the first module traps in instantiation, then the normal
# checks below will ensure the merged module does as well.)
if traps_in_instantiation(second_output) and \
not traps_in_instantiation(output):
# The merged module should also trap in instantiation, but the
# exports will not be called, so there's nothing else to compare.
if not traps_in_instantiation(merged_output):
raise Exception('expected merged module to trap during ' +
'instantiation because second module traps ' +
'during instantiation')
compare(merged_output, second_output, 'Merge: second module traps' +
' in instantiation')
# If either original module traps in instantiation, the merged module
# must do so as well.
if traps_in_instantiation(second_output) or traps_in_instantiation(output):
assert traps_in_instantiation(merged_output)
return

# a complication is that the second module's exports are appended, so we
Expand Down Expand Up @@ -1823,7 +1867,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts):
# (rarely, none might exist), unless we've decided to ignore the entire
# run, or if the wasm errored during instantiation, which can happen due
# to a testcase with a segment out of bounds, say.
if output != IGNORE and not output.startswith(INSTANTIATE_ERROR):
if output != IGNORE and INSTANTIATE_ERROR not in output:
# Do the work to find if there were function exports: extract the
# wasm from the JS, and process it.
run([sys.executable,
Expand Down Expand Up @@ -1910,6 +1954,9 @@ def handle(self, wasm):
# Most of the time, use the first wasm as an import to the second.
if random.random() < 0.8:
args += ['--fuzz-import=' + wasm]
# Always remove the second module's start function, see comment before
# in Merge.
args += ['--remove-start']

given = os.environ.get('BINARYEN_SECOND_WASM')
if not given:
Expand Down Expand Up @@ -2022,10 +2069,13 @@ def handle(self, wasm):
if output == IGNORE:
return

# We ruled out things we must ignore, like host limitations, and also
# exited earlier on a deterministic instantiation error, so there should
# be no such error in V8.
assert not output.startswith(INSTANTIATE_ERROR)
if INSTANTIATE_ERROR in output:
# We ruled out a bynterpreter instantiation error, but v8 might have
# one for a different reason (e.g. JS conversion error on the
# boundary, if the start function calls an import). Verify we have
# the same error without the second module, and skip.
assert INSTANTIATE_ERROR in run_d8_wasm(wasm)
return

output = fix_output(output)

Expand Down
3 changes: 1 addition & 2 deletions scripts/fuzz_shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,8 +520,7 @@ function build(binary, isSecond) {
try {
instance = new WebAssembly.Instance(module, imports);
} catch (e) {
console.log('exception thrown: failed to instantiate module: ' + e);
quit();
throw new Error('exception thrown: failed to instantiate module: ' + e);
}

// Do not add the second instance's exports to the list, as that would be
Expand Down
1 change: 1 addition & 0 deletions src/passes/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ set(passes_SOURCES
PrintCallGraph.cpp
PrintFeatures.cpp
PrintFunctionMap.cpp
RemoveStart.cpp
RoundTrip.cpp
SetGlobals.cpp
SignaturePruning.cpp
Expand Down
32 changes: 32 additions & 0 deletions src/passes/RemoveStart.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2026 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

//
// Removes the start function.
//

#include "pass.h"
#include "wasm.h"

namespace wasm {

struct RemoveStart : public Pass {
void run(Module* module) override { module->start = Name(); }
};

Pass* createRemoveStartPass() { return new RemoveStart(); }

} // namespace wasm
2 changes: 2 additions & 0 deletions src/passes/pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,8 @@ void PassRegistry::registerPasses() {
registerTestPass("randomize-branch-hints",
"randomize branch hints (for fuzzing)",
createRandomizeBranchHintsPass);
registerTestPass(
"remove-start", "remove the start function", createRemoveStartPass);
registerTestPass("reorder-globals-always",
"sorts globals by access frequency (even if there are few)",
createReorderGlobalsAlwaysPass);
Expand Down
1 change: 1 addition & 0 deletions src/passes/passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ Pass* createRemoveRelaxedSIMDPass();
Pass* createRemoveExportsPass();
Pass* createRemoveImportsPass();
Pass* createRemoveMemoryInitPass();
Pass* createRemoveStartPass();
Pass* createRemoveUnusedBrsPass();
Pass* createRemoveUnusedModuleElementsPass();
Pass* createRemoveUnusedNonFunctionModuleElementsPass();
Expand Down
15 changes: 12 additions & 3 deletions src/tools/execution-results.h
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,10 @@ struct LoggingExternalInterface : public ShellExternalInterface {
// Check for errors here, duplicating tableLoad(), because that will
// trap, and we just want to throw an exception (the same as JS
// would).
if (!exportedTable) {
//
// Note that we trap if we are in the start, as exports do not exist
// yet.
if (!exportedTable || instance->inStart) {
throwJSException();
}
auto index = arguments[0].getUnsigned();
Expand All @@ -213,7 +216,7 @@ struct LoggingExternalInterface : public ShellExternalInterface {
}
return table->get(index);
} else if (import->base == "table-set") {
if (!exportedTable) {
if (!exportedTable || instance->inStart) {
throwJSException();
}
auto index = arguments[0].getUnsigned();
Expand Down Expand Up @@ -289,6 +292,12 @@ struct LoggingExternalInterface : public ShellExternalInterface {
}

Literals callExportAsJS(Index index) {
if (instance->inStart) {
// No exports are even available yet. JS throws on trying to call an
// undefined (which is what we get when we read from the un-populated list
// of exports).
throwJSException();
}
if (index >= wasm.exports.size()) {
// No export.
throwJSException();
Expand Down Expand Up @@ -494,7 +503,7 @@ struct ExecutionResults {
// change whether a host limit is reached.
ignore = true;
} catch (const WasmException&) {
std::cout << "[exception thrown: start]\n";
std::cout << "[exception thrown: failed to instantiate module]\n";
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/tools/fuzzing.h
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,9 @@ class TranslateToFuzzReader {
// Checks if a function is a callRef* import (call-ref or call-ref-catch).
bool isCallRefImport(Name func);

// Pick a start function.
Name pickStart();

// statistical distributions

// 0 to the limit, logarithmic scale
Expand Down
44 changes: 36 additions & 8 deletions src/tools/fuzzing/fuzzing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

#include "tools/fuzzing.h"
#include "ir/eh-utils.h"
#include "ir/gc-type-utils.h"
#include "ir/glbs.h"
#include "ir/iteration.h"
Expand Down Expand Up @@ -1671,6 +1672,30 @@ void TranslateToFuzzReader::processFunctions() {
}
}

// Decide what to do with the start function. Most of the time we remove it,
// as that is the least risky for fuzzing (any trap in the start will make
// the entire module not execute), but other cases are important too.
//
// When preserving imports and exports, however, we always keep the start
// method, as it may be important to keep the contract between the wasm and
// the outside (even in that mode, though we have a chance to mutate and
// empty out or replace the current start, though it declines with the amount
// of mutation, so the user can control it).
if (!preserveImportsAndExports) {
switch (upTo(10)) {
case 0:
// Do not modify the start, potentially leaving the existing one.
break;
case 1:
// Pick a new start.
wasm.start = pickStart();
break;
default:
// Remove it.
wasm.start = Name();
}
}

// At the very end, add hang limit checks (so no modding can override them).
if (fuzzParams->HANG_LIMIT > 0) {
for (auto& func : wasm.functions) {
Expand Down Expand Up @@ -2394,14 +2419,6 @@ void TranslateToFuzzReader::modifyInitialFunctions() {
func->body = make(func->getResults());
}
}

// Remove a start function - the fuzzing harness expects code to run only
// 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::mutateJSBoundary() {
Expand Down Expand Up @@ -6752,4 +6769,15 @@ bool TranslateToFuzzReader::isCallRefImport(Name target) {
func->base.startsWith("call-ref");
}

Name TranslateToFuzzReader::pickStart() {
// Any none-none function is an option.
std::vector<Name> options;
for (auto& func : wasm.functions) {
if (func->getParams() == Type::none && func->getResults() == Type::none) {
options.push_back(func->name);
}
}
return options.empty() ? Name() : pick(options);
}

} // namespace wasm
Loading
Loading