Skip to content

Commit 57c9c25

Browse files
authored
[EH] Fuzzer: Add WebAssembly.JSTag fuzzing (#7283)
This JS API represents the tag of JS exceptions. Import it into the wasm so that wasm can catch and throw JS exceptions. Rename the previous "jsTag", which means "wasm tag created in JS" to "wasmTag" to avoid confusion.
1 parent 3fcfec4 commit 57c9c25

File tree

7 files changed

+137
-66
lines changed

7 files changed

+137
-66
lines changed

scripts/fuzz_opt.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,18 @@ def can_run_on_wasm(self, wasm):
13721372
return not wasm_notices_export_changes(wasm)
13731373

13741374

1375+
# see https://github.com/WebAssembly/binaryen/issues/6823#issuecomment-2649122032
1376+
# as the interpreter refers to tags by name, two imports of the same Tag give it
1377+
# two different names, but they should behave as if they are one.
1378+
def wasm_has_duplicate_tags(wasm):
1379+
# as with wasm_notices_export_changes, we could be more precise here and
1380+
# disassemble the wasm.
1381+
binary = open(wasm, 'rb').read()
1382+
# check if we import jstag or wasmtag, which are used in the wasm, so any
1383+
# duplication may hit the github issue mentioned above.
1384+
return binary.count(b'jstag') >= 2 or binary.count(b'wasmtag') >= 2
1385+
1386+
13751387
# Tests wasm-merge
13761388
class Merge(TestCaseHandler):
13771389
frequency = 0.15
@@ -1424,6 +1436,10 @@ def handle(self, wasm):
14241436
abspath('second.wasm'), 'second', '-o', merged,
14251437
'--skip-export-conflicts'] + FEATURE_OPTS + ['-all'])
14261438

1439+
if wasm_has_duplicate_tags(merged):
1440+
note_ignored_vm_run('dupe_tags')
1441+
return
1442+
14271443
# sometimes also optimize the merged module
14281444
if random.random() < 0.5:
14291445
opts = get_random_opts()

scripts/fuzz_shell.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ var imports = {
272272
if (!which) {
273273
throw 'some JS error';
274274
} else {
275-
throw new WebAssembly.Exception(jsTag, [which]);
275+
throw new WebAssembly.Exception(wasmTag, [which]);
276276
}
277277
},
278278

