Skip to content

Commit 60d5626

Browse files
authored
[EH] Fuzz catch+rethrow on the JS side (#7287)
Add a parameter to call-export, call-ref, where the first bit says "catch and rethrow any exception". This has no observable effect in binaryen, but causes us to use different code paths in VMs (for example, a wasm exception may end up caught in JS, then thrown in JS; passing wasm exnrefs to JS for throwing is not possible otherwise, so this is the closest we can get).
1 parent fbcd71c commit 60d5626

File tree

5 files changed

+157
-25
lines changed

5 files changed

+157
-25
lines changed

scripts/fuzz_shell.js

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,18 @@ function callFunc(func) {
170170
return func.apply(null, args);
171171
}
172172

173-
// Calls a given function in a try-catch, swallowing JS exceptions, and return 1
174-
// if we did in fact swallow an exception. Wasm traps are not swallowed (see
175-
// details below).
176-
/* async */ function tryCall(func) {
173+
// Calls a given function in a try-catch. Return 1 if an exception was thrown.
174+
// If |rethrow| is set, and an exception is thrown, it is caught and rethrown.
175+
// Wasm traps are not swallowed (see details below).
176+
/* async */ function tryCall(func, rethrow) {
177177
try {
178178
/* await */ func();
179179
return 0;
180180
} catch (e) {
181+
// The exception must exist, and not behave oddly when we access a
182+
// property on it. (VM bugs could cause errors here.)
183+
e.a;
184+
181185
// We only want to catch exceptions, not wasm traps: traps should still
182186
// halt execution. Handling this requires different code in wasm2js, so
183187
// check for that first (wasm2js does not define RuntimeError, so use
@@ -208,7 +212,11 @@ function callFunc(func) {
208212
}
209213
}
210214
// Otherwise, this is a normal exception we want to catch (a wasm
211-
// exception, or a conversion error on the wasm/JS boundary, etc.).
215+
// exception, or a conversion error on the wasm/JS boundary, etc.). Rethrow
216+
// if we were asked to.
217+
if (rethrow) {
218+
throw e;
219+
}
212220
return 1;
213221
}
214222
}
@@ -271,7 +279,7 @@ var imports = {
271279
'throw': (which) => {
272280
if (!which) {
273281
// Throw a JS exception.
274-
throw 0;
282+
throw new Error('js exception');
275283
} else {
276284
// Throw a wasm exception.
277285
throw new WebAssembly.Exception(wasmTag, [which]);
@@ -287,19 +295,39 @@ var imports = {
287295
},
288296

289297
// Export operations.
290-
'call-export': /* async */ (index) => {
291-
/* await */ callFunc(exportList[index].value);
298+
'call-export': /* async */ (index, flags) => {
299+
var rethrow = flags & 1;
300+
if (JSPI) {
301+
// TODO: Figure out why JSPI fails here.
302+
rethrow = 0;
303+
}
304+
if (!rethrow) {
305+
/* await */ callFunc(exportList[index].value);
306+
} else {
307+
tryCall(/* async */ () => /* await */ callFunc(exportList[index].value),
308+
rethrow);
309+
}
292310
},
293311
'call-export-catch': /* async */ (index) => {
294312
return tryCall(/* async */ () => /* await */ callFunc(exportList[index].value));
295313
},
296314

297315
// Funcref operations.
298-
'call-ref': /* async */ (ref) => {
316+
'call-ref': /* async */ (ref, flags) => {
299317
// This is a direct function reference, and just like an export, it must
300318
// be wrapped for JSPI.
301319
ref = wrapExportForJSPI(ref);
302-
/* await */ callFunc(ref);
320+
var rethrow = flags & 1;
321+
if (JSPI) {
322+
// TODO: Figure out why JSPI fails here.
323+
rethrow = 0;
324+
}
325+
if (!rethrow) {
326+
/* await */ callFunc(ref);
327+
} else {
328+
tryCall(/* async */ () => /* await */ callFunc(ref),
329+
rethrow);
330+
}
303331
},
304332
'call-ref-catch': /* async */ (ref) => {
305333
ref = wrapExportForJSPI(ref);

src/tools/execution-results.h

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ struct LoggingExternalInterface : public ShellExternalInterface {
132132
return {};
133133
} else if (import->base == "call-export") {
134134
callExportAsJS(arguments[0].geti32());
135+
// The second argument determines if we should catch and rethrow
136+
// exceptions. There is no observable difference in those two modes in
137+
// the binaryen interpreter, so we don't need to do anything.
138+
135139
// Return nothing. If we wanted to return a value we'd need to have
136140
// multiple such functions, one for each signature.
137141
return {};
@@ -143,9 +147,8 @@ struct LoggingExternalInterface : public ShellExternalInterface {
143147
return {Literal(int32_t(1))};
144148
}
145149
} else if (import->base == "call-ref") {
150+
// Similar to call-export*, but with a ref.
146151
callRefAsJS(arguments[0]);
147-
// Return nothing. If we wanted to return a value we'd need to have
148-
// multiple such functions, one for each signature.
149152
return {};
150153
} else if (import->base == "call-ref-catch") {
151154
try {
@@ -181,11 +184,13 @@ struct LoggingExternalInterface : public ShellExternalInterface {
181184
}
182185

183186
void throwJSException() {
184-
// JS exceptions contain an externref, which wasm can't read (so the actual
185-
// value here does not matter, but it does need to match what the 'throw'
186-
// import does in fuzz_shell.js, as the fuzzer will do comparisons).
187-
Literal externref = Literal::makeI31(0, Unshared).externalize();
188-
Literals arguments = {externref};
187+
// JS exceptions contain an externref. Use the same type of value as a JS
188+
// exception would have, which is a reference to an object, and which will
189+
// print out "object" in the logging from JS. A trivial struct is enough for
190+
// us to log the same thing here.
191+
auto empty = HeapType(Struct{});
192+
auto inner = Literal(std::make_shared<GCData>(empty, Literals{}), empty);
193+
Literals arguments = {inner.externalize()};
189194
auto payload = std::make_shared<ExnData>(jsTag, arguments);
190195
throwException(WasmException{Literal(payload)});
191196
}

src/tools/fuzzing/fuzzing.cpp

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -851,12 +851,16 @@ void TranslateToFuzzReader::addImportCallingSupport() {
851851

852852
if (choice & 1) {
853853
// Given an export index, call it from JS.
854+
// A second parameter has flags. The first bit determines whether we catch
855+
// and rethrow all exceptions. (This ends up giving us the same signature
856+
// and behavior as when we do not rethrow, so we just add the flags here
857+
// rather than another export.)
854858
callExportImportName = Names::getValidFunctionName(wasm, "call-export");
855859
auto func = std::make_unique<Function>();
856860
func->name = callExportImportName;
857861
func->module = "fuzzing-support";
858862
func->base = "call-export";
859-
func->type = Signature({Type::i32}, Type::none);
863+
func->type = Signature({Type::i32, Type::i32}, Type::none);
860864
wasm.addFunction(std::move(func));
861865
}
862866

@@ -884,7 +888,10 @@ void TranslateToFuzzReader::addImportCallingSupport() {
884888
func->name = callRefImportName;
885889
func->module = "fuzzing-support";
886890
func->base = "call-ref";
887-
func->type = Signature({Type(HeapType::func, Nullable)}, Type::none);
891+
// As call-export, there is a flags param that allows us to catch+rethrow
892+
// all exceptions.
893+
func->type =
894+
Signature({Type(HeapType::func, Nullable), Type::i32}, Type::none);
888895
wasm.addFunction(std::move(func));
889896
}
890897

@@ -1135,7 +1142,13 @@ Expression* TranslateToFuzzReader::makeImportCallCode(Type type) {
11351142
if ((catching && (!exportTarget || oneIn(2))) || (!catching && oneIn(4))) {
11361143
// Most of the time make a non-nullable funcref, to avoid errors.
11371144
auto refType = Type(HeapType::func, oneIn(10) ? Nullable : NonNullable);
1138-
return builder.makeCall(refTarget, {make(refType)}, type);
1145+
std::vector<Expression*> args = {make(refType)};
1146+
if (!catching) {
1147+
// Only the first bit matters here, so we can send anything (this is
1148+
// future-proof for later bits, and has no downside now).
1149+
args.push_back(make(Type::i32));
1150+
}
1151+
return builder.makeCall(refTarget, args, type);
11391152
}
11401153
}
11411154

@@ -1163,7 +1176,16 @@ Expression* TranslateToFuzzReader::makeImportCallCode(Type type) {
11631176
index = builder.makeBinary(
11641177
RemUInt32, index, builder.makeConst(int32_t(maxIndex)));
11651178
}
1166-
return builder.makeCall(exportTarget, {index}, type);
1179+
1180+
// The non-catching variants send a flags argument, which says whether to
1181+
// catch+rethrow.
1182+
std::vector<Expression*> args = {index};
1183+
if (!catching) {
1184+
// Only the first bit matters here, so we can send anything (this is
1185+
// future-proof for later bits, and has no downside now).
1186+
args.push_back(make(Type::i32));
1187+
}
1188+
return builder.makeCall(exportTarget, args, type);
11671189
}
11681190

11691191
Expression* TranslateToFuzzReader::makeImportSleep(Type type) {

test/lit/d8/fuzz_shell_exceptions.wast

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
;; RUN: v8 %S/../../../scripts/fuzz_shell.js -- %t.wasm | filecheck %s
3232
;;
3333
;; CHECK: [fuzz-exec] calling throwing-js
34-
;; CHECK: exception thrown: 0
34+
;; CHECK: exception thrown: Error: js exception
3535
;; CHECK: [fuzz-exec] calling throwing-tag
3636
;; CHECK: exception thrown: [object WebAssembly.Exception]
3737

test/lit/exec/fuzzing-api.wast

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
(import "fuzzing-support" "table-set" (func $table.set (param i32 funcref)))
1414
(import "fuzzing-support" "table-get" (func $table.get (param i32) (result funcref)))
1515

16-
(import "fuzzing-support" "call-export" (func $call.export (param i32)))
16+
(import "fuzzing-support" "call-export" (func $call.export (param i32 i32)))
1717
(import "fuzzing-support" "call-export-catch" (func $call.export.catch (param i32) (result i32)))
1818

19-
(import "fuzzing-support" "call-ref" (func $call.ref (param funcref)))
19+
(import "fuzzing-support" "call-ref" (func $call.ref (param funcref i32)))
2020
(import "fuzzing-support" "call-ref-catch" (func $call.ref.catch (param funcref) (result i32)))
2121

2222
(import "fuzzing-support" "sleep" (func $sleep (param i32 i32) (result i32)))
@@ -110,10 +110,32 @@
110110
;; At index 0 in the exports we have $logging, so we will do those loggings.
111111
(call $call.export
112112
(i32.const 0)
113+
;; First bit unset in the flags means a normal call.
114+
(i32.const 0)
115+
)
116+
;; At index 999 we have nothing, so we'll error.
117+
(call $call.export
118+
(i32.const 999)
119+
(i32.const 0)
120+
)
121+
)
122+
123+
;; CHECK: [fuzz-exec] calling export.calling.rethrow
124+
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
125+
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
126+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
127+
(func $export.calling.rethrow (export "export.calling.rethrow")
128+
;; As above, but the second param is different.
129+
(call $call.export
130+
(i32.const 0)
131+
;; First bit set in the flags means a catch+rethrow. There is no visible
132+
;; effect here, but there might be in JS VMs.
133+
(i32.const 1)
113134
)
114135
;; At index 999 we have nothing, so we'll error.
115136
(call $call.export
116137
(i32.const 999)
138+
(i32.const 1)
117139
)
118140
)
119141

@@ -146,10 +168,31 @@
146168
;; This will emit some logging.
147169
(call $call.ref
148170
(ref.func $logging)
171+
;; Normal call.
172+
(i32.const 0)
173+
)
174+
;; This will throw.
175+
(call $call.ref
176+
(ref.null func)
177+
(i32.const 0)
178+
)
179+
)
180+
181+
;; CHECK: [fuzz-exec] calling ref.calling.rethrow
182+
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
183+
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
184+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
185+
(func $ref.calling.rethrow (export "ref.calling.rethrow")
186+
;; As with calling an export, when we set the flags to 1 exceptions are
187+
;; caught and rethrown, but there is no noticeable difference here.
188+
(call $call.ref
189+
(ref.func $logging)
190+
(i32.const 1)
149191
)
150192
;; This will throw.
151193
(call $call.ref
152194
(ref.null func)
195+
(i32.const 1)
153196
)
154197
)
155198

@@ -195,6 +238,7 @@
195238
;; logging from the function, "12".
196239
(call $call.ref
197240
(ref.func $legal)
241+
(i32.const 1)
198242
)
199243
)
200244

@@ -335,7 +379,6 @@
335379

336380
;; CHECK: [fuzz-exec] calling do-sleep
337381
;; CHECK-NEXT: [fuzz-exec] note result: do-sleep => 42
338-
;; CHECK-NEXT: warning: no passes specified, not doing any work
339382
(func $do-sleep (export "do-sleep") (result i32)
340383
(call $sleep
341384
;; A ridiculous amount of ms, but in the interpreter it is ignored anyhow.
@@ -344,6 +387,24 @@
344387
(i32.const 42)
345388
)
346389
)
390+
391+
;; CHECK: [fuzz-exec] calling return-externref-exception
392+
;; CHECK-NEXT: [fuzz-exec] note result: return-externref-exception => object
393+
;; CHECK-NEXT: warning: no passes specified, not doing any work
394+
(func $return-externref-exception (export "return-externref-exception") (result externref)
395+
;; Call JS table.set in a way that throws (on out of bounds). The JS exception
396+
;; is caught and returned from the function, so we can see what it looks like
397+
;; to the fuzzer, which should be "object" (an exception object).
398+
(block $block (result externref)
399+
(try_table (catch $imported-js-tag $block)
400+
(call $table.set
401+
(i32.const 99990)
402+
(ref.null func)
403+
)
404+
)
405+
(unreachable)
406+
)
407+
)
347408
)
348409
;; CHECK: [fuzz-exec] calling logging
349410
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
@@ -368,6 +429,11 @@
368429
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
369430
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
370431

432+
;; CHECK: [fuzz-exec] calling export.calling.rethrow
433+
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
434+
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
435+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
436+
371437
;; CHECK: [fuzz-exec] calling export.calling.catching
372438
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
373439
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
@@ -379,6 +445,11 @@
379445
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
380446
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
381447

448+
;; CHECK: [fuzz-exec] calling ref.calling.rethrow
449+
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
450+
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
451+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
452+
382453
;; CHECK: [fuzz-exec] calling ref.calling.catching
383454
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
384455
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
@@ -413,10 +484,14 @@
413484

414485
;; CHECK: [fuzz-exec] calling do-sleep
415486
;; CHECK-NEXT: [fuzz-exec] note result: do-sleep => 42
487+
488+
;; CHECK: [fuzz-exec] calling return-externref-exception
489+
;; CHECK-NEXT: [fuzz-exec] note result: return-externref-exception => object
416490
;; CHECK-NEXT: [fuzz-exec] comparing catch-js-tag
417491
;; CHECK-NEXT: [fuzz-exec] comparing do-sleep
418492
;; CHECK-NEXT: [fuzz-exec] comparing export.calling
419493
;; CHECK-NEXT: [fuzz-exec] comparing export.calling.catching
494+
;; CHECK-NEXT: [fuzz-exec] comparing export.calling.rethrow
420495
;; CHECK-NEXT: [fuzz-exec] comparing logging
421496
;; CHECK-NEXT: [fuzz-exec] comparing ref.calling
422497
;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.catching
@@ -426,7 +501,9 @@
426501
;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.illegal-v128
427502
;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.legal
428503
;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.legal-result
504+
;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.rethrow
429505
;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.trap
506+
;; CHECK-NEXT: [fuzz-exec] comparing return-externref-exception
430507
;; CHECK-NEXT: [fuzz-exec] comparing table.getting
431508
;; CHECK-NEXT: [fuzz-exec] comparing table.setting
432509
;; CHECK-NEXT: [fuzz-exec] comparing throwing

0 commit comments

Comments
 (0)