Skip to content

Commit

Permalink
Fuzzer: Use a WebAssembly.Tag from JS (#7277)
Browse files Browse the repository at this point in the history
We now create a Tag in JS and import it in the wasm.

We can then use that tag from JS when we throw (only some of the
time, controlled by a new parameter to the throwing import).
  • Loading branch information
kripken authored Feb 7, 2025
1 parent 1de13c3 commit 3fcfec4
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 56 deletions.
16 changes: 13 additions & 3 deletions scripts/fuzz_shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,12 @@ var imports = {
'log-v128': logValue,

// Throw an exception from JS.
'throw': () => {
throw 'some JS error';
'throw': (which) => {
if (!which) {
throw 'some JS error';
} else {
throw new WebAssembly.Exception(jsTag, [which]);
}
},

// Table operations.
Expand Down Expand Up @@ -326,8 +330,14 @@ var imports = {
},
};

// If Tags are available, add the import j2wasm expects.
// 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({
'parameters': ['i32']
});

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

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

// The ModuleRunner and this ExternalInterface end up needing links both ways,
// so we cannot init this in the constructor.
ModuleRunner* instance = nullptr;
Expand All @@ -55,6 +58,13 @@ struct LoggingExternalInterface : public ShellExternalInterface {
break;
}
}

for (auto& tag : wasm.tags) {
if (tag->module == "fuzzing-support" && tag->base == "tag") {
fuzzTag = tag->name;
break;
}
}
}

