Skip to content

Commit

Permalink
[EH] Fuzz throws from JS (WebAssembly#7027)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kripken authored Oct 23, 2024
1 parent 0d9b750 commit dcc70bb
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 115 deletions.
7 changes: 7 additions & 0 deletions scripts/fuzz_shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ function logValue(x, y) {
var tempRet0;
var imports = {
'fuzzing-support': {
// Logging.
'log-i32': logValue,
'log-i64': logValue,
'log-f32': logValue,
Expand All @@ -147,7 +148,13 @@ var imports = {
// we could avoid running JS on code with SIMD in it, but it is useful to
// fuzz such code as much as we can.)
'log-v128': logValue,

// Throw an exception from JS.
'throw': () => {
throw 'some JS error';
}
},
// Emscripten support.
'env': {
'setTempRet0': function(x) { tempRet0 = x },
'getTempRet0': function() { return tempRet0 },
Expand Down
47 changes: 29 additions & 18 deletions src/tools/execution-results.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ using Loggings = std::vector<Literal>;

// Logs every relevant import call parameter.
struct LoggingExternalInterface : public ShellExternalInterface {
private:
Loggings& loggings;

struct State {
Expand All @@ -37,30 +38,40 @@ struct LoggingExternalInterface : public ShellExternalInterface {
uint32_t tempRet0 = 0;
} state;

public:
LoggingExternalInterface(Loggings& loggings) : loggings(loggings) {}

Literals callImport(Function* import, const Literals& arguments) override {
if (import->module == "fuzzing-support") {
std::cout << "[LoggingExternalInterface logging";
loggings.push_back(Literal()); // buffer with a None between calls
for (auto argument : arguments) {
if (argument.type == Type::i64) {
// To avoid JS legalization changing logging results, treat a logging
// of an i64 as two i32s (which is what legalization would turn us
// into).
auto low = Literal(int32_t(argument.getInteger()));
auto high = Literal(int32_t(argument.getInteger() >> int32_t(32)));
std::cout << ' ' << low;
loggings.push_back(low);
std::cout << ' ' << high;
loggings.push_back(high);
} else {
std::cout << ' ' << argument;
loggings.push_back(argument);
if (import->base.startsWith("log")) {
// This is a logging function like log-i32 or log-f64
std::cout << "[LoggingExternalInterface logging";
loggings.push_back(Literal()); // buffer with a None between calls
for (auto argument : arguments) {
if (argument.type == Type::i64) {
// To avoid JS legalization changing logging results, treat a
// logging of an i64 as two i32s (which is what legalization would
// turn us into).
auto low = Literal(int32_t(argument.getInteger()));
auto high = Literal(int32_t(argument.getInteger() >> int32_t(32)));
std::cout << ' ' << low;
loggings.push_back(low);
std::cout << ' ' << high;
loggings.push_back(high);
} else {
std::cout << ' ' << argument;
loggings.push_back(argument);
}
}
std::cout << "]\n";
return {};
} else if (import->base == "throw") {
// Throw something. We use a (hopefully) private name here.
auto payload = std::make_shared<ExnData>("__private", Literals{});
throwException(WasmException{Literal(payload)});
} else {
WASM_UNREACHABLE("unknown fuzzer import");
}
std::cout << "]\n";
return {};
} else if (import->module == ENV) {
if (import->base == "log_execution") {
std::cout << "[LoggingExternalInterface log-execution";
Expand Down
10 changes: 9 additions & 1 deletion src/tools/fuzzing.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ class TranslateToFuzzReader {

Name funcrefTableName;

std::unordered_map<Type, Name> logImportNames;

Name throwImportName;

std::unordered_map<Type, std::vector<Name>> globalsByType;
std::unordered_map<Type, std::vector<Name>> mutableGlobalsByType;
std::unordered_map<Type, std::vector<Name>> immutableGlobalsByType;
Expand Down Expand Up @@ -220,12 +224,16 @@ class TranslateToFuzzReader {
void finalizeTable();
void prepareHangLimitSupport();
void addHangLimitSupport();
// Imports that we call to log out values.
void addImportLoggingSupport();
// An import that we call to throw an exception from outside.
void addImportThrowingSupport();
void addHashMemorySupport();

// Special expression makers
Expression* makeHangLimitCheck();
Expression* makeLogging();
Expression* makeImportLogging();
Expression* makeImportThrowing(Type type);
Expression* makeMemoryHashLogging();

// Function creation
Expand Down
66 changes: 47 additions & 19 deletions src/tools/fuzzing/fuzzing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,10 @@ void TranslateToFuzzReader::build() {
setupGlobals();
if (wasm.features.hasExceptionHandling()) {
setupTags();
addImportThrowingSupport();
}
modifyInitialFunctions();
addImportLoggingSupport();
modifyInitialFunctions();
// keep adding functions until we run out of input
while (!random.finished()) {
auto* func = addFunction();
Expand Down Expand Up @@ -583,16 +584,31 @@ void TranslateToFuzzReader::addHangLimitSupport() {

void TranslateToFuzzReader::addImportLoggingSupport() {
for (auto type : loggableTypes) {
auto* func = new Function;
Name name = std::string("log-") + type.toString();
func->name = name;
auto func = std::make_unique<Function>();
Name baseName = std::string("log-") + type.toString();
func->name = Names::getValidFunctionName(wasm, baseName);
logImportNames[type] = func->name;
func->module = "fuzzing-support";
func->base = name;
func->base = baseName;
func->type = Signature(type, Type::none);
wasm.addFunction(func);
wasm.addFunction(std::move(func));
}
}

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.
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);
wasm.addFunction(std::move(func));
}

void TranslateToFuzzReader::addHashMemorySupport() {
// Add memory hasher helper (for the hash, see hash.h). The function looks
// like:
Expand Down Expand Up @@ -692,21 +708,30 @@ Expression* TranslateToFuzzReader::makeHangLimitCheck() {
builder.makeConst(int32_t(1)))));
}

Expression* TranslateToFuzzReader::makeLogging() {
Expression* TranslateToFuzzReader::makeImportLogging() {
auto type = getLoggableType();
return builder.makeCall(
std::string("log-") + type.toString(), {make(type)}, Type::none);
return builder.makeCall(logImportNames[type], {make(type)}, Type::none);
}

Expression* TranslateToFuzzReader::makeImportThrowing(Type type) {
// 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);
}

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

// TODO: return std::unique_ptr<Function>
Function* TranslateToFuzzReader::addFunction() {
LOGGING_PERCENT = upToSquared(100);
auto* func = new Function;
auto allocation = std::make_unique<Function>();
auto* func = allocation.get();
func->name = Names::getValidFunctionName(wasm, "func");
FunctionCreationContext context(*this, func);
assert(funcContext->typeLocals.empty());
Expand Down Expand Up @@ -765,7 +790,7 @@ Function* TranslateToFuzzReader::addFunction() {
}

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

void visitDrop(Drop* curr) {
if (parent.isLoggableType(curr->value->type) && parent.oneIn(2)) {
replaceCurrent(parent.builder.makeCall(std::string("log-") +
curr->value->type.toString(),
{curr->value},
Type::none));
auto target = parent.logImportNames[curr->value->type];
replaceCurrent(
parent.builder.makeCall(target, {curr->value}, Type::none));
}
}
};
Expand Down Expand Up @@ -1430,7 +1457,7 @@ Expression* TranslateToFuzzReader::_makenone() {
auto choice = upTo(100);
if (choice < LOGGING_PERCENT) {
if (choice < LOGGING_PERCENT / 2) {
return makeLogging();
return makeImportLogging();
} else {
return makeMemoryHashLogging();
}
Expand All @@ -1455,6 +1482,7 @@ Expression* TranslateToFuzzReader::_makenone() {
.add(FeatureSet::Atomics, &Self::makeAtomic)
.add(FeatureSet::ExceptionHandling, &Self::makeTry)
.add(FeatureSet::ExceptionHandling, &Self::makeTryTable)
.add(FeatureSet::ExceptionHandling, &Self::makeImportThrowing)
.add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeCallRef)
.add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeStructSet)
.add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeArraySet)
Expand Down
39 changes: 39 additions & 0 deletions test/lit/exec/fuzzing-api.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited.

;; RUN: wasm-opt %s -all --fuzz-exec -o /dev/null 2>&1 | filecheck %s

;; Test the fuzzing-support module imports.

(module
(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))

;; CHECK: [fuzz-exec] calling logging
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]
(func $logging (export "logging")
(call $log-i32
(i32.const 42)
)
(call $log-f64
(f64.const 3.14159)
)
)

;; CHECK: [fuzz-exec] calling throwing
;; CHECK-NEXT: [exception thrown: __private ()]
;; CHECK-NEXT: warning: no passes specified, not doing any work
(func $throwing (export "throwing")
(call $throw)
)
)
;; CHECK: [fuzz-exec] calling logging
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159]

;; CHECK: [fuzz-exec] calling throwing
;; CHECK-NEXT: [exception thrown: __private ()]
;; CHECK-NEXT: [fuzz-exec] comparing logging
;; CHECK-NEXT: [fuzz-exec] comparing throwing
2 changes: 1 addition & 1 deletion test/lit/exec/host-limit.wast
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
(module
(type $type$0 (array i8))

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

(global $global (mut (ref $type$0)) (array.new_default $type$0
(i32.const -1)
Expand Down
52 changes: 26 additions & 26 deletions test/passes/fuzz_metrics_noprint.bin.txt
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
Metrics
total
[exports] : 23
[funcs] : 34
[exports] : 50
[funcs] : 72
[globals] : 9
[imports] : 4
[memories] : 1
[memory-data] : 2
[table-data] : 6
[table-data] : 25
[tables] : 1
[tags] : 0
[total] : 4303
[vars] : 100
Binary : 355
Block : 684
Break : 149
Call : 219
[total] : 4381
[vars] : 218
Binary : 335
Block : 725
Break : 120
Call : 210
CallIndirect : 23
Const : 643
Drop : 50
GlobalGet : 367
GlobalSet : 258
If : 206
Load : 78
LocalGet : 339
LocalSet : 236
Loop : 93
Nop : 41
RefFunc : 6
Return : 45
Select : 41
Store : 36
Switch : 1
Unary : 304
Unreachable : 129
Const : 692
Drop : 64
GlobalGet : 391
GlobalSet : 298
If : 236
Load : 71
LocalGet : 285
LocalSet : 209
Loop : 76
Nop : 63
RefFunc : 25
Return : 60
Select : 23
Store : 29
Switch : 2
Unary : 293
Unreachable : 151
6 changes: 3 additions & 3 deletions test/passes/simplify-locals_all-features.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1220,9 +1220,9 @@
(type $9 (func (result f64)))
(type $10 (func (param i32 i32) (result f64)))
(type $11 (func (param i32 i32) (result i32)))
(import "fuzzing-support" "log1" (func $fimport$0 (type $FUNCSIG$i) (result i32)))
(import "fuzzing-support" "log2" (func $fimport$1 (type $4) (param i32)))
(import "fuzzing-support" "log3" (func $fimport$2 (type $7) (param f32)))
(import "env" "get1" (func $fimport$0 (type $FUNCSIG$i) (result i32)))
(import "fuzzing-support" "log-i32" (func $fimport$1 (type $4) (param i32)))
(import "fuzzing-support" "log-f32" (func $fimport$2 (type $7) (param f32)))
(global $global$0 (mut i32) (i32.const 10))
(memory $0 256 256 shared)
(func $nonatomics (type $FUNCSIG$i) (result i32)
Expand Down
6 changes: 3 additions & 3 deletions test/passes/simplify-locals_all-features.wast
Original file line number Diff line number Diff line change
Expand Up @@ -1194,9 +1194,9 @@
(type $4 (func (param i32)))
(type $5 (func (param i32) (result i32)))
(type $6 (func (param i32 i32 i32 i32 i32 i32)))
(import "fuzzing-support" "log1" (func $fimport$0 (result i32)))
(import "fuzzing-support" "log2" (func $fimport$1 (param i32)))
(import "fuzzing-support" "log3" (func $fimport$2 (param f32)))
(import "env" "get1" (func $fimport$0 (result i32)))
(import "fuzzing-support" "log-i32" (func $fimport$1 (param i32)))
(import "fuzzing-support" "log-f32" (func $fimport$2 (param f32)))
(memory 256 256 shared)
(global $global$0 (mut i32) (i32.const 10))
(func $nonatomics (result i32) ;; loads are reordered
Expand Down
Loading

0 comments on commit dcc70bb

Please sign in to comment.