@@ -333,10 +333,13 @@ var imports = {
333333
// If Tags are available, add some.
334334
if (typeof WebAssembly.Tag !== 'undefined') {
335335
// A tag for general use in the fuzzer.
336-
var jsTag = imports['fuzzing-support']['tag'] = new WebAssembly.Tag({
336+
var wasmTag = imports['fuzzing-support']['wasmtag'] = new WebAssembly.Tag({
337337
'parameters': ['i32']
338338
});
339339

340+
// The JSTag that represents a JS tag.
341+
imports['fuzzing-support']['jstag'] = WebAssembly.JSTag;
342+
340343
// This allows j2wasm content to run in the fuzzer.
341344
imports['imports'] = {
342345
'j2wasm.ExceptionUtils.tag': new WebAssembly.Tag({

src/tools/execution-results.h

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,13 @@ struct LoggingExternalInterface : public ShellExternalInterface {
4242
Name exportedTable;
4343
Module& wasm;
4444

45-
// The name of the imported fuzzing tag.
46-
Name fuzzTag;
45+
// The name of the imported fuzzing tag for wasm.
46+
Name wasmTag;
47+
48+
// The name of the imported tag for js exceptions. If it is not imported, we
49+
// use a default name here (which should differentiate it from any wasm
50+
// exceptions).
51+
Name jsTag = "__private";
4752

4853
// The ModuleRunner and this ExternalInterface end up needing links both ways,
4954
// so we cannot init this in the constructor.
@@ -60,9 +65,12 @@ struct LoggingExternalInterface : public ShellExternalInterface {
6065
}
6166

6267
for (auto& tag : wasm.tags) {
63-
if (tag->module == "fuzzing-support" && tag->base == "tag") {
64-
fuzzTag = tag->name;
65-
break;
68+
if (tag->module == "fuzzing-support") {
69+
if (tag->base == "wasmtag") {
70+
wasmTag = tag->name;
71+
} else if (tag->base == "jstag") {
72+
jsTag = tag->name;
73+
}
6674
}
6775
}
6876
}
@@ -96,29 +104,29 @@ struct LoggingExternalInterface : public ShellExternalInterface {
96104
// should throw a JS exception, and any other value means we should
97105
// throw a wasm exception (with that value as the payload).
98106
if (arguments[0].geti32() == 0) {
99-
throwEmptyException();
107+
throwJSException();
100108
} else {
101-
auto payload = std::make_shared<ExnData>(fuzzTag, arguments);
109+
auto payload = std::make_shared<ExnData>(wasmTag, arguments);
102110
throwException(WasmException{Literal(payload)});
103111
}
104112
} else if (import->base == "table-get") {
105113
// Check for errors here, duplicating tableLoad(), because that will
106114
// trap, and we just want to throw an exception (the same as JS would).
107115
if (!exportedTable) {
108-
throwEmptyException();
116+
throwJSException();
109117
}
110118
auto index = arguments[0].getUnsigned();
111119
if (index >= tables[exportedTable].size()) {
112-
throwEmptyException();
120+
throwJSException();
113121
}
114122
return {tableLoad(exportedTable, index)};
115123
} else if (import->base == "table-set") {
116124
if (!exportedTable) {
117-
throwEmptyException();
125+
throwJSException();
118126
}
119127
auto index = arguments[0].getUnsigned();
120128
if (index >= tables[exportedTable].size()) {
121-
throwEmptyException();
129+
throwJSException();
122130
}
123131
tableStore(exportedTable, index, arguments[1]);
124132
return {};
@@ -172,29 +180,32 @@ struct LoggingExternalInterface : public ShellExternalInterface {
172180
return {};
173181
}
174182

175-
void throwEmptyException() {
176-
// Use a hopefully private tag.
177-
auto payload = std::make_shared<ExnData>("__private", Literals{});
183+
void throwJSException() {
184+
// JS exceptions contain an externref, which wasm can't read (so the actual
185+
// value here does not matter).
186+
Literal externref = Literal::makeI31(0, Unshared).externalize();
187+
Literals arguments = {externref};
188+
auto payload = std::make_shared<ExnData>(jsTag, arguments);
178189
throwException(WasmException{Literal(payload)});
179190
}
180191

181192
Literals callExportAsJS(Index index) {
182193
if (index >= wasm.exports.size()) {
183194
// No export.
184-
throwEmptyException();
195+
throwJSException();
185196
}
186197
auto& exp = wasm.exports[index];
187198
if (exp->kind != ExternalKind::Function) {
188199
// No callable export.
189-
throwEmptyException();
200+
throwJSException();
190201
}
191202
return callFunctionAsJS(exp->value);
192203
}
193204

194205
Literals callRefAsJS(Literal ref) {
195206
if (!ref.isFunction()) {
196207
// Not a callable ref.
197-
throwEmptyException();
208+
throwJSException();
198209
}
199210
return callFunctionAsJS(ref.getFunc());
200211
}
@@ -210,10 +221,10 @@ struct LoggingExternalInterface : public ShellExternalInterface {
210221
// An i64 param can work from JS, but fuzz_shell provides 0, which errors
211222
// on attempts to convert it to BigInt. v128 and exnref are disalloewd.
212223
if (param == Type::i64 || param == Type::v128 || param.isExn()) {
213-
throwEmptyException();
224+
throwJSException();
214225
}
215226
if (!param.isDefaultable()) {
216-
throwEmptyException();
227+
throwJSException();
217228
}
218229
arguments.push_back(Literal::makeZero(param));
219230
}
@@ -224,7 +235,7 @@ struct LoggingExternalInterface : public ShellExternalInterface {
224235
// An i64 result is fine: a BigInt will be provided. But v128 and exnref
225236
// still error.
226237
if (result == Type::v128 || result.isExn()) {
227-
throwEmptyException();
238+
throwJSException();
228239
}
229240
}
230241

src/tools/fuzzing/fuzzing.cpp

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -648,13 +648,20 @@ void TranslateToFuzzReader::setupTags() {
648648
addTag();
649649
}
650650

651-
// Add the fuzzing support tag manually sometimes.
651+
// Add the fuzzing support tags manually sometimes.
652652
if (oneIn(2)) {
653-
auto tag = builder.makeTag(Names::getValidTagName(wasm, "tag"),
654-
Signature(Type::i32, Type::none));
655-
tag->module = "fuzzing-support";
656-
tag->base = "tag";
657-
wasm.addTag(std::move(tag));
653+
auto wasmTag = builder.makeTag(Names::getValidTagName(wasm, "wasmtag"),
654+
Signature(Type::i32, Type::none));
655+
wasmTag->module = "fuzzing-support";
656+
wasmTag->base = "wasmtag";
657+
wasm.addTag(std::move(wasmTag));
658+
659+
auto externref = Type(HeapType::ext, Nullable);
660+
auto jsTag = builder.makeTag(Names::getValidTagName(wasm, "jstag"),
661+
Signature(externref, Type::none));
662+
jsTag->module = "fuzzing-support";
663+
jsTag->base = "jstag";
664+
wasm.addTag(std::move(jsTag));
658665
}
659666
}
660667

test/lit/d8/fuzz_shell_exceptions.wast

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
(module
44
(import "fuzzing-support" "throw" (func $throw (param i32)))
55

6+
;; Verify that fuzz_shell.js provides these imports for the wasm.
7+
(import "fuzzing-support" "wasmtag" (tag $imported-wasm-tag (param i32)))
8+
(import "fuzzing-support" "jstag" (tag $imported-js-tag (param externref)))
9+
610
(func $throwing-js (export "throwing-js")
711
;; Telling JS to throw with arg 0 leads to a JS exception thrown.
812
(call $throw
@@ -20,7 +24,7 @@
2024

2125
;; Build to a binary wasm.
2226
;;
23-
;; RUN: wasm-opt %s -o %t.wasm -q
27+
;; RUN: wasm-opt %s -o %t.wasm -q -all
2428

2529
;; Run in node.
2630
;;

test/lit/exec/fuzzing-api.wast

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
(import "fuzzing-support" "sleep" (func $sleep (param i32 i32) (result i32)))
2323

24-
(import "fuzzing-support" "tag" (tag $imported-tag (param i32)))
24+
(import "fuzzing-support" "wasmtag" (tag $imported-wasm-tag (param i32)))
25+
(import "fuzzing-support" "jstag" (tag $imported-js-tag (param externref)))
2526

2627
(table $table 10 20 funcref)
2728

@@ -42,7 +43,7 @@
4243
)
4344

4445
;; CHECK: [fuzz-exec] calling throwing
45-
;; CHECK-NEXT: [exception thrown: __private ()]
46+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
4647
(func $throwing (export "throwing")
4748
;; Throwing 0 throws a JS ("private") exception.
4849
(call $throw
@@ -51,7 +52,7 @@
5152
)
5253

5354
;; CHECK: [fuzz-exec] calling throwing-tag
54-
;; CHECK-NEXT: [exception thrown: imported-tag 42]
55+
;; CHECK-NEXT: [exception thrown: imported-wasm-tag 42]
5556
(func $throwing-tag (export "throwing-tag")
5657
;; Throwing non-0 throws using the tag we imported.
5758
(call $throw
@@ -60,7 +61,7 @@
6061
)
6162

6263
;; CHECK: [fuzz-exec] calling table.setting
63-
;; CHECK-NEXT: [exception thrown: __private ()]
64+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
6465
(func $table.setting (export "table.setting")
6566
(call $table.set
6667
(i32.const 5)
@@ -76,7 +77,7 @@
7677
;; CHECK: [fuzz-exec] calling table.getting
7778
;; CHECK-NEXT: [LoggingExternalInterface logging 0]
7879
;; CHECK-NEXT: [LoggingExternalInterface logging 1]
79-
;; CHECK-NEXT: [exception thrown: __private ()]
80+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
8081
(func $table.getting (export "table.getting")
8182
;; There is a non-null value at 5, and a null at 6.
8283
(call $log-i32
@@ -104,7 +105,7 @@
104105
;; CHECK: [fuzz-exec] calling export.calling
105106
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
106107
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
107-
;; CHECK-NEXT: [exception thrown: __private ()]
108+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
108109
(func $export.calling (export "export.calling")
109110
;; At index 0 in the exports we have $logging, so we will do those loggings.
110111
(call $call.export
@@ -140,7 +141,7 @@
140141
;; CHECK: [fuzz-exec] calling ref.calling
141142
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
142143
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
143-
;; CHECK-NEXT: [exception thrown: __private ()]
144+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
144145
(func $ref.calling (export "ref.calling")
145146
;; This will emit some logging.
146147
(call $call.ref
@@ -310,6 +311,28 @@
310311
)
311312
)
312313

314+
;; CHECK: [fuzz-exec] calling catch-js-tag
315+
;; CHECK-NEXT: [fuzz-exec] note result: catch-js-tag => 100
316+
(func $catch-js-tag (export "catch-js-tag") (result i32)
317+
;; The table.set out of bounds will throw a JS exception, so it will be caught
318+
;; by the catch here, and we'll return the number at the end.
319+
(drop
320+
(block $out (result externref)
321+
(try_table (catch $imported-js-tag $out)
322+
(call $table.set
323+
(i32.const 9999)
324+
(ref.func $table.setting)
325+
)
326+
(return
327+
(i32.const -1)
328+
)
329+
)
330+
)
331+
)
332+
(i32.const 100)
333+
)
334+
335+
313336
;; CHECK: [fuzz-exec] calling do-sleep
314337
;; CHECK-NEXT: [fuzz-exec] note result: do-sleep => 42
315338
;; CHECK-NEXT: warning: no passes specified, not doing any work
@@ -327,23 +350,23 @@
327350
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
328351

329352
;; CHECK: [fuzz-exec] calling throwing
330-
;; CHECK-NEXT: [exception thrown: __private ()]
353+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
331354

332355
;; CHECK: [fuzz-exec] calling throwing-tag
333-
;; CHECK-NEXT: [exception thrown: imported-tag 42]
356+
;; CHECK-NEXT: [exception thrown: imported-wasm-tag 42]
334357

335358
;; CHECK: [fuzz-exec] calling table.setting
336-
;; CHECK-NEXT: [exception thrown: __private ()]
359+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
337360

338361
;; CHECK: [fuzz-exec] calling table.getting
339362
;; CHECK-NEXT: [LoggingExternalInterface logging 0]
340363
;; CHECK-NEXT: [LoggingExternalInterface logging 1]
341-
;; CHECK-NEXT: [exception thrown: __private ()]
364+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
342365

343366
;; CHECK: [fuzz-exec] calling export.calling
344367
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
345368
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
346-
;; CHECK-NEXT: [exception thrown: __private ()]
369+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
347370

348371
;; CHECK: [fuzz-exec] calling export.calling.catching
349372
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
@@ -354,7 +377,7 @@
354377
;; CHECK: [fuzz-exec] calling ref.calling
355378
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
356379
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
357-
;; CHECK-NEXT: [exception thrown: __private ()]
380+
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
358381

359382
;; CHECK: [fuzz-exec] calling ref.calling.catching
360383
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
@@ -385,8 +408,12 @@
385408
;; CHECK: [fuzz-exec] calling ref.calling.trap
386409
;; CHECK-NEXT: [trap unreachable]
387410

411+
;; CHECK: [fuzz-exec] calling catch-js-tag
412+
;; CHECK-NEXT: [fuzz-exec] note result: catch-js-tag => 100
413+
388414
;; CHECK: [fuzz-exec] calling do-sleep
389415
;; CHECK-NEXT: [fuzz-exec] note result: do-sleep => 42
416+
;; CHECK-NEXT: [fuzz-exec] comparing catch-js-tag
390417
;; CHECK-NEXT: [fuzz-exec] comparing do-sleep
391418
;; CHECK-NEXT: [fuzz-exec] comparing export.calling
392419
;; CHECK-NEXT: [fuzz-exec] comparing export.calling.catching

0 commit comments

Comments
 (0)