Literals callImport(Function* import, const Literals& arguments) override {
Expand Down Expand Up @@ -82,7 +92,15 @@ struct LoggingExternalInterface : public ShellExternalInterface {
std::cout << "]\n";
return {};
} else if (import->base == "throw") {
throwEmptyException();
// Throw something, depending on the value of the argument. 0 means we
// 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();
} else {
auto payload = std::make_shared<ExnData>(fuzzTag, 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).
Expand Down
34 changes: 25 additions & 9 deletions src/tools/fuzzing/fuzzing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,8 @@ void TranslateToFuzzReader::setupGlobals() {
}

void TranslateToFuzzReader::setupTags() {
// As in modifyInitialFunctions(), we can't allow tag imports as it would trap
// when the fuzzing infrastructure doesn't know what to provide.
// As in modifyInitialFunctions(), we can't allow arbitrary tag imports, which
// would trap when the fuzzing infrastructure doesn't know what to provide.
for (auto& tag : wasm.tags) {
if (tag->imported()) {
tag->module = tag->base = Name();
Expand All @@ -647,6 +647,15 @@ void TranslateToFuzzReader::setupTags() {
for (size_t i = 0; i < num; i++) {
addTag();
}

// Add the fuzzing support tag 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));
}
}

void TranslateToFuzzReader::addTag() {
Expand Down Expand Up @@ -888,16 +897,14 @@ void TranslateToFuzzReader::addImportCallingSupport() {
}

void TranslateToFuzzReader::addImportThrowingSupport() {
// Throw some kind of exception from JS.
// TODO: Send an index, which is which exported wasm Tag we should throw, or
// something not exported if out of bounds. First we must also export
// tags sometimes.
// Throw some kind of exception from JS. If we send 0 then a pure JS
// exception is thrown, and any other value is the value in a wasm tag.
throwImportName = Names::getValidFunctionName(wasm, "throw");
auto func = std::make_unique<Function>();
func->name = throwImportName;
func->module = "fuzzing-support";
func->base = "throw";
func->type = Signature(Type::none, Type::none);
func->type = Signature(Type::i32, Type::none);
wasm.addFunction(std::move(func));
}

Expand Down Expand Up @@ -1067,12 +1074,21 @@ Expression* TranslateToFuzzReader::makeImportLogging() {
}

Expression* TranslateToFuzzReader::makeImportThrowing(Type type) {
// TODO: This and makeThrow should probably be rare, as they halt the program.

// We throw from the import, so this call appears to be none and not
// unreachable.
assert(type == Type::none);

// TODO: This and makeThrow should probably be rare, as they halt the program.
return builder.makeCall(throwImportName, {}, Type::none);
// An argument of 0 means to throw a JS exception, and otherwise the value in
// a wasm tag. Emit 0 or non-zero with ~equal probability.
Expression* arg;
if (oneIn(2)) {
arg = builder.makeConst(int32_t(0));
} else {
arg = makeConst(Type::i32);
}
return builder.makeCall(throwImportName, {arg}, Type::none);
}

Expression* TranslateToFuzzReader::makeImportTableGet() {
Expand Down
35 changes: 35 additions & 0 deletions test/lit/d8/fuzz_shell_exceptions.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
;; Test throwing from JS by calling the throw import.

(module
(import "fuzzing-support" "throw" (func $throw (param i32)))

(func $throwing-js (export "throwing-js")
;; Telling JS to throw with arg 0 leads to a JS exception thrown.
(call $throw
(i32.const 0)
)
)

(func $throwing-tag (export "throwing-tag")
;; A non-0 arg leads to a wasm Tag being thrown.
(call $throw
(i32.const 42)
)
)
)

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

;; Run in node.
;;
;; RUN: v8 %S/../../../scripts/fuzz_shell.js -- %t.wasm | filecheck %s
;;
;; CHECK: [fuzz-exec] calling throwing-js
;; CHECK: exception thrown: some JS error
;; CHECK: [fuzz-exec] calling throwing-tag
;; CHECK: exception thrown: [object WebAssembly.Exception]



22 changes: 20 additions & 2 deletions test/lit/exec/fuzzing-api.wast
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
(import "fuzzing-support" "log-i32" (func $log-i32 (param i32)))
(import "fuzzing-support" "log-f64" (func $log-f64 (param f64)))

(import "fuzzing-support" "throw" (func $throw))
(import "fuzzing-support" "throw" (func $throw (param i32)))

(import "fuzzing-support" "table-set" (func $table.set (param i32 funcref)))
(import "fuzzing-support" "table-get" (func $table.get (param i32) (result funcref)))
Expand All @@ -21,6 +21,8 @@

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

(import "fuzzing-support" "tag" (tag $imported-tag (param i32)))

(table $table 10 20 funcref)

;; Note that the exported table appears first here, but in the binary and in
Expand All @@ -42,7 +44,19 @@
;; CHECK: [fuzz-exec] calling throwing
;; CHECK-NEXT: [exception thrown: __private ()]
(func $throwing (export "throwing")
(call $throw)
;; Throwing 0 throws a JS ("private") exception.
(call $throw
(i32.const 0)
)
)

;; CHECK: [fuzz-exec] calling throwing-tag
;; CHECK-NEXT: [exception thrown: imported-tag 42]
(func $throwing-tag (export "throwing-tag")
;; Throwing non-0 throws using the tag we imported.
(call $throw
(i32.const 42)
)
)

;; CHECK: [fuzz-exec] calling table.setting
Expand Down Expand Up @@ -315,6 +329,9 @@
;; CHECK: [fuzz-exec] calling throwing
;; CHECK-NEXT: [exception thrown: __private ()]

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

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

Expand Down Expand Up @@ -386,3 +403,4 @@
;; CHECK-NEXT: [fuzz-exec] comparing table.getting
;; CHECK-NEXT: [fuzz-exec] comparing table.setting
;; CHECK-NEXT: [fuzz-exec] comparing throwing
;; CHECK-NEXT: [fuzz-exec] comparing throwing-tag
72 changes: 31 additions & 41 deletions test/passes/translate-to-fuzz_all-features_metrics_noprint.txt
Original file line number Diff line number Diff line change
@@ -1,52 +1,42 @@
Metrics
total
[exports] : 10
[funcs] : 9
[exports] : 15
[funcs] : 17
[globals] : 4
[imports] : 8
[imports] : 11
[memories] : 1
[memory-data] : 112
[table-data] : 0
[table-data] : 4
[tables] : 1
[tags] : 1
[total] : 553
[vars] : 38
ArrayCopy : 2
ArrayLen : 4
[tags] : 0
[total] : 688
[vars] : 15
ArrayNew : 1
ArrayNewFixed : 7
Binary : 69
Block : 55
BrOn : 1
Break : 1
Call : 23
Const : 107
Drop : 7
GlobalGet : 19
GlobalSet : 18
If : 17
Load : 17
LocalGet : 66
LocalSet : 39
Loop : 1
ArrayNewFixed : 5
Binary : 70
Block : 77
Call : 43
Const : 166
Drop : 8
GlobalGet : 43
GlobalSet : 38
If : 19
Load : 16
LocalGet : 45
LocalSet : 21
Loop : 3
Nop : 3
RefAs : 13
RefFunc : 9
RefI31 : 2
RefIsNull : 2
RefNull : 7
RefTest : 1
RefAs : 2
RefFunc : 28
RefNull : 8
Return : 5
SIMDReplace : 1
Select : 3
Select : 1
Store : 1
StringConst : 3
StringEncode : 1
StringMeasure : 1
StructGet : 1
StructNew : 15
StructSet : 2
TupleExtract : 1
StringConst : 2
StructNew : 37
StructSet : 1
Throw : 1
TryTable : 3
TupleMake : 3
Unary : 16
Unreachable : 9
Unary : 19
Unreachable : 19

0 comments on commit 3fcfec4

Please sign in to comment.