From dcc70bbfb16c2f8fce29dad94d80d1b78123655f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 23 Oct 2024 10:17:13 -0700 Subject: [PATCH] [EH] Fuzz throws from JS (#7027) 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. --- scripts/fuzz_shell.js | 7 ++ src/tools/execution-results.h | 47 +++++++---- src/tools/fuzzing.h | 10 ++- src/tools/fuzzing/fuzzing.cpp | 66 ++++++++++----- test/lit/exec/fuzzing-api.wast | 39 +++++++++ test/lit/exec/host-limit.wast | 2 +- test/passes/fuzz_metrics_noprint.bin.txt | 52 ++++++------ test/passes/simplify-locals_all-features.txt | 6 +- test/passes/simplify-locals_all-features.wast | 6 +- ...ll-features_disable-exception-handling.txt | 6 +- ...l-features_disable-exception-handling.wast | 6 +- ...e-to-fuzz_all-features_metrics_noprint.txt | 83 ++++++++++--------- 12 files changed, 215 insertions(+), 115 deletions(-) create mode 100644 test/lit/exec/fuzzing-api.wast diff --git a/scripts/fuzz_shell.js b/scripts/fuzz_shell.js index 3d29b197ce8..4cf3ba35866 100644 --- a/scripts/fuzz_shell.js +++ b/scripts/fuzz_shell.js @@ -138,6 +138,7 @@ function logValue(x, y) { var tempRet0; var imports = { 'fuzzing-support': { + // Logging. 'log-i32': logValue, 'log-i64': logValue, 'log-f32': logValue, @@ -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 }, diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 920bb200ac6..d9fefc44ec7 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -27,6 +27,7 @@ using Loggings = std::vector; // Logs every relevant import call parameter. struct LoggingExternalInterface : public ShellExternalInterface { +private: Loggings& loggings; struct State { @@ -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("__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"; diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h index 92e99791341..69fee656c07 100644 --- a/src/tools/fuzzing.h +++ b/src/tools/fuzzing.h @@ -104,6 +104,10 @@ class TranslateToFuzzReader { Name funcrefTableName; + std::unordered_map logImportNames; + + Name throwImportName; + std::unordered_map> globalsByType; std::unordered_map> mutableGlobalsByType; std::unordered_map> immutableGlobalsByType; @@ -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 diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index d3f52d3b0ec..b7d075dccfb 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -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(); @@ -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(); + 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(); + 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: @@ -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* TranslateToFuzzReader::addFunction() { LOGGING_PERCENT = upToSquared(100); - auto* func = new Function; + auto allocation = std::make_unique(); + auto* func = allocation.get(); func->name = Names::getValidFunctionName(wasm, "func"); FunctionCreationContext context(*this, func); assert(funcContext->typeLocals.empty()); @@ -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 @@ -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()); } @@ -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)); } } }; @@ -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(); } @@ -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) diff --git a/test/lit/exec/fuzzing-api.wast b/test/lit/exec/fuzzing-api.wast new file mode 100644 index 00000000000..d37d7ef4a35 --- /dev/null +++ b/test/lit/exec/fuzzing-api.wast @@ -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 diff --git a/test/lit/exec/host-limit.wast b/test/lit/exec/host-limit.wast index e64a47d8a3f..ba5087f6942 100644 --- a/test/lit/exec/host-limit.wast +++ b/test/lit/exec/host-limit.wast @@ -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) diff --git a/test/passes/fuzz_metrics_noprint.bin.txt b/test/passes/fuzz_metrics_noprint.bin.txt index 2f1719633b8..fab2ccc9d63 100644 --- a/test/passes/fuzz_metrics_noprint.bin.txt +++ b/test/passes/fuzz_metrics_noprint.bin.txt @@ -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 diff --git a/test/passes/simplify-locals_all-features.txt b/test/passes/simplify-locals_all-features.txt index befbe33efb7..9daee90a264 100644 --- a/test/passes/simplify-locals_all-features.txt +++ b/test/passes/simplify-locals_all-features.txt @@ -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) diff --git a/test/passes/simplify-locals_all-features.wast b/test/passes/simplify-locals_all-features.wast index 47bb7f1b588..0c296e378b4 100644 --- a/test/passes/simplify-locals_all-features.wast +++ b/test/passes/simplify-locals_all-features.wast @@ -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 diff --git a/test/passes/simplify-locals_all-features_disable-exception-handling.txt b/test/passes/simplify-locals_all-features_disable-exception-handling.txt index 111223c49c5..92356286763 100644 --- a/test/passes/simplify-locals_all-features_disable-exception-handling.txt +++ b/test/passes/simplify-locals_all-features_disable-exception-handling.txt @@ -1214,9 +1214,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) diff --git a/test/passes/simplify-locals_all-features_disable-exception-handling.wast b/test/passes/simplify-locals_all-features_disable-exception-handling.wast index 0fc11069693..44c31e344b3 100644 --- a/test/passes/simplify-locals_all-features_disable-exception-handling.wast +++ b/test/passes/simplify-locals_all-features_disable-exception-handling.wast @@ -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 diff --git a/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt b/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt index 3cc84161097..c17c9cd1eea 100644 --- a/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt +++ b/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt @@ -1,51 +1,58 @@ Metrics total - [exports] : 4 - [funcs] : 7 + [exports] : 3 + [funcs] : 4 [globals] : 26 - [imports] : 5 + [imports] : 6 [memories] : 1 [memory-data] : 20 - [table-data] : 2 + [table-data] : 0 [tables] : 1 [tags] : 2 - [total] : 510 - [vars] : 15 - ArrayNew : 14 - ArrayNewFixed : 2 - AtomicFence : 1 - Binary : 64 - Block : 45 - Break : 2 - Call : 8 + [total] : 665 + [vars] : 20 + ArrayGet : 2 + ArrayLen : 2 + ArrayNew : 15 + ArrayNewFixed : 3 + AtomicCmpxchg : 1 + AtomicRMW : 1 + Binary : 71 + Block : 63 + BrOn : 4 + Break : 7 + Call : 18 CallRef : 2 - Const : 127 - Drop : 5 - GlobalGet : 28 + Const : 142 + Drop : 10 + GlobalGet : 33 GlobalSet : 16 - If : 12 - Load : 18 - LocalGet : 46 - LocalSet : 29 - Loop : 3 - MemoryCopy : 1 + I31Get : 1 + If : 21 + Load : 20 + LocalGet : 61 + LocalSet : 52 + Loop : 6 + MemoryFill : 1 MemoryInit : 1 - Nop : 4 - Pop : 2 - RefCast : 1 - RefFunc : 7 - RefNull : 6 - Return : 9 + Nop : 7 + Pop : 1 + RefAs : 11 + RefEq : 1 + RefFunc : 3 + RefI31 : 1 + RefNull : 14 + Return : 5 SIMDExtract : 1 - Store : 1 - StringConst : 7 - StringEq : 1 - StringMeasure : 1 - StringSliceWTF : 1 - StructNew : 14 - Try : 2 + Select : 2 + StringConst : 5 + StringEncode : 1 + StructGet : 4 + StructNew : 16 + StructSet : 1 + Try : 1 TryTable : 1 - TupleExtract : 3 + TupleExtract : 2 TupleMake : 6 - Unary : 9 - Unreachable : 10 + Unary : 20 + Unreachable : 9