Skip to content

[EH] Fuzzer: Add WebAssembly.JSTag fuzzing #7283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions scripts/fuzz_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,18 @@ def can_run_on_wasm(self, wasm):
return not wasm_notices_export_changes(wasm)


# see https://github.com/WebAssembly/binaryen/issues/6823#issuecomment-2649122032
# as the interpreter refers to tags by name, two imports of the same Tag give it
# two different names, but they should behave as if they are one.
def wasm_has_duplicate_tags(wasm):
# as with wasm_notices_export_changes, we could be more precise here and
# disassemble the wasm.
binary = open(wasm, 'rb').read()
# check if we import jstag or wasmtag, which are used in the wasm, so any
# duplication may hit the github issue mentioned above.
return binary.count(b'jstag') >= 2 or binary.count(b'wasmtag') >= 2


# Tests wasm-merge
class Merge(TestCaseHandler):
frequency = 0.15
Expand Down Expand Up @@ -1424,6 +1436,10 @@ def handle(self, wasm):
abspath('second.wasm'), 'second', '-o', merged,
'--skip-export-conflicts'] + FEATURE_OPTS + ['-all'])

if wasm_has_duplicate_tags(merged):
note_ignored_vm_run('dupe_tags')
return

# sometimes also optimize the merged module
if random.random() < 0.5:
opts = get_random_opts()
Expand Down
7 changes: 5 additions & 2 deletions scripts/fuzz_shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ var imports = {
if (!which) {
throw 'some JS error';
} else {
throw new WebAssembly.Exception(jsTag, [which]);
throw new WebAssembly.Exception(wasmTag, [which]);
}
},

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

// The JSTag that represents a JS tag.
imports['fuzzing-support']['jstag'] = WebAssembly.JSTag;

