Skip to content

Commit 379e5ec

Browse files
authored
Fuzzer: Add an option to preserve imports and exports (#7300)
Normally the fuzzer will add new imports (for the JS stuff we can call), and new exports as it adds functions. This adds an option to avoid that, and instead keep the imports and exports fixed. This is useful when we are used to modify an existing fuzzer's testcase, as its connection to the JS side should be considered fixed (and it will run in that fuzzer's JS, not ours). This is added as --fuzz-preserve-imports-exports for wasm-opt.
1 parent 26c5502 commit 379e5ec

File tree

8 files changed

+190
-32
lines changed

8 files changed

+190
-32
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ full changeset diff at the end of each section.
1515
Current Trunk
1616
-------------
1717

18+
- Add an option to preserve imports and exports in the fuzzer (for fuzzer
19+
harnesses where they only want Binaryen to modify their given testcases, not
20+
generate new things in them).
21+
1822
v122
1923
----
2024

scripts/fuzz_opt.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1649,7 +1649,6 @@ def handle(self, wasm):
16491649
# run, or if the wasm errored during instantiation, which can happen due
16501650
# to a testcase with a segment out of bounds, say).
16511651
if output != IGNORE and not output.startswith(INSTANTIATE_ERROR):
1652-
16531652
assert FUZZ_EXEC_CALL_PREFIX in output
16541653

16551654
def ensure(self):
@@ -1756,6 +1755,52 @@ def can_run_on_wasm(self, wasm):
17561755
return not CLOSED_WORLD and all_disallowed(['shared-everything']) and not NANS
17571756

17581757

1758+
# Test --fuzz-preserve-imports-exports, which never modifies imports or exports.
1759+
class PreserveImportsExports(TestCaseHandler):
1760+
frequency = 0.1
1761+
1762+
def handle(self, wasm):
1763+
# We will later verify that no imports or exports changed, by comparing
1764+
# to the unprocessed original text.
1765+
original = run([in_bin('wasm-opt'), wasm] + FEATURE_OPTS + ['--print'])
1766+
1767+
# We leave if the module has (ref exn) in struct fields (because we have
1768+
# no way to generate an exn in a non-function context, and if we picked
1769+
# that struct for a global, we'd end up needing a (ref exn) in the
1770+
# global scope, which is impossible). The fuzzer is designed to be
1771+
# careful not to emit that in testcases, but after the optimizer runs,
1772+
# we may end up with struct fields getting refined to that, so we need
1773+
# this extra check (which should be hit very rarely).
1774+
structs = [line for line in original.split('\n') if '(struct ' in line]
1775+
if '(ref exn)' in '\n'.join(structs):
1776+
note_ignored_vm_run('has non-nullable exn in struct')
1777+
return
1778+
1779+
# Generate some random input data.
1780+
data = abspath('preserve_input.dat')
1781+
make_random_input(random_size(), data)
1782+
1783+
# Process the existing wasm file.
1784+
processed = run([in_bin('wasm-opt'), data] + FEATURE_OPTS + [
1785+
'-ttf',
1786+
'--fuzz-preserve-imports-exports',
1787+
'--initial-fuzz=' + wasm,
1788+
'--print',
1789+
])
1790+
1791+
def get_relevant_lines(wat):
1792+
# Imports and exports are relevant.
1793+
lines = [line for line in wat.splitlines() if '(export ' in line or '(import ' in line]
1794+
1795+
# Ignore type names, which may vary (e.g. one file may have $5 and
1796+
# another may call the same type $17).
1797+
lines = [re.sub(r'[(]type [$][0-9a-zA-Z_$]+[)]', '', line) for line in lines]
1798+
1799+
return '\n'.join(lines)
1800+
1801+
compare(get_relevant_lines(original), get_relevant_lines(processed), 'Preserve')
1802+
1803+
17591804
# The global list of all test case handlers
17601805
testcase_handlers = [
17611806
FuzzExec(),
@@ -1770,6 +1815,7 @@ def can_run_on_wasm(self, wasm):
17701815
RoundtripText(),
17711816
ClusterFuzz(),
17721817
Two(),
1818+
PreserveImportsExports(),
17731819
]
17741820

