Skip to content

Commit 7f62a42

Browse files
authored
Fuzzer: Add call-ref, call-ref-catch imports (WebAssembly#7137)
Similar to call-export*, these imports call a wasm function from outside the module. The difference is that we send a function reference for them to call (rather than an export index). This gives more coverage, first by sending a ref from wasm to JS, and also since we will now try to call anything that is sent. Exports, in comparison, are filtered by the fuzzer to things that JS can handle, so this may lead to more traps, but maybe also some new situations. This also leads to adding more logic to execution-results.h to model JS trapping properly. fuzz_shell.js is refactored to allow sharing code between call-export* and call-ref*.
1 parent 729ea41 commit 7f62a42

File tree

9 files changed

+493
-176
lines changed

9 files changed

+493
-176
lines changed

scripts/fuzz_shell.js

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,49 @@ function callFunc(func) {
160160
return func.apply(null, args);
161161
}
162162

163+
// Calls a given function in a try-catch, swallowing JS exceptions, and return 1
164+
// if we did in fact swallow an exception. Wasm traps are not swallowed (see
165+
// details below).
166+
function tryCall(func) {
167+
try {
168+
func();
169+
return 0;
170+
} catch (e) {
171+
// We only want to catch exceptions, not wasm traps: traps should still
172+
// halt execution. Handling this requires different code in wasm2js, so
173+
// check for that first (wasm2js does not define RuntimeError, so use
174+
// that for the check - when wasm2js is run, we override the entire
175+
// WebAssembly object with a polyfill, so we know exactly what it
176+
// contains).
177+
var wasm2js = !WebAssembly.RuntimeError;
178+
if (!wasm2js) {
179+
// When running native wasm, we can detect wasm traps.
180+
if (e instanceof WebAssembly.RuntimeError) {
181+
throw e;
182+
}
183+
}
184+
var text = e + '';
185+
// We must not swallow host limitations here: a host limitation is a
186+
// problem that means we must not compare the outcome here to any other
187+
// VM.
188+
var hostIssues = ['requested new array is too large',
189+
'out of memory',
190+
'Maximum call stack size exceeded'];
191+
if (wasm2js) {
192+
// When wasm2js does trap, it just throws an "abort" error.
193+
hostIssues.push('abort');
194+
}
195+
for (var hostIssue of hostIssues) {
196+
if (text.includes(hostIssue)) {
197+
throw e;
198+
}
199+
}
200+
// Otherwise, this is a normal exception we want to catch (a wasm
201+
// exception, or a conversion error on the wasm/JS boundary, etc.).
202+
return 1;
203+
}
204+
}
205+
163206
// Table get/set operations need a BigInt if the table has 64-bit indexes. This
164207
// adds a proper cast as needed.
165208
function toAddressType(table, index) {
@@ -204,43 +247,15 @@ var imports = {
204247
callFunc(exportList[index].value);
205248
},
206249
'call-export-catch': (index) => {
207-
try {
208-
callFunc(exportList[index].value);
209-
return 0;
210-
} catch (e) {
211-
// We only want to catch exceptions, not wasm traps: traps should still
212-
// halt execution. Handling this requires different code in wasm2js, so
213-
// check for that first (wasm2js does not define RuntimeError, so use
214-
// that for the check - when wasm2js is run, we override the entire
215-
// WebAssembly object with a polyfill, so we know exactly what it
216-
// contains).
217-
var wasm2js = !WebAssembly.RuntimeError;
218-
if (!wasm2js) {
219-
// When running native wasm, we can detect wasm traps.
220-
if (e instanceof WebAssembly.RuntimeError) {
221-
throw e;
222-
}
223-
}
224-
var text = e + '';
225-
// We must not swallow host limitations here: a host limitation is a
226-
// problem that means we must not compare the outcome here to any other
227-
// VM.
228-
var hostIssues = ['requested new array is too large',
229-
'out of memory',
230-
'Maximum call stack size exceeded'];
231-
if (wasm2js) {
232-
// When wasm2js does trap, it just throws an "abort" error.
233-
hostIssues.push('abort');
234-
}
235-
for (var hostIssue of hostIssues) {
236-
if (text.includes(hostIssue)) {
237-
throw e;
238-
}
239-
}
240-
// Otherwise, this is a normal exception we want to catch (a wasm
241-
// exception, or a conversion error on the wasm/JS boundary, etc.).
242-
return 1;
243-
}
250+
return tryCall(() => callFunc(exportList[index].value));
251+
},
252+
253+
// Funcref operations.
254+
'call-ref': (ref) => {
255+
callFunc(ref);
256+
},
257+
'call-ref-catch': (ref) => {
258+
return tryCall(() => callFunc(ref));
244259
},
245260
},
246261
// Emscripten support.

src/tools/execution-results.h

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,25 @@ struct LoggingExternalInterface : public ShellExternalInterface {
105105
tableStore(exportedTable, index, arguments[1]);
106106
return {};
107107
} else if (import->base == "call-export") {
108-
callExport(arguments[0].geti32());
108+
callExportAsJS(arguments[0].geti32());
109109
// Return nothing. If we wanted to return a value we'd need to have
110110
// multiple such functions, one for each signature.
111111
return {};
112112
} else if (import->base == "call-export-catch") {
113113
try {
114-
callExport(arguments[0].geti32());
114+
callExportAsJS(arguments[0].geti32());
115+
return {Literal(int32_t(0))};
116+
} catch (const WasmException& e) {
117+
return {Literal(int32_t(1))};
118+
}
119+
} else if (import->base == "call-ref") {
120+
callRefAsJS(arguments[0]);
121+
// Return nothing. If we wanted to return a value we'd need to have
122+
// multiple such functions, one for each signature.
123+
return {};
124+
} else if (import->base == "call-ref-catch") {
125+
try {
126+
callRefAsJS(arguments[0]);
115127
return {Literal(int32_t(0))};
116128
} catch (const WasmException& e) {
117129
return {Literal(int32_t(1))};
@@ -145,7 +157,7 @@ struct LoggingExternalInterface : public ShellExternalInterface {
145157
throwException(WasmException{Literal(payload)});
146158
}
147159

148-
Literals callExport(Index index) {
160+
Literals callExportAsJS(Index index) {
149161
if (index >= wasm.exports.size()) {
150162
// No export.
151163
throwEmptyException();
@@ -155,20 +167,47 @@ struct LoggingExternalInterface : public ShellExternalInterface {
155167
// No callable export.
156168
throwEmptyException();
157169
}
158-
auto* func = wasm.getFunction(exp->value);
170+
return callFunctionAsJS(exp->value);
171+
}
172+
173+
Literals callRefAsJS(Literal ref) {
174+
if (!ref.isFunction()) {
175+
// Not a callable ref.
176+
throwEmptyException();
177+
}
178+
return callFunctionAsJS(ref.getFunc());
179+
}
159180

160-
// TODO JS traps on some types on the boundary, which we should behave the
161-
// same on. For now, this is not needed because the fuzzer will prune all
162-
// non-JS-compatible exports anyhow.
181+
// Call a function in a "JS-ey" manner, adding arguments as needed, and
182+
// throwing if necessary, the same way JS does.
183+
Literals callFunctionAsJS(Name name) {
184+
auto* func = wasm.getFunction(name);
163185

164-
// Send default values as arguments, or trap if we need anything else.
186+
// Send default values as arguments, or error if we need anything else.
165187
Literals arguments;
166188
for (const auto& param : func->getParams()) {
189+
// An i64 param can work from JS, but fuzz_shell provides 0, which errors
190+
// on attempts to convert it to BigInt. v128 cannot work at all.
191+
if (param == Type::i64 || param == Type::v128) {
192+
throwEmptyException();
193+
}
167194
if (!param.isDefaultable()) {
168195
throwEmptyException();
169196
}
170197
arguments.push_back(Literal::makeZero(param));
171198
}
199+
200+
// Error on illegal results. Note that this happens, as per JS semantics,
201+
// *before* the call.
202+
for (const auto& result : func->getResults()) {
203+
// An i64 result is fine: a BigInt will be provided. But v128 still
204+
// errors.
205+
if (result == Type::v128) {
206+
throwEmptyException();
207+
}
208+
}
209+
210+
// Call the function.
172211
return instance->callFunction(func->name, arguments);
173212
}
174213

src/tools/fuzzing.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ class TranslateToFuzzReader {
115115
Name tableSetImportName;
116116
Name callExportImportName;
117117
Name callExportCatchImportName;
118+
Name callRefImportName;
119+
Name callRefCatchImportName;
118120

119121
std::unordered_map<Type, std::vector<Name>> globalsByType;
120122
std::unordered_map<Type, std::vector<Name>> mutableGlobalsByType;
@@ -244,7 +246,9 @@ class TranslateToFuzzReader {
244246
Expression* makeImportThrowing(Type type);
245247
Expression* makeImportTableGet();
246248
Expression* makeImportTableSet(Type type);
247-
Expression* makeImportCallExport(Type type);
249+
// Call either an export or a ref. We do this from a single function to better
250+
// control the frequency of each.
251+
Expression* makeImportCallCode(Type type);
248252
Expression* makeMemoryHashLogging();
249253

250254
// Function creation

src/tools/fuzzing/fuzzing.cpp

Lines changed: 91 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -771,22 +771,33 @@ void TranslateToFuzzReader::addImportLoggingSupport() {
771771
}
772772

773773
void TranslateToFuzzReader::addImportCallingSupport() {
774+
if (wasm.features.hasReferenceTypes() && closedWorld) {
775+
// In closed world mode we must *remove* the call-ref* imports, if they
776+
// exist in the initial content. These are not valid to call in closed-world
777+
// mode as they call function references. (Another solution here would be to
778+
// make closed-world issue validation errors on these imports, but that
779+
// would require changes to the general-purpose validator.)
780+
for (auto& func : wasm.functions) {
781+
if (func->imported() && func->module == "fuzzing-support" &&
782+
func->base.startsWith("call-ref")) {
783+
// Make it non-imported, and with a simple body.
784+
func->module = func->base = Name();
785+
auto results = func->getResults();
786+
func->body =
787+
results.isConcrete() ? makeConst(results) : makeNop(Type::none);
788+
}
789+
}
790+
}
791+
774792
// Only add these some of the time, as they inhibit some fuzzing (things like
775793
// wasm-ctor-eval and wasm-merge are sensitive to the wasm being able to call
776-
// its own exports, and to care about the indexes of the exports):
777-
//
778-
// 0 - none
779-
// 1 - call-export
780-
// 2 - call-export-catch
781-
// 3 - call-export & call-export-catch
782-
// 4 - none
783-
// 5 - none
784-
//
785-
auto choice = upTo(6);
786-
if (choice >= 4) {
794+
// its own exports, and to care about the indexes of the exports).
795+
if (oneIn(2)) {
787796
return;
788797
}
789798

799+
auto choice = upTo(16);
800+
790801
if (choice & 1) {
791802
// Given an export index, call it from JS.
792803
callExportImportName = Names::getValidFunctionName(wasm, "call-export");
@@ -811,6 +822,34 @@ void TranslateToFuzzReader::addImportCallingSupport() {
811822
func->type = Signature(Type::i32, Type::i32);
812823
wasm.addFunction(std::move(func));
813824
}
825+
826+
// If the wasm will be used for closed-world testing, we cannot use the
827+
// call-ref variants, as mentioned before.
828+
if (wasm.features.hasReferenceTypes() && !closedWorld) {
829+
if (choice & 4) {
830+
// Given an funcref, call it from JS.
831+
callRefImportName = Names::getValidFunctionName(wasm, "call-ref");
832+
auto func = std::make_unique<Function>();
833+
func->name = callRefImportName;
834+
func->module = "fuzzing-support";
835+
func->base = "call-ref";
836+
func->type = Signature({Type(HeapType::func, Nullable)}, Type::none);
837+
wasm.addFunction(std::move(func));
838+
}
839+
840+
if (choice & 8) {
841+
// Given an funcref, call it from JS and catch all exceptions (similar
842+
// to callExportCatch), return 1 if we caught).
843+
callRefCatchImportName =
844+
Names::getValidFunctionName(wasm, "call-ref-catch");
845+
auto func = std::make_unique<Function>();
846+
func->name = callRefCatchImportName;
847+
func->module = "fuzzing-support";
848+
func->base = "call-ref-catch";
849+
func->type = Signature(Type(HeapType::func, Nullable), Type::i32);
850+
wasm.addFunction(std::move(func));
851+
}
852+
}
814853
}
815854

816855
void TranslateToFuzzReader::addImportThrowingSupport() {
@@ -998,27 +1037,48 @@ Expression* TranslateToFuzzReader::makeImportTableSet(Type type) {
9981037
Type::none);
9991038
}
10001039

1001-
Expression* TranslateToFuzzReader::makeImportCallExport(Type type) {
1002-
// The none-returning variant just does the call. The i32-returning one
1003-
// catches any errors and returns 1 when it saw an error. Based on the
1004-
// variant, pick which to call, and the maximum index to call.
1005-
Name target;
1040+
Expression* TranslateToFuzzReader::makeImportCallCode(Type type) {
1041+
// Call code: either an export or a ref. Each has a catching and non-catching
1042+
// variant. The catching variants return i32, the others none.
1043+
assert(type == Type::none || type == Type::i32);
1044+
auto catching = type == Type::i32;
1045+
auto exportTarget =
1046+
catching ? callExportCatchImportName : callExportImportName;
1047+
auto refTarget = catching ? callRefCatchImportName : callRefImportName;
1048+
1049+
// We want to call a ref less often, as refs are more likely to error (a
1050+
// function reference can have arbitrary params and results, including things
1051+
// that error on the JS boundary; an export is already filtered for such
1052+
// things in some cases - when we legalize the boundary - and even if not, we
1053+
// emit lots of void(void) functions - all the invoke_foo functions - that are
1054+
// safe to call).
1055+
if (refTarget) {
1056+
// This matters a lot more in the variants that do *not* catch (in the
1057+
// catching ones, we just get a result of 1, but when not caught it halts
1058+
// execution).
1059+
if ((catching && (!exportTarget || oneIn(2))) || (!catching && oneIn(4))) {
1060+
// Most of the time make a non-nullable funcref, to avoid errors.
1061+
auto refType = Type(HeapType::func, oneIn(10) ? Nullable : NonNullable);
1062+
return builder.makeCall(refTarget, {make(refType)}, type);
1063+
}
1064+
}
1065+
1066+
if (!exportTarget) {
1067+
// We decided not to emit a call-ref here, due to fear of erroring, and
1068+
// there is no call-export, so just emit something trivial.
1069+
return makeTrivial(type);
1070+
}
1071+
1072+
// Pick the maximum export index to call.
10061073
Index maxIndex = wasm.exports.size();
1007-
if (type == Type::none) {
1008-
target = callExportImportName;
1009-
} else if (type == Type::i32) {
1010-
target = callExportCatchImportName;
1011-
// This never traps, so we can be less careful, but we do still want to
1012-
// avoid trapping a lot as executing code is more interesting. (Note that
1074+
if (type == Type::i32) {
1075+
// This swallows errors, so we can be less careful, but we do still want to
1076+
// avoid swallowing a lot as executing code is more interesting. (Note that
10131077
// even though we double here, the risk is not that great: we are still
10141078
// adding functions as we go, so the first half of functions/exports can
10151079
// double here and still end up in bounds by the time we've added them all.)
10161080
maxIndex = (maxIndex + 1) * 2;
1017-
} else {
1018-
WASM_UNREACHABLE("bad import.call");
10191081
}
1020-
// We must have set up the target function.
1021-
assert(target);
10221082

10231083
// Most of the time, call a valid export index in the range we picked, but
10241084
// sometimes allow anything at all.
@@ -1027,7 +1087,7 @@ Expression* TranslateToFuzzReader::makeImportCallExport(Type type) {
10271087
index = builder.makeBinary(
10281088
RemUInt32, index, builder.makeConst(int32_t(maxIndex)));
10291089
}
1030-
return builder.makeCall(target, {index}, type);
1090+
return builder.makeCall(exportTarget, {index}, type);
10311091
}
10321092

10331093
Expression* TranslateToFuzzReader::makeMemoryHashLogging() {
@@ -1705,8 +1765,8 @@ Expression* TranslateToFuzzReader::_makeConcrete(Type type) {
17051765
options.add(FeatureSet::Atomics, &Self::makeAtomic);
17061766
}
17071767
if (type == Type::i32) {
1708-
if (callExportCatchImportName) {
1709-
options.add(FeatureSet::MVP, &Self::makeImportCallExport);
1768+
if (callExportCatchImportName || callRefCatchImportName) {
1769+
options.add(FeatureSet::MVP, &Self::makeImportCallCode);
17101770
}
17111771
options.add(FeatureSet::ReferenceTypes, &Self::makeRefIsNull);
17121772
options.add(FeatureSet::ReferenceTypes | FeatureSet::GC,
@@ -1787,8 +1847,8 @@ Expression* TranslateToFuzzReader::_makenone() {
17871847
if (tableSetImportName) {
17881848
options.add(FeatureSet::ReferenceTypes, &Self::makeImportTableSet);
17891849
}
1790-
if (callExportImportName) {
1791-
options.add(FeatureSet::MVP, &Self::makeImportCallExport);
1850+
if (callExportImportName || callRefImportName) {
1851+
options.add(FeatureSet::MVP, &Self::makeImportCallCode);
17921852
}
17931853
return (this->*pick(options))(Type::none);
17941854
}

0 commit comments

Comments
 (0)