// This allows j2wasm content to run in the fuzzer.
imports['imports'] = {
'j2wasm.ExceptionUtils.tag': new WebAssembly.Tag({
Expand Down
51 changes: 31 additions & 20 deletions src/tools/execution-results.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,13 @@ struct LoggingExternalInterface : public ShellExternalInterface {
Name exportedTable;
Module& wasm;

// The name of the imported fuzzing tag.
Name fuzzTag;
// The name of the imported fuzzing tag for wasm.
Name wasmTag;

// The name of the imported tag for js exceptions. If it is not imported, we
// use a default name here (which should differentiate it from any wasm
// exceptions).
Name jsTag = "__private";

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

for (auto& tag : wasm.tags) {
if (tag->module == "fuzzing-support" && tag->base == "tag") {
fuzzTag = tag->name;
break;
if (tag->module == "fuzzing-support") {
if (tag->base == "wasmtag") {
wasmTag = tag->name;
} else if (tag->base == "jstag") {
jsTag = tag->name;
}
}
}
}
Expand Down Expand Up @@ -96,29 +104,29 @@ struct LoggingExternalInterface : public ShellExternalInterface {
// should throw a JS exception, and any other value means we should
// throw a wasm exception (with that value as the payload).
if (arguments[0].geti32() == 0) {
throwEmptyException();
throwJSException();
} else {
auto payload = std::make_shared<ExnData>(fuzzTag, arguments);
auto payload = std::make_shared<ExnData>(wasmTag, arguments);
throwException(WasmException{Literal(payload)});
}
} else if (import->base == "table-get") {
// Check for errors here, duplicating tableLoad(), because that will
// trap, and we just want to throw an exception (the same as JS would).
if (!exportedTable) {
throwEmptyException();
throwJSException();
}
auto index = arguments[0].getUnsigned();
if (index >= tables[exportedTable].size()) {
throwEmptyException();
throwJSException();
}
return {tableLoad(exportedTable, index)};
} else if (import->base == "table-set") {
if (!exportedTable) {
throwEmptyException();
throwJSException();
}
auto index = arguments[0].getUnsigned();
if (index >= tables[exportedTable].size()) {
throwEmptyException();
throwJSException();
}
tableStore(exportedTable, index, arguments[1]);
return {};
Expand Down Expand Up @@ -172,29 +180,32 @@ struct LoggingExternalInterface : public ShellExternalInterface {
return {};
}

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

Literals callExportAsJS(Index index) {
if (index >= wasm.exports.size()) {
// No export.
throwEmptyException();
throwJSException();
}
auto& exp = wasm.exports[index];
if (exp->kind != ExternalKind::Function) {
// No callable export.
throwEmptyException();
throwJSException();
}
return callFunctionAsJS(exp->value);
}

Literals callRefAsJS(Literal ref) {
if (!ref.isFunction()) {
// Not a callable ref.
throwEmptyException();
throwJSException();
}
return callFunctionAsJS(ref.getFunc());
}
Expand All @@ -210,10 +221,10 @@ struct LoggingExternalInterface : public ShellExternalInterface {
// An i64 param can work from JS, but fuzz_shell provides 0, which errors
// on attempts to convert it to BigInt. v128 and exnref are disalloewd.
if (param == Type::i64 || param == Type::v128 || param.isExn()) {
throwEmptyException();
throwJSException();
}
if (!param.isDefaultable()) {
throwEmptyException();
throwJSException();
}
arguments.push_back(Literal::makeZero(param));
}
Expand All @@ -224,7 +235,7 @@ struct LoggingExternalInterface : public ShellExternalInterface {
// An i64 result is fine: a BigInt will be provided. But v128 and exnref
// still error.
if (result == Type::v128 || result.isExn()) {
throwEmptyException();
throwJSException();
}
}

Expand Down
19 changes: 13 additions & 6 deletions src/tools/fuzzing/fuzzing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -648,13 +648,20 @@ void TranslateToFuzzReader::setupTags() {
addTag();
}

// Add the fuzzing support tag manually sometimes.
// Add the fuzzing support tags manually sometimes.
if (oneIn(2)) {
auto tag = builder.makeTag(Names::getValidTagName(wasm, "tag"),
Signature(Type::i32, Type::none));
tag->module = "fuzzing-support";
tag->base = "tag";
wasm.addTag(std::move(tag));
auto wasmTag = builder.makeTag(Names::getValidTagName(wasm, "wasmtag"),
Signature(Type::i32, Type::none));
wasmTag->module = "fuzzing-support";
wasmTag->base = "wasmtag";
wasm.addTag(std::move(wasmTag));

auto externref = Type(HeapType::ext, Nullable);
auto jsTag = builder.makeTag(Names::getValidTagName(wasm, "jstag"),
Signature(externref, Type::none));
jsTag->module = "fuzzing-support";
jsTag->base = "jstag";
wasm.addTag(std::move(jsTag));
}
}

Expand Down
6 changes: 5 additions & 1 deletion test/lit/d8/fuzz_shell_exceptions.wast
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
(module
(import "fuzzing-support" "throw" (func $throw (param i32)))

;; Verify that fuzz_shell.js provides these imports for the wasm.
(import "fuzzing-support" "wasmtag" (tag $imported-wasm-tag (param i32)))
(import "fuzzing-support" "jstag" (tag $imported-js-tag (param externref)))

(func $throwing-js (export "throwing-js")
;; Telling JS to throw with arg 0 leads to a JS exception thrown.
(call $throw
Expand All @@ -20,7 +24,7 @@

;; Build to a binary wasm.
;;
;; RUN: wasm-opt %s -o %t.wasm -q
;; RUN: wasm-opt %s -o %t.wasm -q -all

;; Run in node.
;;
Expand Down
53 changes: 40 additions & 13 deletions test/lit/exec/fuzzing-api.wast
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@

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

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

(table $table 10 20 funcref)

Expand All @@ -42,7 +43,7 @@
)

;; CHECK: [fuzz-exec] calling throwing
;; CHECK-NEXT: [exception thrown: __private ()]
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
(func $throwing (export "throwing")
;; Throwing 0 throws a JS ("private") exception.
(call $throw
Expand All @@ -51,7 +52,7 @@
)

;; CHECK: [fuzz-exec] calling throwing-tag
;; CHECK-NEXT: [exception thrown: imported-tag 42]
;; CHECK-NEXT: [exception thrown: imported-wasm-tag 42]
(func $throwing-tag (export "throwing-tag")
;; Throwing non-0 throws using the tag we imported.
(call $throw
Expand All @@ -60,7 +61,7 @@
)

;; CHECK: [fuzz-exec] calling table.setting
;; CHECK-NEXT: [exception thrown: __private ()]
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
(func $table.setting (export "table.setting")
(call $table.set
(i32.const 5)
Expand All @@ -76,7 +77,7 @@
;; CHECK: [fuzz-exec] calling table.getting
;; CHECK-NEXT: [LoggingExternalInterface logging 0]
;; CHECK-NEXT: [LoggingExternalInterface logging 1]
;; CHECK-NEXT: [exception thrown: __private ()]
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
(func $table.getting (export "table.getting")
;; There is a non-null value at 5, and a null at 6.
(call $log-i32
Expand Down Expand Up @@ -104,7 +105,7 @@
;; CHECK: [fuzz-exec] calling export.calling
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
;; CHECK-NEXT: [exception thrown: __private ()]
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
(func $export.calling (export "export.calling")
;; At index 0 in the exports we have $logging, so we will do those loggings.
(call $call.export
Expand Down Expand Up @@ -140,7 +141,7 @@
;; CHECK: [fuzz-exec] calling ref.calling
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
;; CHECK-NEXT: [exception thrown: __private ()]
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]
(func $ref.calling (export "ref.calling")
;; This will emit some logging.
(call $call.ref
Expand Down Expand Up @@ -310,6 +311,28 @@
)
)

;; CHECK: [fuzz-exec] calling catch-js-tag
;; CHECK-NEXT: [fuzz-exec] note result: catch-js-tag => 100
(func $catch-js-tag (export "catch-js-tag") (result i32)
;; The table.set out of bounds will throw a JS exception, so it will be caught
;; by the catch here, and we'll return the number at the end.
(drop
(block $out (result externref)
(try_table (catch $imported-js-tag $out)
(call $table.set
(i32.const 9999)
(ref.func $table.setting)
)
(return
(i32.const -1)
)
)
)
)
(i32.const 100)
)


;; CHECK: [fuzz-exec] calling do-sleep
;; CHECK-NEXT: [fuzz-exec] note result: do-sleep => 42
;; CHECK-NEXT: warning: no passes specified, not doing any work
Expand All @@ -327,23 +350,23 @@
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]

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

;; CHECK: [fuzz-exec] calling throwing-tag
;; CHECK-NEXT: [exception thrown: imported-tag 42]
;; CHECK-NEXT: [exception thrown: imported-wasm-tag 42]

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

;; CHECK: [fuzz-exec] calling table.getting
;; CHECK-NEXT: [LoggingExternalInterface logging 0]
;; CHECK-NEXT: [LoggingExternalInterface logging 1]
;; CHECK-NEXT: [exception thrown: __private ()]
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]

;; CHECK: [fuzz-exec] calling export.calling
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
;; CHECK-NEXT: [exception thrown: __private ()]
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]

;; CHECK: [fuzz-exec] calling export.calling.catching
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
Expand All @@ -354,7 +377,7 @@
;; CHECK: [fuzz-exec] calling ref.calling
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
;; CHECK-NEXT: [exception thrown: __private ()]
;; CHECK-NEXT: [exception thrown: imported-js-tag externref]

;; CHECK: [fuzz-exec] calling ref.calling.catching
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
Expand Down Expand Up @@ -385,8 +408,12 @@
;; CHECK: [fuzz-exec] calling ref.calling.trap
;; CHECK-NEXT: [trap unreachable]

;; CHECK: [fuzz-exec] calling catch-js-tag
;; CHECK-NEXT: [fuzz-exec] note result: catch-js-tag => 100

;; CHECK: [fuzz-exec] calling do-sleep
;; CHECK-NEXT: [fuzz-exec] note result: do-sleep => 42
;; CHECK-NEXT: [fuzz-exec] comparing catch-js-tag
;; CHECK-NEXT: [fuzz-exec] comparing do-sleep
;; CHECK-NEXT: [fuzz-exec] comparing export.calling
;; CHECK-NEXT: [fuzz-exec] comparing export.calling.catching
Expand Down
Loading