Skip to content

Commit dcc70bb

Browse files
authored
[EH] Fuzz throws from JS (WebAssembly#7027)
We already generated (throw ..) instructions in wasm, but it makes sense to model throws from outside as well, as they cross the module boundary. This adds a new fuzzer import to the generated modules, "throw", that just does a throw from JS etc. Also be more precise about handling fuzzing-support imports in fuzz-exec: we now check that logging functions start with "log*" and error otherwise (this check is now needed given we have "throw", which is not logging). Also fix a minor issue with name conflicts for logging functions by using getValidFunctionName for them, both for logging and for throw.
1 parent 0d9b750 commit dcc70bb

12 files changed

+215
-115
lines changed

scripts/fuzz_shell.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ function logValue(x, y) {
138138
var tempRet0;
139139
var imports = {
140140
'fuzzing-support': {
141+
// Logging.
141142
'log-i32': logValue,
142143
'log-i64': logValue,
143144
'log-f32': logValue,
@@ -147,7 +148,13 @@ var imports = {
147148
// we could avoid running JS on code with SIMD in it, but it is useful to
148149
// fuzz such code as much as we can.)
149150
'log-v128': logValue,
151+
152+
// Throw an exception from JS.
153+
'throw': () => {
154+
throw 'some JS error';
155+
}
150156
},
157+
// Emscripten support.
151158
'env': {
152159
'setTempRet0': function(x) { tempRet0 = x },
153160
'getTempRet0': function() { return tempRet0 },

src/tools/execution-results.h

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ using Loggings = std::vector<Literal>;
2727

2828
// Logs every relevant import call parameter.
2929
struct LoggingExternalInterface : public ShellExternalInterface {
30+
private:
3031
Loggings& loggings;
3132

3233
struct State {
@@ -37,30 +38,40 @@ struct LoggingExternalInterface : public ShellExternalInterface {
3738
uint32_t tempRet0 = 0;
3839
} state;
3940

41+
public:
4042
LoggingExternalInterface(Loggings& loggings) : loggings(loggings) {}
4143

4244
Literals callImport(Function* import, const Literals& arguments) override {
4345
if (import->module == "fuzzing-support") {
44-
std::cout << "[LoggingExternalInterface logging";
45-
loggings.push_back(Literal()); // buffer with a None between calls
46-
for (auto argument : arguments) {
47-
if (argument.type == Type::i64) {
48-
// To avoid JS legalization changing logging results, treat a logging
49-
// of an i64 as two i32s (which is what legalization would turn us
50-
// into).
51-
auto low = Literal(int32_t(argument.getInteger()));
52-
auto high = Literal(int32_t(argument.getInteger() >> int32_t(32)));
53-
std::cout << ' ' << low;
54-
loggings.push_back(low);
55-
std::cout << ' ' << high;
56-
loggings.push_back(high);
57-
} else {
58-
std::cout << ' ' << argument;
59-
loggings.push_back(argument);
46+
if (import->base.startsWith("log")) {
47+
// This is a logging function like log-i32 or log-f64
48+
std::cout << "[LoggingExternalInterface logging";
49+
loggings.push_back(Literal()); // buffer with a None between calls
50+
for (auto argument : arguments) {
51+
if (argument.type == Type::i64) {
52+
// To avoid JS legalization changing logging results, treat a
53+
// logging of an i64 as two i32s (which is what legalization would
54+
// turn us into).
55+
auto low = Literal(int32_t(argument.getInteger()));
56+
auto high = Literal(int32_t(argument.getInteger() >> int32_t(32)));
57+
std::cout << ' ' << low;
58+
loggings.push_back(low);
59+
std::cout << ' ' << high;
60+
loggings.push_back(high);
61+
} else {
62+
std::cout << ' ' << argument;
63+
loggings.push_back(argument);
64+
}
6065
}
66+
std::cout << "]\n";
67+
return {};
68+
} else if (import->base == "throw") {
69+
// Throw something. We use a (hopefully) private name here.
70+
auto payload = std::make_shared<ExnData>("__private", Literals{});
71+
throwException(WasmException{Literal(payload)});
72+
} else {
73+
WASM_UNREACHABLE("unknown fuzzer import");
6174
}
62-
std::cout << "]\n";
63-
return {};
6475
} else if (import->module == ENV) {
6576
if (import->base == "log_execution") {
6677
std::cout << "[LoggingExternalInterface log-execution";

src/tools/fuzzing.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ class TranslateToFuzzReader {
104104

105105
Name funcrefTableName;
106106

107+
std::unordered_map<Type, Name> logImportNames;
108+
109+
Name throwImportName;
110+
107111
std::unordered_map<Type, std::vector<Name>> globalsByType;
108112
std::unordered_map<Type, std::vector<Name>> mutableGlobalsByType;
109113
std::unordered_map<Type, std::vector<Name>> immutableGlobalsByType;
@@ -220,12 +224,16 @@ class TranslateToFuzzReader {
220224
void finalizeTable();
221225
void prepareHangLimitSupport();
222226
void addHangLimitSupport();
227+
// Imports that we call to log out values.
223228
void addImportLoggingSupport();
229+
// An import that we call to throw an exception from outside.
230+
void addImportThrowingSupport();
224231
void addHashMemorySupport();
225232

226233
// Special expression makers
227234
Expression* makeHangLimitCheck();
228-
Expression* makeLogging();
235+
Expression* makeImportLogging();
236+
Expression* makeImportThrowing(Type type);
229237
Expression* makeMemoryHashLogging();
230238

231239
// Function creation

src/tools/fuzzing/fuzzing.cpp

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,10 @@ void TranslateToFuzzReader::build() {
176176
setupGlobals();
177177
if (wasm.features.hasExceptionHandling()) {
178178
setupTags();
179+
addImportThrowingSupport();
179180
}
180-
modifyInitialFunctions();
181181
addImportLoggingSupport();
182+
modifyInitialFunctions();
182183
// keep adding functions until we run out of input
183184
while (!random.finished()) {
184185
auto* func = addFunction();
@@ -583,16 +584,31 @@ void TranslateToFuzzReader::addHangLimitSupport() {
583584

584585
void TranslateToFuzzReader::addImportLoggingSupport() {
585586
for (auto type : loggableTypes) {
586-
auto* func = new Function;
587-
Name name = std::string("log-") + type.toString();
588-
func->name = name;
587+
auto func = std::make_unique<Function>();
588+
Name baseName = std::string("log-") + type.toString();
589+
func->name = Names::getValidFunctionName(wasm, baseName);
590+
logImportNames[type] = func->name;
589591
func->module = "fuzzing-support";
590-
func->base = name;
592+
func->base = baseName;
591593
func->type = Signature(type, Type::none);
592-
wasm.addFunction(func);
594+
wasm.addFunction(std::move(func));
593595
}
594596
}
595597

598+
void TranslateToFuzzReader::addImportThrowingSupport() {
599+
// Throw some kind of exception from JS.
600+
// TODO: Send an index, which is which exported wasm Tag we should throw, or
601+
// something not exported if out of bounds. First we must also export
602+
// tags sometimes.
603+
throwImportName = Names::getValidFunctionName(wasm, "throw");
604+
auto func = std::make_unique<Function>();
605+
func->name = throwImportName;
606+
func->module = "fuzzing-support";
607+
func->base = "throw";
608+
func->type = Signature(Type::none, Type::none);
609+
wasm.addFunction(std::move(func));
610+
}
611+
596612
void TranslateToFuzzReader::addHashMemorySupport() {
597613
// Add memory hasher helper (for the hash, see hash.h). The function looks
598614
// like:
@@ -692,21 +708,30 @@ Expression* TranslateToFuzzReader::makeHangLimitCheck() {
692708
builder.makeConst(int32_t(1)))));
693709
}
694710

695-
Expression* TranslateToFuzzReader::makeLogging() {
711+
Expression* TranslateToFuzzReader::makeImportLogging() {
696712
auto type = getLoggableType();
697-
return builder.makeCall(
698-
std::string("log-") + type.toString(), {make(type)}, Type::none);
713+
return builder.makeCall(logImportNames[type], {make(type)}, Type::none);
714+
}
715+
716+
Expression* TranslateToFuzzReader::makeImportThrowing(Type type) {
717+
// We throw from the import, so this call appears to be none and not
718+
// unreachable.
719+
assert(type == Type::none);
720+
721+
// TODO: This and makeThrow should probably be rare, as they halt the program.
722+
return builder.makeCall(throwImportName, {}, Type::none);
699723
}
700724

701725
Expression* TranslateToFuzzReader::makeMemoryHashLogging() {
702726
auto* hash = builder.makeCall(std::string("hashMemory"), {}, Type::i32);
703-
return builder.makeCall(std::string("log-i32"), {hash}, Type::none);
727+
return builder.makeCall(logImportNames[Type::i32], {hash}, Type::none);
704728
}
705729

706730
// TODO: return std::unique_ptr<Function>
707731
Function* TranslateToFuzzReader::addFunction() {
708732
LOGGING_PERCENT = upToSquared(100);
709-
auto* func = new Function;
733+
auto allocation = std::make_unique<Function>();
734+
auto* func = allocation.get();
710735
func->name = Names::getValidFunctionName(wasm, "func");
711736
FunctionCreationContext context(*this, func);
712737
assert(funcContext->typeLocals.empty());
@@ -765,7 +790,7 @@ Function* TranslateToFuzzReader::addFunction() {
765790
}
766791

767792
// Add hang limit checks after all other operations on the function body.
768-
wasm.addFunction(func);
793+
wasm.addFunction(std::move(allocation));
769794
// Export some functions, but not all (to allow inlining etc.). Try to export
770795
// at least one, though, to keep each testcase interesting. Only functions
771796
// with valid params and returns can be exported because the trap fuzzer
@@ -1215,10 +1240,13 @@ void TranslateToFuzzReader::modifyInitialFunctions() {
12151240
// the end (currently that is not needed atm, but it might in the future).
12161241
for (Index i = 0; i < wasm.functions.size(); i++) {
12171242
auto* func = wasm.functions[i].get();
1243+
// We can't allow extra imports, as the fuzzing infrastructure wouldn't
1244+
// know what to provide. Keep only our own fuzzer imports.
1245+
if (func->imported() && func->module == "fuzzing-support") {
1246+
continue;
1247+
}
12181248
FunctionCreationContext context(*this, func);
12191249
if (func->imported()) {
1220-
// We can't allow extra imports, as the fuzzing infrastructure wouldn't
1221-
// know what to provide.
12221250
func->module = func->base = Name();
12231251
func->body = make(func->getResults());
12241252
}
@@ -1261,10 +1289,9 @@ void TranslateToFuzzReader::dropToLog(Function* func) {
12611289

12621290
void visitDrop(Drop* curr) {
12631291
if (parent.isLoggableType(curr->value->type) && parent.oneIn(2)) {
1264-
replaceCurrent(parent.builder.makeCall(std::string("log-") +
1265-
curr->value->type.toString(),
1266-
{curr->value},
1267-
Type::none));
1292+
auto target = parent.logImportNames[curr->value->type];
1293+
replaceCurrent(
1294+
parent.builder.makeCall(target, {curr->value}, Type::none));
12681295
}
12691296
}
12701297
};
@@ -1430,7 +1457,7 @@ Expression* TranslateToFuzzReader::_makenone() {
14301457
auto choice = upTo(100);
14311458
if (choice < LOGGING_PERCENT) {
14321459
if (choice < LOGGING_PERCENT / 2) {
1433-
return makeLogging();
1460+
return makeImportLogging();
14341461
} else {
14351462
return makeMemoryHashLogging();
14361463
}
@@ -1455,6 +1482,7 @@ Expression* TranslateToFuzzReader::_makenone() {
14551482
.add(FeatureSet::Atomics, &Self::makeAtomic)
14561483
.add(FeatureSet::ExceptionHandling, &Self::makeTry)
14571484
.add(FeatureSet::ExceptionHandling, &Self::makeTryTable)
1485+
.add(FeatureSet::ExceptionHandling, &Self::makeImportThrowing)
14581486
.add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeCallRef)
14591487
.add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeStructSet)
14601488
.add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeArraySet)

test/lit/exec/fuzzing-api.wast

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited.
2+
3+
;; RUN: wasm-opt %s -all --fuzz-exec -o /dev/null 2>&1 | filecheck %s
4+
5+
;; Test the fuzzing-support module imports.
6+
7+
(module
8+
(import "fuzzing-support" "log-i32" (func $log-i32 (param i32)))
9+
(import "fuzzing-support" "log-f64" (func $log-f64 (param f64)))
10+
11+
(import "fuzzing-support" "throw" (func $throw))
12+
13+
;; CHECK: [fuzz-exec] calling logging
14+
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
15+
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
16+
(func $logging (export "logging")
17+
(call $log-i32
18+
(i32.const 42)
19+
)
20+
(call $log-f64
21+
(f64.const 3.14159)
22+
)
23+
)
24+
25+
;; CHECK: [fuzz-exec] calling throwing
26+
;; CHECK-NEXT: [exception thrown: __private ()]
27+
;; CHECK-NEXT: warning: no passes specified, not doing any work
28+
(func $throwing (export "throwing")
29+
(call $throw)
30+
)
31+
)
32+
;; CHECK: [fuzz-exec] calling logging
33+
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
34+
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
35+
36+
;; CHECK: [fuzz-exec] calling throwing
37+
;; CHECK-NEXT: [exception thrown: __private ()]
38+
;; CHECK-NEXT: [fuzz-exec] comparing logging
39+
;; CHECK-NEXT: [fuzz-exec] comparing throwing

test/lit/exec/host-limit.wast

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
(module
1010
(type $type$0 (array i8))
1111

12-
(import "fuzzing-support" "log" (func $log (param i32)))
12+
(import "fuzzing-support" "log-i32" (func $log (param i32)))
1313

1414
(global $global (mut (ref $type$0)) (array.new_default $type$0
1515
(i32.const -1)
Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
Metrics
22
total
3-
[exports] : 23
4-
[funcs] : 34
3+
[exports] : 50
4+
[funcs] : 72
55
[globals] : 9
66
[imports] : 4
77
[memories] : 1
88
[memory-data] : 2
9-
[table-data] : 6
9+
[table-data] : 25
1010
[tables] : 1
1111
[tags] : 0
12-
[total] : 4303
13-
[vars] : 100
14-
Binary : 355
15-
Block : 684
16-
Break : 149
17-
Call : 219
12+
[total] : 4381
13+
[vars] : 218
14+
Binary : 335
15+
Block : 725
16+
Break : 120
17+
Call : 210
1818
CallIndirect : 23
19-
Const : 643
20-
Drop : 50
21-
GlobalGet : 367
22-
GlobalSet : 258
23-
If : 206
24-
Load : 78
25-
LocalGet : 339
26-
LocalSet : 236
27-
Loop : 93
28-
Nop : 41
29-
RefFunc : 6
30-
Return : 45
31-
Select : 41
32-
Store : 36
33-
Switch : 1
34-
Unary : 304
35-
Unreachable : 129
19+
Const : 692
20+
Drop : 64
21+
GlobalGet : 391
22+
GlobalSet : 298
23+
If : 236
24+
Load : 71
25+
LocalGet : 285
26+
LocalSet : 209
27+
Loop : 76
28+
Nop : 63
29+
RefFunc : 25
30+
Return : 60
31+
Select : 23
32+
Store : 29
33+
Switch : 2
34+
Unary : 293
35+
Unreachable : 151

test/passes/simplify-locals_all-features.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,9 +1220,9 @@
12201220
(type $9 (func (result f64)))
12211221
(type $10 (func (param i32 i32) (result f64)))
12221222
(type $11 (func (param i32 i32) (result i32)))
1223-
(import "fuzzing-support" "log1" (func $fimport$0 (type $FUNCSIG$i) (result i32)))
1224-
(import "fuzzing-support" "log2" (func $fimport$1 (type $4) (param i32)))
1225-
(import "fuzzing-support" "log3" (func $fimport$2 (type $7) (param f32)))
1223+
(import "env" "get1" (func $fimport$0 (type $FUNCSIG$i) (result i32)))
1224+
(import "fuzzing-support" "log-i32" (func $fimport$1 (type $4) (param i32)))
1225+
(import "fuzzing-support" "log-f32" (func $fimport$2 (type $7) (param f32)))
12261226
(global $global$0 (mut i32) (i32.const 10))
12271227
(memory $0 256 256 shared)
12281228
(func $nonatomics (type $FUNCSIG$i) (result i32)

test/passes/simplify-locals_all-features.wast

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,9 +1194,9 @@
11941194
(type $4 (func (param i32)))
11951195
(type $5 (func (param i32) (result i32)))
11961196
(type $6 (func (param i32 i32 i32 i32 i32 i32)))
1197-
(import "fuzzing-support" "log1" (func $fimport$0 (result i32)))
1198-
(import "fuzzing-support" "log2" (func $fimport$1 (param i32)))
1199-
(import "fuzzing-support" "log3" (func $fimport$2 (param f32)))
1197+
(import "env" "get1" (func $fimport$0 (result i32)))
1198+
(import "fuzzing-support" "log-i32" (func $fimport$1 (param i32)))
1199+
(import "fuzzing-support" "log-f32" (func $fimport$2 (param f32)))
12001200
(memory 256 256 shared)
12011201
(global $global$0 (mut i32) (i32.const 10))
12021202
(func $nonatomics (result i32) ;; loads are reordered

0 commit comments

Comments
 (0)