17751821

src/tools/fuzzing.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ class TranslateToFuzzReader {
128128
void pickPasses(OptimizationOptions& options);
129129
void setAllowMemory(bool allowMemory_) { allowMemory = allowMemory_; }
130130
void setAllowOOB(bool allowOOB_) { allowOOB = allowOOB_; }
131+
void setPreserveImportsAndExports(bool preserveImportsAndExports_) {
132+
preserveImportsAndExports = preserveImportsAndExports_;
133+
}
131134

132135
void build();
133136

@@ -146,6 +149,13 @@ class TranslateToFuzzReader {
146149
// of bounds (which traps in wasm, and is undefined behavior in C).
147150
bool allowOOB = true;
148151

152+
// Whether we preserve imports and exports. Normally we add imports (for
153+
// logging and other useful functionality for testing), and add exports of
154+
// functions as we create them. With this set, we add neither imports nor
155+
// exports, which is useful if the tool using us only wants us to mutate an
156+
// existing testcase (using initial-content).
157+
bool preserveImportsAndExports = false;
158+
149159
// Whether we allow the fuzzer to add unreachable code when generating changes
150160
// to existing code. This is randomized during startup, but could be an option
151161
// like the above options eventually if we find that useful.

src/tools/fuzzing/fuzzing.cpp

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -614,10 +614,12 @@ void TranslateToFuzzReader::setupGlobals() {
614614
// run the wasm.
615615
for (auto& global : wasm.globals) {
616616
if (global->imported()) {
617-
// Remove import info from imported globals, and give them a simple
618-
// initializer.
619-
global->module = global->base = Name();
620-
global->init = makeConst(global->type);
617+
if (!preserveImportsAndExports) {
618+
// Remove import info from imported globals, and give them a simple
619+
// initializer.
620+
global->module = global->base = Name();
621+
global->init = makeConst(global->type);
622+
}
621623
} else {
622624
// If the initialization referred to an imported global, it no longer can
623625
// point to the same global after we make it a non-imported global unless
@@ -695,7 +697,7 @@ void TranslateToFuzzReader::setupTags() {
695697
// As in modifyInitialFunctions(), we can't allow arbitrary tag imports, which
696698
// would trap when the fuzzing infrastructure doesn't know what to provide.
697699
for (auto& tag : wasm.tags) {
698-
if (tag->imported()) {
700+
if (tag->imported() && !preserveImportsAndExports) {
699701
tag->module = tag->base = Name();
700702
}
701703
}
@@ -707,7 +709,7 @@ void TranslateToFuzzReader::setupTags() {
707709
}
708710

709711
// Add the fuzzing support tags manually sometimes.
710-
if (oneIn(2)) {
712+
if (!preserveImportsAndExports && oneIn(2)) {
711713
auto wasmTag = builder.makeTag(Names::getValidTagName(wasm, "wasmtag"),
712714
Signature(Type::i32, Type::none));
713715
wasmTag->module = "fuzzing-support";
@@ -779,9 +781,12 @@ void TranslateToFuzzReader::finalizeMemory() {
779781
memory->max =
780782
std::min(Address(memory->initial + 1), Address(Memory::kMaxSize32));
781783
}
782-
// Avoid an imported memory (which the fuzz harness would need to handle).
783-
for (auto& memory : wasm.memories) {
784-
memory->module = memory->base = Name();
784+
785+
if (!preserveImportsAndExports) {
786+
// Avoid an imported memory (which the fuzz harness would need to handle).
787+
for (auto& memory : wasm.memories) {
788+
memory->module = memory->base = Name();
789+
}
785790
}
786791
}
787792

@@ -826,8 +831,11 @@ void TranslateToFuzzReader::finalizeTable() {
826831
assert(ReasonableMaxTableSize <= Table::kMaxSize);
827832

828833
table->max = oneIn(2) ? Address(Table::kUnlimitedSize) : table->initial;
829-
// Avoid an imported table (which the fuzz harness would need to handle).
830-
table->module = table->base = Name();
834+
835+
if (!preserveImportsAndExports) {
836+
// Avoid an imported table (which the fuzz harness would need to handle).
837+
table->module = table->base = Name();
838+
}
831839
}
832840
}
833841

@@ -841,8 +849,9 @@ void TranslateToFuzzReader::shuffleExports() {
841849
// we emit invokes for a function right after it (so we end up calling the
842850
// same code several times in succession, but interleaving it with others may
843851
// find more things). But we also keep a good chance for the natural order
844-
// here, as it may help some initial content.
845-
if (wasm.exports.empty() || oneIn(2)) {
852+
// here, as it may help some initial content. Note we cannot do this if we are
853+
// preserving the exports, as their order is something we must maintain.
854+
if (wasm.exports.empty() || preserveImportsAndExports || oneIn(2)) {
846855
return;
847856
}
848857

@@ -881,14 +890,24 @@ void TranslateToFuzzReader::addImportLoggingSupport() {
881890
Name baseName = std::string("log-") + type.toString();
882891
func->name = Names::getValidFunctionName(wasm, baseName);
883892
logImportNames[type] = func->name;
884-
func->module = "fuzzing-support";
885-
func->base = baseName;
893+
if (!preserveImportsAndExports) {
894+
func->module = "fuzzing-support";
895+
func->base = baseName;
896+
} else {
897+
// We cannot add an import, so just make it a trivial function (this is
898+
// simpler than avoiding calls to logging in all the rest of the logic).
899+
func->body = builder.makeNop();
900+
}
886901
func->type = Signature(type, Type::none);
887902
wasm.addFunction(std::move(func));
888903
}
889904
}
890905

891906
void TranslateToFuzzReader::addImportCallingSupport() {
907+
if (preserveImportsAndExports) {
908+
return;
909+
}
910+
892911
if (wasm.features.hasReferenceTypes() && closedWorld) {
893912
// In closed world mode we must *remove* the call-ref* imports, if they
894913
// exist in the initial content. These are not valid to call in closed-world
@@ -983,8 +1002,13 @@ void TranslateToFuzzReader::addImportThrowingSupport() {
9831002
throwImportName = Names::getValidFunctionName(wasm, "throw");
9841003
auto func = std::make_unique<Function>();
9851004
func->name = throwImportName;
986-
func->module = "fuzzing-support";
987-
func->base = "throw";
1005+
if (!preserveImportsAndExports) {
1006+
func->module = "fuzzing-support";
1007+
func->base = "throw";
1008+
} else {
1009+
// As with logging, implement in a trivial way when we cannot add imports.
1010+
func->body = builder.makeNop();
1011+
}
9881012
func->type = Signature(Type::i32, Type::none);
9891013
wasm.addFunction(std::move(func));
9901014
}
@@ -999,8 +1023,9 @@ void TranslateToFuzzReader::addImportTableSupport() {
9991023
}
10001024

10011025
// If a "table" export already exists, skip fuzzing these imports, as the
1002-
// current export may not contain a valid table for it.
1003-
if (wasm.getExportOrNull("table")) {
1026+
// current export may not contain a valid table for it. We also skip if we are
1027+
// not adding imports or exports.
1028+
if (wasm.getExportOrNull("table") || preserveImportsAndExports) {
10041029
return;
10051030
}
10061031

@@ -1033,8 +1058,9 @@ void TranslateToFuzzReader::addImportTableSupport() {
10331058
}
10341059

10351060
void TranslateToFuzzReader::addImportSleepSupport() {
1036-
if (!oneIn(4)) {
1037-
// Fuzz this somewhat rarely, as it may be slow.
1061+
// Fuzz this somewhat rarely, as it may be slow, and only when we can add
1062+
// imports.
1063+
if (preserveImportsAndExports || !oneIn(4)) {
10381064
return;
10391065
}
10401066

@@ -1087,12 +1113,15 @@ void TranslateToFuzzReader::addHashMemorySupport() {
10871113
auto* body = builder.makeBlock(contents);
10881114
auto* hasher = wasm.addFunction(builder.makeFunction(
10891115
"hashMemory", Signature(Type::none, Type::i32), {Type::i32}, body));
1090-
wasm.addExport(
1091-
builder.makeExport(hasher->name, hasher->name, ExternalKind::Function));
1092-
// Export memory so JS fuzzing can use it
1093-
if (!wasm.getExportOrNull("memory")) {
1094-
wasm.addExport(builder.makeExport(
1095-
"memory", wasm.memories[0]->name, ExternalKind::Memory));
1116+
1117+
if (!preserveImportsAndExports) {
1118+
wasm.addExport(
1119+
builder.makeExport(hasher->name, hasher->name, ExternalKind::Function));
1120+
// Export memory so JS fuzzing can use it
1121+
if (!wasm.getExportOrNull("memory")) {
1122+
wasm.addExport(builder.makeExport(
1123+
"memory", wasm.memories[0]->name, ExternalKind::Memory));
1124+
}
10961125
}
10971126
}
10981127

@@ -1340,7 +1369,7 @@ Function* TranslateToFuzzReader::addFunction() {
13401369
return t.isDefaultable();
13411370
});
13421371
if (validExportParams && (numAddedFunctions == 0 || oneIn(2)) &&
1343-
!wasm.getExportOrNull(func->name)) {
1372+
!wasm.getExportOrNull(func->name) && !preserveImportsAndExports) {
13441373
auto* export_ = new Export;
13451374
export_->name = func->name;
13461375
export_->value = func->name;
@@ -1805,8 +1834,10 @@ void TranslateToFuzzReader::modifyInitialFunctions() {
18051834
for (Index i = 0; i < wasm.functions.size(); i++) {
18061835
auto* func = wasm.functions[i].get();
18071836
// We can't allow extra imports, as the fuzzing infrastructure wouldn't
1808-
// know what to provide. Keep only our own fuzzer imports.
1809-
if (func->imported() && func->module == "fuzzing-support") {
1837+
// know what to provide. Keep only our own fuzzer imports (or, if we are
1838+
// preserving imports, keep them all).
1839+
if (func->imported() &&
1840+
(func->module == "fuzzing-support" || preserveImportsAndExports)) {
18101841
continue;
18111842
}
18121843
FunctionCreationContext context(*this, func);
@@ -1907,7 +1938,12 @@ void TranslateToFuzzReader::addInvocations(Function* func) {
19071938
}
19081939
body->list.set(invocations);
19091940
wasm.addFunction(std::move(invoker));
1910-
wasm.addExport(builder.makeExport(name, name, ExternalKind::Function));
1941+
1942+
// Most of the benefit of invocations is lost when we do not add exports for
1943+
// them, but still, they might be called by existing functions.
1944+
if (!preserveImportsAndExports) {
1945+
wasm.addExport(builder.makeExport(name, name, ExternalKind::Function));
1946+
}
19111947
}
19121948

19131949
Expression* TranslateToFuzzReader::make(Type type) {

src/tools/wasm-opt.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ int main(int argc, const char* argv[]) {
8585
bool fuzzPasses = false;
8686
bool fuzzMemory = true;
8787
bool fuzzOOB = true;
88+
bool fuzzPreserveImportsAndExports = false;
8889
std::string emitSpecWrapper;
8990
std::string emitWasm2CWrapper;
9091
std::string inputSourceMapFilename;
@@ -178,6 +179,14 @@ int main(int argc, const char* argv[]) {
178179
WasmOptOption,
179180
Options::Arguments::Zero,
180181
[&](Options* o, const std::string& arguments) { fuzzOOB = false; })
182+
.add("--fuzz-preserve-imports-exports",
183+
"",
184+
"don't add imports and exports in -ttf mode",
185+
WasmOptOption,
186+
Options::Arguments::Zero,
187+
[&](Options* o, const std::string& arguments) {
188+
fuzzPreserveImportsAndExports = true;
189+
})
181190
.add("--emit-spec-wrapper",
182191
"-esw",
183192
"Emit a wasm spec interpreter wrapper file that can run the wasm with "
@@ -310,6 +319,7 @@ int main(int argc, const char* argv[]) {
310319
}
311320
reader.setAllowMemory(fuzzMemory);
312321
reader.setAllowOOB(fuzzOOB);
322+
reader.setPreserveImportsAndExports(fuzzPreserveImportsAndExports);
313323
reader.build();
314324
if (options.passOptions.validate) {
315325
if (!WasmValidator().validate(wasm, options.passOptions)) {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
;; Test the flag to preserve imports and exports in fuzzer generation.
2+
3+
;; Generate fuzz output using this wat as initial contents, and with the flag to
4+
;; preserve imports and exports. There should be no new imports or exports, and
5+
;; old ones must stay the same.
6+
7+
;; RUN: wasm-opt %s.ttf --initial-fuzz=%s -all -ttf --fuzz-preserve-imports-exports \
8+
;; RUN: --metrics -S -o - | filecheck %s --check-prefix=PRESERVE
9+
10+
;; PRESERVE: [exports] : 1
11+
;; PRESERVE: [imports] : 5
12+
13+
;; [sic] - we do not close ("))") some imports, which have info in the wat
14+
;; which we do not care about.
15+
;; PRESERVE: (import "a" "d" (memory $imemory
16+
;; PRESERVE: (import "a" "e" (table $itable
17+
;; PRESERVE: (import "a" "b" (global $iglobal i32))
18+
;; PRESERVE: (import "a" "f" (func $ifunc
19+
;; PRESERVE: (import "a" "c" (tag $itag
20+
21+
;; PRESERVE: (export "foo" (func $foo))
22+
23+
;; And, without the flag, we do generate both imports and exports.
24+
25+
;; RUN: wasm-opt %s.ttf --initial-fuzz=%s -all -ttf \
26+
;; RUN: --metrics -S -o - | filecheck %s --check-prefix=NORMAL
27+
28+
;; Rather than hardcode the number here, find two of each.
29+
;; NORMAL: (import
30+
;; NORMAL: (import
31+
;; NORMAL: (export
32+
;; NORMAL: (export
33+
34+
(module
35+
;; Existing imports. Note that the fuzzer normally turns imported globals etc.
36+
;; into normal ones (as the fuzz harness does not know what to provide at
37+
;; compile time), so we also test that --fuzz-preserve-imports-exports leaves
38+
;; such imports alone.
39+
(import "a" "b" (global $iglobal i32))
40+
(import "a" "c" (tag $itag))
41+
(import "a" "d" (memory $imemory 10 20))
42+
(import "a" "e" (table $itable 10 20 funcref))
43+
(import "a" "f" (func $ifunc))
44+
45+
;; One existing export.
46+
(func $foo (export "foo")
47+
)
48+
)
49+
3.93 KB
Binary file not shown.

test/lit/help/wasm-opt.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
;; CHECK-NEXT: loads/stores/indirect calls when
5454
;; CHECK-NEXT: fuzzing
5555
;; CHECK-NEXT:
56+
;; CHECK-NEXT: --fuzz-preserve-imports-exports don't add imports and exports in
57+
;; CHECK-NEXT: -ttf mode
58+
;; CHECK-NEXT:
5659
;; CHECK-NEXT: --emit-spec-wrapper,-esw Emit a wasm spec interpreter
5760
;; CHECK-NEXT: wrapper file that can run the
5861
;; CHECK-NEXT: wasm with some test values,

0 commit comments

Comments
 (0)