diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h index 85d05830910..c2e533f0567 100644 --- a/src/tools/fuzzing.h +++ b/src/tools/fuzzing.h @@ -119,10 +119,12 @@ class TranslateToFuzzReader { static constexpr size_t Important = 2; public: - TranslateToFuzzReader(Module& wasm, + TranslateToFuzzReader(FuzzParams fuzzParams_, + Module& wasm, std::vector&& input, WorldMode worldMode = WorldMode::Open); - TranslateToFuzzReader(Module& wasm, + TranslateToFuzzReader(FuzzParams fuzzParams_, + Module& wasm, std::string& filename, WorldMode worldMode = WorldMode::Open); @@ -138,6 +140,8 @@ class TranslateToFuzzReader { void build(); + FuzzParams fuzzParams; + Module& wasm; private: @@ -276,27 +280,6 @@ class TranslateToFuzzReader { FunctionCreationContext* funcContext = nullptr; - // The fuzzing parameters we use. This may change from function to function or - // even in a more refined manner, so we use an RAII context to manage it. - struct FuzzParamsContext : public FuzzParams { - TranslateToFuzzReader& parent; - - FuzzParamsContext* old; - - FuzzParamsContext(TranslateToFuzzReader& parent) - : parent(parent), old(parent.fuzzParams) { - parent.fuzzParams = this; - } - - ~FuzzParamsContext() { parent.fuzzParams = old; } - }; - - FuzzParamsContext* fuzzParams = nullptr; - - // The default global context we use throughout the process (unless it is - // overridden using another context in an RAII manner). - std::unique_ptr globalParams; - const std::vector atomicMemoryOrders; public: diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index e6cbafe2ba3..3c3f1438cee 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -102,21 +102,18 @@ std::vector getMemoryOrders(const FeatureSet& features) { } // namespace -TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, +TranslateToFuzzReader::TranslateToFuzzReader(FuzzParams fuzzParams_, + Module& wasm, std::vector&& input, WorldMode worldMode) - : wasm(wasm), worldMode(worldMode), builder(wasm), + : fuzzParams(fuzzParams_), wasm(wasm), worldMode(worldMode), builder(wasm), random(std::move(input), wasm.features), intrinsics(wasm), loggableTypes(getLoggableTypes(wasm.features)), atomicMemoryOrders(getMemoryOrders(wasm.features)), - publicTypeValidator(wasm.features) { haveInitialFunctions = !wasm.functions.empty(); - // Setup params. Start with the defaults. - globalParams = std::make_unique(*this); - // Some of the time, adjust parameters based on the size, e.g. allowing more // heap types in larger inputs, etc. if (random.oneIn(2)) { @@ -133,28 +130,28 @@ TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, auto bits = random.get(); if (bits & 1) { - fuzzParams->MAX_NEW_GC_TYPES *= ratio; + fuzzParams.MAX_NEW_GC_TYPES *= ratio; } if (bits & 2) { - fuzzParams->MAX_GLOBALS *= ratio; + fuzzParams.MAX_GLOBALS *= ratio; } if (bits & 4) { // Only adjust the limit if there is one. - if (fuzzParams->HANG_LIMIT) { - fuzzParams->HANG_LIMIT *= ratio; + if (fuzzParams.HANG_LIMIT) { + fuzzParams.HANG_LIMIT *= ratio; // There is a limit, so keep it non-zero to actually prevent hangs. - fuzzParams->HANG_LIMIT = std::max(fuzzParams->HANG_LIMIT, 1); + fuzzParams.HANG_LIMIT = std::max(fuzzParams.HANG_LIMIT, 1); } } if (bits & 8) { // Only increase the number of tries. Trying fewer times does not help // find more interesting patterns. if (ratio > 1) { - fuzzParams->TRIES *= ratio; + fuzzParams.TRIES *= ratio; } } if (bits & 16) { - fuzzParams->MAX_ARRAY_SIZE *= ratio; + fuzzParams.MAX_ARRAY_SIZE *= ratio; } } @@ -163,11 +160,14 @@ TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, allowAddingUnreachableCode = oneIn(2); } -TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, +TranslateToFuzzReader::TranslateToFuzzReader(FuzzParams fuzzParams_, + Module& wasm, std::string& filename, WorldMode worldMode) - : TranslateToFuzzReader( - wasm, read_file>(filename, Flags::Binary), worldMode) {} + : TranslateToFuzzReader(fuzzParams_, + wasm, + read_file>(filename, Flags::Binary), + worldMode) {} void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { // Pick random passes to further shape the wasm. This is similar to how we @@ -413,7 +413,7 @@ void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { } void TranslateToFuzzReader::build() { - if (fuzzParams->HANG_LIMIT > 0) { + if (fuzzParams.HANG_LIMIT > 0) { prepareHangLimitSupport(); } if (allowMemory) { @@ -440,7 +440,7 @@ void TranslateToFuzzReader::build() { useImportedFunctions(); processFunctions(); - if (fuzzParams->HANG_LIMIT > 0) { + if (fuzzParams.HANG_LIMIT > 0) { addHangLimitSupport(); } if (allowMemory) { @@ -495,7 +495,7 @@ void TranslateToFuzzReader::setupMemory() { segment->setName(Names::getValidDataSegmentName(wasm, Name::fromInt(i)), false); bool isPassive = bool(upTo(2)); - size_t segSize = upTo(fuzzParams->USABLE_MEMORY * 2); + size_t segSize = upTo(fuzzParams.USABLE_MEMORY * 2); segment->data.resize(segSize); for (size_t j = 0; j < segSize; j++) { segment->data[j] = upTo(512); @@ -517,7 +517,7 @@ void TranslateToFuzzReader::setupMemory() { builder.makeConst(Literal::makeFromInt32(0, memory->addressType)); segment->setName(Names::getValidDataSegmentName(wasm, Name::fromInt(0)), false); - auto num = upTo(fuzzParams->USABLE_MEMORY * 2); + auto num = upTo(fuzzParams.USABLE_MEMORY * 2); for (size_t i = 0; i < num; i++) { auto value = upTo(512); segment->data.push_back(value >= 256 ? 0 : (value & 0xff)); @@ -550,7 +550,7 @@ void TranslateToFuzzReader::setupHeapTypes() { // For GC, also generate random types. if (wasm.features.hasGC()) { auto generator = HeapTypeGenerator::create( - random, wasm.features, upTo(fuzzParams->MAX_NEW_GC_TYPES)); + random, wasm.features, upTo(fuzzParams.MAX_NEW_GC_TYPES)); auto result = generator.builder.build(); if (auto* err = result.getError()) { Fatal() << "Failed to build heap types: " << err->reason << " at index " @@ -803,7 +803,7 @@ void TranslateToFuzzReader::setupGlobals() { } // Create new random globals. - for (size_t index = upTo(fuzzParams->MAX_GLOBALS); index > 0; --index) { + for (size_t index = upTo(fuzzParams.MAX_GLOBALS); index > 0; --index) { // Prefer immutable globals as they can be used in global.gets in other // globals for more interesting patterns. auto mutability = oneIn(3) ? Builder::Mutable : Builder::Immutable; @@ -950,7 +950,7 @@ void TranslateToFuzzReader::finalizeMemory() { (1 << memory->pageSizeLog2))); } } - memory->initial = std::max(memory->initial, fuzzParams->USABLE_MEMORY); + memory->initial = std::max(memory->initial, fuzzParams.USABLE_MEMORY); // Avoid an unlimited memory size, which would make fuzzing very difficult // as different VMs will run out of system memory in different ways. Also use // the initial memory size as the maximum, if the initial is now larger @@ -1083,7 +1083,7 @@ void TranslateToFuzzReader::addHangLimitSupport() { auto glob = builder.makeGlobal(HANG_LIMIT_GLOBAL, Type::i32, - builder.makeConst(int32_t(fuzzParams->HANG_LIMIT)), + builder.makeConst(int32_t(fuzzParams.HANG_LIMIT)), Builder::Mutable); wasm.addGlobal(std::move(glob)); } @@ -1302,7 +1302,7 @@ void TranslateToFuzzReader::addHashMemorySupport() { contents.push_back( builder.makeLocalSet(0, builder.makeConst(uint32_t(5381)))); auto zero = Literal::makeFromInt32(0, wasm.memories[0]->addressType); - for (Index i = 0; i < fuzzParams->USABLE_MEMORY; i++) { + for (Index i = 0; i < fuzzParams.USABLE_MEMORY; i++) { contents.push_back(builder.makeLocalSet( 0, builder.makeBinary( @@ -1484,10 +1484,10 @@ Expression* TranslateToFuzzReader::makeHangLimitCheck() { builder.makeIf( builder.makeUnary(UnaryOp::EqZInt32, builder.makeGlobalGet(HANG_LIMIT_GLOBAL, Type::i32)), - builder.makeSequence(builder.makeGlobalSet(HANG_LIMIT_GLOBAL, - builder.makeConst(int32_t( - fuzzParams->HANG_LIMIT))), - builder.makeUnreachable())), + builder.makeSequence( + builder.makeGlobalSet( + HANG_LIMIT_GLOBAL, builder.makeConst(int32_t(fuzzParams.HANG_LIMIT))), + builder.makeUnreachable())), builder.makeGlobalSet( HANG_LIMIT_GLOBAL, builder.makeBinary(BinaryOp::SubInt32, @@ -1747,7 +1747,7 @@ void TranslateToFuzzReader::processFunctions() { } // At the very end, add hang limit checks (so no modding can override them). - if (fuzzParams->HANG_LIMIT > 0) { + if (fuzzParams.HANG_LIMIT > 0) { for (auto& func : wasm.functions) { if (!func->imported()) { addHangLimitChecks(func.get()); @@ -1778,14 +1778,14 @@ Function* TranslateToFuzzReader::addFunction() { auto& funcTypes = interestingHeapSubTypes[HeapTypes::func]; if (!funcTypes.empty() && oneIn(2)) { auto type = pick(funcTypes); - if (type.getSignature().params.size() < (size_t)fuzzParams->MAX_PARAMS) { + if (type.getSignature().params.size() < (size_t)fuzzParams.MAX_PARAMS) { // This is suitable for us. funcType = type; } } if (!funcType) { // Generate a new type on the fly. - Index numParams = upToSquared(fuzzParams->MAX_PARAMS); + Index numParams = upToSquared(fuzzParams.MAX_PARAMS); std::vector params; params.reserve(numParams); for (Index i = 0; i < numParams; i++) { @@ -1798,7 +1798,7 @@ Function* TranslateToFuzzReader::addFunction() { } func->type = Type(*funcType, NonNullable, Exact); - Index numVars = upToSquared(fuzzParams->MAX_VARS); + Index numVars = upToSquared(fuzzParams.MAX_VARS); for (Index i = 0; i < numVars; i++) { func->vars.push_back(getConcreteType()); } @@ -2808,8 +2808,8 @@ Expression* TranslateToFuzzReader::make(Type type) { } // When we should stop, emit something small (but not necessarily trivial). if (random.finished() || - nesting >= 5 * fuzzParams->NESTING_LIMIT || // hard limit - (nesting >= fuzzParams->NESTING_LIMIT && !oneIn(3))) { + nesting >= 5 * fuzzParams.NESTING_LIMIT || // hard limit + (nesting >= fuzzParams.NESTING_LIMIT && !oneIn(3))) { if (type.isConcrete()) { if (!funcContext || oneIn(2)) { return makeConst(type); @@ -3095,11 +3095,11 @@ Expression* TranslateToFuzzReader::makeBlock(Type type) { ret->type = type; // so we have it during child creation ret->name = makeLabel(); funcContext->breakableStack.push_back(ret); - Index num = upToSquared(fuzzParams->BLOCK_FACTOR - 1); // we add another later - if (nesting >= fuzzParams->NESTING_LIMIT / 2) { + Index num = upToSquared(fuzzParams.BLOCK_FACTOR - 1); // we add another later + if (nesting >= fuzzParams.NESTING_LIMIT / 2) { // smaller blocks past the limit num /= 2; - if (nesting >= fuzzParams->NESTING_LIMIT && oneIn(2)) { + if (nesting >= fuzzParams.NESTING_LIMIT && oneIn(2)) { // smaller blocks past the limit num /= 2; } @@ -3170,7 +3170,7 @@ Expression* TranslateToFuzzReader::makeCondition() { Expression* TranslateToFuzzReader::makeMaybeBlock(Type type) { // if past the limit, prefer not to emit blocks - if (nesting >= fuzzParams->NESTING_LIMIT || oneIn(3)) { + if (nesting >= fuzzParams.NESTING_LIMIT || oneIn(3)) { return make(type); } else { return makeBlock(type); @@ -3217,7 +3217,7 @@ Expression* TranslateToFuzzReader::makeTry(Type type) { auto* body = make(type); std::vector catchTags; std::vector catchBodies; - auto numTags = upTo(fuzzParams->MAX_TRY_CATCHES); + auto numTags = upTo(fuzzParams.MAX_TRY_CATCHES); std::unordered_set usedTags; for (Index i = 0; i < numTags; i++) { if (exceptionTags.empty()) { @@ -3279,7 +3279,7 @@ Expression* TranslateToFuzzReader::makeTryTable(Type type) { std::vector catchTags; std::vector catchDests; std::vector catchRefs; - auto numCatches = upTo(fuzzParams->MAX_TRY_CATCHES); + auto numCatches = upTo(fuzzParams.MAX_TRY_CATCHES); for (Index i = 0; i <= numCatches; i++) { Name tagName; Type tagType; @@ -3304,7 +3304,7 @@ Expression* TranslateToFuzzReader::makeTryTable(Type type) { // also accept a target that is nullable. vec.push_back(Type(HeapType::exn, NonNullable)); auto tagTypeWithExn = Type(vec); - int tries = fuzzParams->TRIES; + int tries = fuzzParams.TRIES; while (tries-- > 0) { auto* target = pick(funcContext->breakableStack); auto dest = getTargetName(target); @@ -3336,7 +3336,7 @@ Expression* TranslateToFuzzReader::makeBreak(Type type) { condition = makeCondition(); } // we need to find a proper target to break to; try a few times - int tries = fuzzParams->TRIES; + int tries = fuzzParams.TRIES; while (tries-- > 0) { auto* target = pick(funcContext->breakableStack); auto name = getTargetName(target); @@ -3410,7 +3410,7 @@ Expression* TranslateToFuzzReader::makeBreak(Type type) { } Expression* TranslateToFuzzReader::makeCall(Type type) { - int tries = fuzzParams->TRIES; + int tries = fuzzParams.TRIES; bool isReturn; while (tries-- > 0) { Function* target = funcContext->func; @@ -3484,9 +3484,9 @@ Expression* TranslateToFuzzReader::makeCallRef(Type type) { // look for a call target with the right type Function* target; bool isReturn; - decltype(fuzzParams->TRIES) i = 0; + decltype(fuzzParams.TRIES) i = 0; while (1) { - if (i == fuzzParams->TRIES || wasm.functions.empty()) { + if (i == fuzzParams.TRIES || wasm.functions.empty()) { // We can't find a proper target, give up. return makeTrivial(type); } @@ -3688,12 +3688,12 @@ Expression* TranslateToFuzzReader::makePointer() { ret = builder.makeBinary( AndInt64, ret, - builder.makeConst(int64_t(fuzzParams->USABLE_MEMORY - 1))); + builder.makeConst(int64_t(fuzzParams.USABLE_MEMORY - 1))); } else { ret = builder.makeBinary( AndInt32, ret, - builder.makeConst(int32_t(fuzzParams->USABLE_MEMORY - 1))); + builder.makeConst(int32_t(fuzzParams.USABLE_MEMORY - 1))); } } return ret; @@ -4480,7 +4480,7 @@ Expression* TranslateToFuzzReader::makeCompoundRef(Type type) { // will only stop here when we exceed the nesting and reach a nullable one. // (This assumes there is a nullable one, that is, that the types are // inhabitable.) - const auto LIMIT = fuzzParams->NESTING_LIMIT + 1; + const auto LIMIT = fuzzParams.NESTING_LIMIT + 1; AutoNester nester(*this); if (type.isNullable() && (random.finished() || nesting >= LIMIT || oneIn(LIMIT - nesting + 1))) { @@ -4552,8 +4552,7 @@ Expression* TranslateToFuzzReader::makeCompoundRef(Type type) { if (!element.type.isDefaultable() || oneIn(2)) { init = makeChild(element.type); } - auto* count = - builder.makeConst(int32_t(upTo(fuzzParams->MAX_ARRAY_SIZE))); + auto* count = builder.makeConst(int32_t(upTo(fuzzParams.MAX_ARRAY_SIZE))); return builder.makeArrayNew(type.getHeapType(), count, init); } case HeapTypeKind::Cont: { @@ -5251,7 +5250,7 @@ Expression* TranslateToFuzzReader::makeSwitch(Type type) { return make(type); } // we need to find proper targets to break to; try a bunch - int tries = fuzzParams->TRIES; + int tries = fuzzParams.TRIES; std::vector names; Type valueType = Type::unreachable; while (tries-- > 0) { @@ -5806,7 +5805,7 @@ Expression* TranslateToFuzzReader::makeBrOn(Type type) { // to, we can then either drop ourselves or wrap ourselves in a block + // another value, so that we return the proper thing here (which is done below // in fixFlowingType). - int tries = fuzzParams->TRIES; + int tries = fuzzParams.TRIES; Name targetName; Type targetType; while (--tries >= 0) { @@ -6570,7 +6569,7 @@ Type TranslateToFuzzReader::getReferenceType() { } Type TranslateToFuzzReader::getCastableReferenceType() { - int tries = fuzzParams->TRIES; + int tries = fuzzParams.TRIES; while (tries-- > 0) { auto type = getReferenceType(); if (type.isCastable()) { @@ -6622,7 +6621,7 @@ Type TranslateToFuzzReader::getTupleType() { } std::vector elements; - size_t numElements = 2 + upTo(fuzzParams->MAX_TUPLE_SIZE - 2); + size_t numElements = 2 + upTo(fuzzParams.MAX_TUPLE_SIZE - 2); for (size_t i = 0; i < numElements; ++i) { auto type = getSingleConcreteType(); // Don't add a non-defaultable type into a tuple, as currently we can't diff --git a/src/tools/wasm-fuzz-lattices.cpp b/src/tools/wasm-fuzz-lattices.cpp index d1409578609..5e6dd763a54 100644 --- a/src/tools/wasm-fuzz-lattices.cpp +++ b/src/tools/wasm-fuzz-lattices.cpp @@ -1036,7 +1036,7 @@ struct Fuzzer { } Module testModule; - TranslateToFuzzReader reader(testModule, std::move(bytes)); + TranslateToFuzzReader reader(FuzzParams(), testModule, std::move(bytes)); reader.build(); if (verbose) { diff --git a/src/tools/wasm-opt.cpp b/src/tools/wasm-opt.cpp index 8cf7e6ef6e1..9903ba7f1e7 100644 --- a/src/tools/wasm-opt.cpp +++ b/src/tools/wasm-opt.cpp @@ -88,6 +88,7 @@ int main(int argc, const char* argv[]) { bool fuzzPreserveImportsAndExports = false; bool fuzzAgainstJS = false; bool fuzzNoInvokes = false; + FuzzParams fuzzParams; std::string fuzzImport; std::string emitSpecWrapper; std::string emitWasm2CWrapper; @@ -227,6 +228,16 @@ For more on how to optimize effectively, see WasmOptOption, Options::Arguments::Zero, [&](Options* o, const std::string& arguments) { fuzzNoInvokes = true; }) + .add( + "--fuzz-hang-limit", + "", + "the number of runtime iterations (function calls, loop backbranches) we " + "allow before we trap to prevent a hangs (0 means no hang protection)", + WasmOptOption, + Options::Arguments::One, + [&](Options* o, const std::string& arguments) { + fuzzParams.HANG_LIMIT = std::stoi(arguments); + }) .add( "--fuzz-import", "", @@ -361,7 +372,7 @@ For more on how to optimize effectively, see } if (translateToFuzz) { TranslateToFuzzReader reader( - wasm, options.extra["infile"], options.passOptions.worldMode); + fuzzParams, wasm, options.extra["infile"], options.passOptions.worldMode); reader.setAllowMemory(fuzzMemory); reader.setAllowOOB(fuzzOOB); reader.setPreserveImportsAndExports(fuzzPreserveImportsAndExports); diff --git a/test/lit/fuzz-hang-limit.wast b/test/lit/fuzz-hang-limit.wast new file mode 100644 index 00000000000..2c38c84eb7f --- /dev/null +++ b/test/lit/fuzz-hang-limit.wast @@ -0,0 +1,12 @@ +;; Test the hang-limit flag. + +;; By default we emit hang limit checks, and there is no change when we set +;; the limit to 100. +;; RUN: wasm-opt %s.dat -all -ttf -S -o - | filecheck %s --check-prefix=NORMAL +;; RUN: wasm-opt %s.dat -all -ttf --fuzz-hang-limit=100 -S -o - | filecheck %s --check-prefix=NORMAL +;; NORMAL: hangLimit + +;; But if we set the limit to 0, that disables hang limit checks. +;; RUN: wasm-opt %s.dat -all -ttf --fuzz-hang-limit=0 -S -o - | filecheck %s --check-prefix=FLAG +;; FLAG-NOT: hangLimit + diff --git a/test/lit/fuzz-hang-limit.wast.dat b/test/lit/fuzz-hang-limit.wast.dat new file mode 100644 index 00000000000..172f50c7db9 Binary files /dev/null and b/test/lit/fuzz-hang-limit.wast.dat differ diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 385f57657e1..387d47d4a04 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -79,6 +79,12 @@ ;; CHECK-NEXT: --fuzz-no-invokes do not emit invoker functions in ;; CHECK-NEXT: the fuzzer ;; CHECK-EMPTY: +;; CHECK-NEXT: --fuzz-hang-limit the number of runtime iterations +;; CHECK-NEXT: (function calls, loop +;; CHECK-NEXT: backbranches) we allow before we +;; CHECK-NEXT: trap to prevent a hangs (0 means +;; CHECK-NEXT: no hang protection) +;; CHECK-EMPTY: ;; CHECK-NEXT: --fuzz-import a module to use as an import in ;; CHECK-NEXT: -ttf mode ;; CHECK-EMPTY: