| /* |
| * Copyright 2021 WebAssembly Community Group participants |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "tools/fuzzing.h" |
| #include "ir/gc-type-utils.h" |
| #include "ir/glbs.h" |
| #include "ir/iteration.h" |
| #include "ir/local-structural-dominance.h" |
| #include "ir/module-utils.h" |
| #include "ir/subtype-exprs.h" |
| #include "ir/subtypes.h" |
| #include "ir/type-updating.h" |
| #include "support/string.h" |
| #include "tools/fuzzing/heap-types.h" |
| #include "wasm-io.h" |
| |
| namespace wasm { |
| |
| TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, |
| std::vector<char>&& input, |
| bool closedWorld) |
| : wasm(wasm), closedWorld(closedWorld), builder(wasm), |
| random(std::move(input), wasm.features), |
| publicTypeValidator(wasm.features) { |
| |
| haveInitialFunctions = !wasm.functions.empty(); |
| |
| // - funcref cannot be logged because referenced functions can be inlined or |
| // removed during optimization |
| // - there's no point in logging anyref because it is opaque |
| // - don't bother logging tuples |
| loggableTypes = {Type::i32, Type::i64, Type::f32, Type::f64}; |
| if (wasm.features.hasSIMD()) { |
| loggableTypes.push_back(Type::v128); |
| } |
| |
| // Setup params. Start with the defaults. |
| globalParams = std::make_unique<FuzzParamsContext>(*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)) { |
| // A typical random wasm input from fuzz_opt.py is fairly large (to minimize |
| // the process creation overhead of all the things we run from python), and |
| // the defaults are tuned to that. This corresponds to INPUT_SIZE_MEAN in |
| // scripts/fuzz_opt.py |
| const double MEAN_SIZE = 40 * 1024; |
| |
| // As we have not read anything from the input, the remaining size is its |
| // size. |
| double size = random.remaining(); |
| auto ratio = size / MEAN_SIZE; |
| |
| auto bits = random.get(); |
| if (bits & 1) { |
| fuzzParams->MAX_NEW_GC_TYPES *= ratio; |
| } |
| if (bits & 2) { |
| fuzzParams->MAX_GLOBALS *= ratio; |
| } |
| if (bits & 4) { |
| // Only adjust the limit if there is one. |
| 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); |
| } |
| } |
| 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; |
| } |
| } |
| if (bits & 16) { |
| fuzzParams->MAX_ARRAY_SIZE *= ratio; |
| } |
| } |
| |
| // Half the time add no unreachable code so that we'll execute the most code |
| // as possible with no early exits. |
| allowAddingUnreachableCode = oneIn(2); |
| } |
| |
| TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, |
| std::string& filename, |
| bool closedWorld) |
| : TranslateToFuzzReader(wasm, |
| read_file<std::vector<char>>(filename, Flags::Binary), |
| closedWorld) {} |
| |
| void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { |
| // Pick random passes to further shape the wasm. This is similar to how we |
| // pick random passes in fuzz_opt.py, but the goal there is to find problems |
| // in the passes, while the goal here is more to shape the wasm, so that |
| // translate-to-fuzz emits interesting outputs (the latter is important for |
| // things like ClusterFuzz, where we are using Binaryen to fuzz other things |
| // than itself). As a result, the list of passes here is different from |
| // fuzz_opt.py. |
| |
| // Enclose the world, some of the time. We do this before picking any other |
| // passes so that we make the initial fuzz contents more optimizable by |
| // closed-world passes later. Note that we do this regardless of whether we |
| // are in closed-world mode or not, as it is good to get this variety |
| // regardless. |
| if (oneIn(2)) { |
| options.passes.push_back("enclose-world"); |
| } |
| |
| // Main selection of passes. |
| while (options.passes.size() < 20 && !random.finished() && !oneIn(3)) { |
| switch (upTo(42)) { |
| case 0: |
| case 1: |
| case 2: |
| case 3: |
| case 4: { |
| options.passOptions.optimizeLevel = upTo(4); |
| options.passOptions.shrinkLevel = upTo(3); |
| options.addDefaultOptPasses(); |
| break; |
| } |
| case 5: |
| options.passes.push_back("coalesce-locals"); |
| break; |
| case 6: |
| options.passes.push_back("code-pushing"); |
| break; |
| case 7: |
| options.passes.push_back("code-folding"); |
| break; |
| case 8: |
| options.passes.push_back("dce"); |
| break; |
| case 9: |
| options.passes.push_back("duplicate-function-elimination"); |
| break; |
| case 10: |
| // Some features do not support flatten yet. |
| if (!wasm.features.hasReferenceTypes() && |
| !wasm.features.hasExceptionHandling() && !wasm.features.hasGC()) { |
| options.passes.push_back("flatten"); |
| if (oneIn(2)) { |
| options.passes.push_back("rereloop"); |
| } |
| } |
| break; |
| case 11: |
| options.passes.push_back("inlining"); |
| break; |
| case 12: |
| options.passes.push_back("inlining-optimizing"); |
| break; |
| case 13: |
| options.passes.push_back("local-cse"); |
| break; |
| case 14: |
| options.passes.push_back("memory-packing"); |
| break; |
| case 15: |
| options.passes.push_back("merge-blocks"); |
| break; |
| case 16: |
| options.passes.push_back("optimize-instructions"); |
| break; |
| case 17: |
| options.passes.push_back("pick-load-signs"); |
| break; |
| case 18: |
| options.passes.push_back("precompute"); |
| break; |
| case 19: |
| options.passes.push_back("precompute-propagate"); |
| break; |
| case 20: |
| options.passes.push_back("remove-unused-brs"); |
| break; |
| case 21: |
| options.passes.push_back("remove-unused-module-elements"); |
| break; |
| case 22: |
| options.passes.push_back("remove-unused-names"); |
| break; |
| case 23: |
| options.passes.push_back("reorder-functions"); |
| break; |
| case 24: |
| options.passes.push_back("reorder-locals"); |
| break; |
| case 25: |
| options.passes.push_back("directize"); |
| break; |
| case 26: |
| options.passes.push_back("simplify-locals"); |
| break; |
| case 27: |
| options.passes.push_back("simplify-locals-notee"); |
| break; |
| case 28: |
| options.passes.push_back("simplify-locals-nostructure"); |
| break; |
| case 29: |
| options.passes.push_back("simplify-locals-notee-nostructure"); |
| break; |
| case 30: |
| options.passes.push_back("ssa"); |
| break; |
| case 31: |
| options.passes.push_back("vacuum"); |
| break; |
| case 32: |
| options.passes.push_back("merge-locals"); |
| break; |
| case 33: |
| options.passes.push_back("licm"); |
| break; |
| case 34: |
| options.passes.push_back("tuple-optimization"); |
| break; |
| case 35: |
| options.passes.push_back("rse"); |
| break; |
| case 36: |
| options.passes.push_back("monomorphize"); |
| break; |
| case 37: |
| options.passes.push_back("monomorphize-always"); |
| break; |
| case 38: |
| case 39: |
| case 40: |
| case 41: |
| // GC specific passes. |
| if (wasm.features.hasGC()) { |
| // Most of these depend on closed world, so just set that. Set it both |
| // on the global pass options, and in the internal state of this |
| // TranslateToFuzzReader instance. |
| options.passOptions.closedWorld = true; |
| closedWorld = true; |
| |
| switch (upTo(16)) { |
| case 0: |
| options.passes.push_back("abstract-type-refining"); |
| break; |
| case 1: |
| options.passes.push_back("cfp"); |
| break; |
| case 2: |
| options.passes.push_back("gsi"); |
| break; |
| case 3: |
| options.passes.push_back("gto"); |
| break; |
| case 4: |
| options.passes.push_back("heap2local"); |
| break; |
| case 5: |
| options.passes.push_back("heap-store-optimization"); |
| break; |
| case 6: |
| options.passes.push_back("minimize-rec-groups"); |
| break; |
| case 7: |
| options.passes.push_back("remove-unused-types"); |
| break; |
| case 8: |
| options.passes.push_back("signature-pruning"); |
| break; |
| case 9: |
| options.passes.push_back("signature-refining"); |
| break; |
| case 10: |
| options.passes.push_back("type-finalizing"); |
| break; |
| case 11: |
| options.passes.push_back("type-refining"); |
| break; |
| case 12: |
| options.passes.push_back("type-merging"); |
| break; |
| case 13: |
| options.passes.push_back("type-ssa"); |
| break; |
| case 14: |
| options.passes.push_back("type-unfinalizing"); |
| break; |
| case 15: |
| options.passes.push_back("unsubtyping"); |
| break; |
| default: |
| WASM_UNREACHABLE("unexpected value"); |
| } |
| } |
| break; |
| default: |
| WASM_UNREACHABLE("unexpected value"); |
| } |
| } |
| |
| if (oneIn(2)) { |
| // We randomize these when we pick -O?, but sometimes do so even without, as |
| // they affect some passes. |
| options.passOptions.optimizeLevel = upTo(4); |
| options.passOptions.shrinkLevel = upTo(3); |
| } |
| |
| if (!options.passOptions.closedWorld && oneIn(2)) { |
| options.passOptions.closedWorld = true; |
| } |
| |
| // Prune things that error in JS if we call them (like SIMD), some of the |
| // time. This alters the wasm/JS boundary quite a lot, so testing both forms |
| // is useful. Note that we do not do this if there is an imported module, |
| // because in that case legalization could alter the contract between the two |
| // (that is, if the first module has an i64 param, we must call it like that, |
| // and not as two i32s which we'd get after legalization). |
| if (!importedModule && oneIn(2)) { |
| options.passes.push_back("legalize-and-prune-js-interface"); |
| } |
| |
| // Usually DCE at the very end, to ensure that our binaries validate in other |
| // VMs, due to how non-nullable local validation and unreachable code |
| // interact. See fuzz_opt.py and |
| // https://github.com/WebAssembly/binaryen/pull/5665 |
| // https://github.com/WebAssembly/binaryen/issues/5599 |
| if (wasm.features.hasGC() && !oneIn(10)) { |
| options.passes.push_back("dce"); |
| } |
| |
| // TODO: We could in theory run some function-level passes on particular |
| // functions, but then we'd need to do this after generation, not |
| // before (and random data no longer remains then). |
| } |
| |
| void TranslateToFuzzReader::build() { |
| if (fuzzParams->HANG_LIMIT > 0) { |
| prepareHangLimitSupport(); |
| } |
| if (allowMemory) { |
| setupMemory(); |
| } |
| setupHeapTypes(); |
| setupTables(); |
| setupGlobals(); |
| useImportedGlobals(); |
| if (wasm.features.hasExceptionHandling()) { |
| setupTags(); |
| addImportThrowingSupport(); |
| } |
| if (wasm.features.hasReferenceTypes()) { |
| addImportTableSupport(); |
| } |
| addImportLoggingSupport(); |
| addImportCallingSupport(); |
| addImportSleepSupport(); |
| |
| // First, modify initial functions. That includes removing imports. Then, |
| // use the imported module, which are function imports that we allow. |
| modifyInitialFunctions(); |
| useImportedFunctions(); |
| |
| processFunctions(); |
| if (fuzzParams->HANG_LIMIT > 0) { |
| addHangLimitSupport(); |
| } |
| if (allowMemory) { |
| finalizeMemory(); |
| addHashMemorySupport(); |
| } |
| finalizeTable(); |
| shuffleExports(); |
| |
| // We may turn various function imports into defined functions. Refinalize at |
| // the end to update all references to them, which may become exact. |
| PassRunner runner(&wasm); |
| ReFinalize().run(&runner, &wasm); |
| ReFinalize().walkModuleCode(&wasm); |
| } |
| |
| void TranslateToFuzzReader::setupMemory() { |
| // Add a memory, if one does not already exist. |
| if (wasm.memories.empty()) { |
| auto memory = Builder::makeMemory("0"); |
| // Add at least one page of memory. |
| memory->initial = 1 + upTo(10); |
| // Make the max potentially higher, or unlimited. |
| if (oneIn(2)) { |
| memory->max = memory->initial + upTo(4); |
| } else { |
| memory->max = Memory::kUnlimitedSize; |
| } |
| // Fuzz wasm64 when possible, sometimes. |
| if (wasm.features.hasMemory64() && oneIn(2)) { |
| memory->addressType = Type::i64; |
| } |
| wasm.addMemory(std::move(memory)); |
| } |
| |
| auto& memory = wasm.memories[0]; |
| if (wasm.features.hasBulkMemory()) { |
| size_t numSegments = upTo(8); |
| // need at least one segment for memory.inits |
| if (wasm.dataSegments.empty() && !numSegments) { |
| numSegments = 1; |
| } |
| size_t memCovered = 0; |
| for (size_t i = 0; i < numSegments; i++) { |
| auto segment = builder.makeDataSegment(); |
| segment->setName(Names::getValidDataSegmentName(wasm, Name::fromInt(i)), |
| false); |
| segment->isPassive = bool(upTo(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); |
| } |
| if (!segment->isPassive) { |
| segment->offset = builder.makeConst( |
| Literal::makeFromInt32(memCovered, memory->addressType)); |
| memCovered += segSize; |
| segment->memory = memory->name; |
| } |
| wasm.addDataSegment(std::move(segment)); |
| } |
| } else { |
| // init some data, especially if none exists before |
| if (!oneIn(wasm.dataSegments.empty() ? 10 : 2)) { |
| auto segment = builder.makeDataSegment(); |
| segment->memory = memory->name; |
| segment->offset = |
| builder.makeConst(Literal::makeFromInt32(0, memory->addressType)); |
| segment->setName(Names::getValidDataSegmentName(wasm, Name::fromInt(0)), |
| false); |
| 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)); |
| } |
| wasm.addDataSegment(std::move(segment)); |
| } |
| } |
| } |
| |
| void TranslateToFuzzReader::setupHeapTypes() { |
| // Start with any existing heap types in the module, which may exist in any |
| // initial content we began with. |
| auto possibleHeapTypes = ModuleUtils::collectHeapTypes(wasm); |
| |
| // Use heap types from an imported module, if present. |
| if (importedModule) { |
| auto importedHeapTypes = ModuleUtils::collectHeapTypes(*importedModule); |
| auto rate = upTo(11); |
| for (auto type : importedHeapTypes) { |
| if (upTo(10) < rate) { |
| possibleHeapTypes.push_back(type); |
| } |
| } |
| } |
| |
| // Filter away uninhabitable heap types, that is, heap types that we cannot |
| // construct, like a type with a non-nullable reference to itself. |
| interestingHeapTypes = HeapTypeGenerator::getInhabitable(possibleHeapTypes); |
| |
| // For GC, also generate random types. |
| if (wasm.features.hasGC()) { |
| auto generator = HeapTypeGenerator::create( |
| 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 " |
| << err->index; |
| } |
| |
| // Make the new types inhabitable. This process modifies existing types, so |
| // it leaves more available compared to HeapTypeGenerator::getInhabitable. |
| // We run that before on existing content, which may have instructions that |
| // use the types, as editing them is not trivial, and for new types here we |
| // are free to modify them so we keep as many as we can. |
| auto inhabitable = HeapTypeGenerator::makeInhabitable(*result); |
| for (auto type : inhabitable) { |
| // Trivial types are already handled specifically in e.g. |
| // getSingleConcreteType(), and we avoid adding them here as then we'd |
| // need to add code to avoid uninhabitable combinations of them (like a |
| // non-nullable bottom heap type). |
| if (!type.isBottom() && !type.isBasic()) { |
| interestingHeapTypes.push_back(type); |
| if (oneIn(2)) { |
| // Add a name for this type. |
| wasm.typeNames[type].name = |
| "generated_type$" + std::to_string(interestingHeapTypes.size()); |
| } |
| } |
| } |
| } |
| |
| // Compute subtypes ahead of time. It is more efficient to do this all at once |
| // now, rather than lazily later. |
| SubTypes subTypes(interestingHeapTypes); |
| for (auto type : interestingHeapTypes) { |
| for (auto subType : subTypes.getImmediateSubTypes(type)) { |
| interestingHeapSubTypes[type].push_back(subType); |
| } |
| // Basic types must be handled directly, since subTypes doesn't look at |
| // those. |
| auto share = type.getShared(); |
| auto struct_ = HeapTypes::struct_.getBasic(share); |
| auto array = HeapTypes::array.getBasic(share); |
| auto eq = HeapTypes::eq.getBasic(share); |
| auto any = HeapTypes::any.getBasic(share); |
| auto func = HeapTypes::func.getBasic(share); |
| auto cont = HeapTypes::cont.getBasic(share); |
| switch (type.getKind()) { |
| case HeapTypeKind::Func: |
| interestingHeapSubTypes[func].push_back(type); |
| break; |
| case HeapTypeKind::Struct: { |
| interestingHeapSubTypes[struct_].push_back(type); |
| interestingHeapSubTypes[eq].push_back(type); |
| interestingHeapSubTypes[any].push_back(type); |
| // Note the mutable fields. |
| auto& fields = type.getStruct().fields; |
| for (Index i = 0; i < fields.size(); i++) { |
| if (fields[i].mutable_) { |
| mutableStructFields.push_back(StructField{type, i}); |
| } |
| } |
| break; |
| } |
| case HeapTypeKind::Array: |
| interestingHeapSubTypes[array].push_back(type); |
| interestingHeapSubTypes[eq].push_back(type); |
| interestingHeapSubTypes[any].push_back(type); |
| if (type.getArray().element.mutable_) { |
| mutableArrays.push_back(type); |
| } |
| break; |
| case HeapTypeKind::Cont: |
| interestingHeapSubTypes[cont].push_back(type); |
| break; |
| case HeapTypeKind::Basic: |
| WASM_UNREACHABLE("unexpected kind"); |
| } |
| } |
| |
| // Compute struct and array fields. |
| for (auto type : interestingHeapTypes) { |
| if (type.isStruct()) { |
| auto& fields = type.getStruct().fields; |
| for (Index i = 0; i < fields.size(); i++) { |
| typeStructFields[fields[i].type].push_back(StructField{type, i}); |
| } |
| } else if (type.isArray()) { |
| typeArrays[type.getArray().element.type].push_back(type); |
| } |
| } |
| } |
| |
| // TODO(reference-types): allow the fuzzer to create multiple tables |
| void TranslateToFuzzReader::setupTables() { |
| // Ensure a funcref element segment and table exist. Segments with more |
| // specific function types may have a smaller chance of getting functions. |
| Table* table = nullptr; |
| Type funcref = Type(HeapType::func, Nullable); |
| auto iter = std::find_if(wasm.tables.begin(), |
| wasm.tables.end(), |
| [&](auto& table) { return table->type == funcref; }); |
| if (iter != wasm.tables.end()) { |
| table = iter->get(); |
| } else { |
| // Start from a potentially empty table. |
| Address initial = upTo(10); |
| // Make the max potentially higher, or unlimited. |
| Address max; |
| if (oneIn(2)) { |
| max = initial + upTo(4); |
| } else { |
| max = Memory::kUnlimitedSize; |
| } |
| // Fuzz wasm64 when possible, sometimes. |
| auto addressType = Type::i32; |
| if (wasm.features.hasMemory64() && oneIn(2)) { |
| addressType = Type::i64; |
| } |
| auto tablePtr = |
| builder.makeTable(Names::getValidTableName(wasm, "fuzzing_table"), |
| funcref, |
| initial, |
| max, |
| addressType); |
| tablePtr->hasExplicitName = true; |
| table = wasm.addTable(std::move(tablePtr)); |
| } |
| funcrefTableName = table->name; |
| bool hasFuncrefElemSegment = |
| std::any_of(wasm.elementSegments.begin(), |
| wasm.elementSegments.end(), |
| [&](auto& segment) { |
| return segment->table.is() && segment->type == funcref; |
| }); |
| auto addressType = wasm.getTable(funcrefTableName)->addressType; |
| if (!hasFuncrefElemSegment) { |
| // TODO: use a random table |
| auto segment = std::make_unique<ElementSegment>( |
| table->name, builder.makeConst(Literal::makeFromInt32(0, addressType))); |
| segment->setName(Names::getValidElementSegmentName(wasm, "elem$"), false); |
| wasm.addElementSegment(std::move(segment)); |
| } |
| |
| // When EH is enabled, set up an exnref table. |
| if (wasm.features.hasExceptionHandling()) { |
| Type exnref = Type(HeapType::exn, Nullable); |
| auto iter = |
| std::find_if(wasm.tables.begin(), wasm.tables.end(), [&](auto& table) { |
| return table->type == exnref; |
| }); |
| if (iter != wasm.tables.end()) { |
| // Use the existing one. |
| exnrefTableName = iter->get()->name; |
| } else { |
| // Create a new exnref table. |
| Address initial = upTo(10); |
| Address max = oneIn(2) ? initial + upTo(4) : Memory::kUnlimitedSize; |
| auto tablePtr = |
| builder.makeTable(Names::getValidTableName(wasm, "exnref_table"), |
| exnref, |
| initial, |
| max, |
| Type::i32); // TODO: wasm64 |
| tablePtr->hasExplicitName = true; |
| table = wasm.addTable(std::move(tablePtr)); |
| exnrefTableName = table->name; |
| } |
| } |
| } |
| |
| void TranslateToFuzzReader::useGlobalLater(Global* global) { |
| auto type = global->type; |
| auto name = global->name; |
| globalsByType[type].push_back(name); |
| if (global->mutable_) { |
| mutableGlobalsByType[type].push_back(name); |
| } else { |
| immutableGlobalsByType[type].push_back(name); |
| if (global->imported()) { |
| importedImmutableGlobalsByType[type].push_back(name); |
| } |
| } |
| } |
| |
| void TranslateToFuzzReader::setupGlobals() { |
| // If there were initial wasm contents, there may be imported globals. That |
| // would be a problem in the fuzzer harness as we'd error if we do not |
| // provide them (and provide the proper type, etc.). |
| // Avoid that, so that all the standard fuzzing infrastructure can always |
| // run the wasm. |
| for (auto& global : wasm.globals) { |
| if (global->imported()) { |
| if (!preserveImportsAndExports) { |
| // Remove import info from imported globals, and give them a simple |
| // initializer. |
| global->module = global->base = Name(); |
| global->init = makeConst(global->type); |
| } |
| } else { |
| // If the initialization referred to an imported global, it no longer can |
| // point to the same global after we make it a non-imported global unless |
| // GC is enabled, since before GC, Wasm only made imported globals |
| // available in constant expressions. |
| if (!wasm.features.hasGC() && |
| !FindAll<GlobalGet>(global->init).list.empty()) { |
| global->init = makeConst(global->type); |
| } |
| } |
| } |
| |
| // Randomly assign some globals from initial content to be ignored for the |
| // fuzzer to use. Such globals will only be used from initial content. This is |
| // important to preserve some real-world patterns, like the "once" pattern in |
| // which a global is used in one function only. (If we randomly emitted gets |
| // and sets of such globals, we'd with very high probability end up breaking |
| // that pattern, and not fuzzing it at all.) |
| if (!wasm.globals.empty()) { |
| unsigned percentUsedInitialGlobals = upTo(100); |
| for (auto& global : wasm.globals) { |
| if (upTo(100) < percentUsedInitialGlobals) { |
| useGlobalLater(global.get()); |
| } |
| } |
| } |
| |
| // Create new random globals. |
| for (size_t index = upTo(fuzzParams->MAX_GLOBALS); index > 0; --index) { |
| auto type = getConcreteType(); |
| |
| // Prefer immutable ones as they can be used in global.gets in other |
| // globals, for more interesting patterns. |
| auto mutability = oneIn(3) ? Builder::Mutable : Builder::Immutable; |
| |
| // We can only make something trivial (like a constant) in a global |
| // initializer. |
| auto* init = makeTrivial(type); |
| |
| if (type.isTuple() && !init->is<TupleMake>()) { |
| // For now we disallow anything but tuple.make at the top level of tuple |
| // globals (see details in wasm-binary.cpp). In the future we may allow |
| // global.get or other things here. |
| init = makeConst(type); |
| assert(init->is<TupleMake>()); |
| } |
| if (!FindAll<RefAs>(init).list.empty() || |
| !FindAll<ContNew>(init).list.empty()) { |
| // When creating this initial value we ended up emitting a RefAs, which |
| // means we had to stop in the middle of an overly-nested struct or array, |
| // which we can break out of using ref.as_non_null of a nullable ref. That |
| // traps in normal code, which is bad enough, but it does not even |
| // validate in a global. Switch to something safe instead. |
| // |
| // Likewise, if we see cont.new, we must switch as well. That can happen |
| // if a nested struct we create has a continuation field, for example. |
| type = getMVPType(); |
| init = makeConst(type); |
| } |
| auto name = Names::getValidGlobalName(wasm, "global$"); |
| auto global = builder.makeGlobal(name, type, init, mutability); |
| useGlobalLater(wasm.addGlobal(std::move(global))); |
| |
| // Export some globals, where we can. |
| if (preserveImportsAndExports || type.isTuple() || |
| !isValidPublicType(type)) { |
| continue; |
| } |
| if (mutability == Builder::Mutable && !wasm.features.hasMutableGlobals()) { |
| continue; |
| } |
| if (oneIn(2)) { |
| auto exportName = Names::getValidExportName(wasm, name); |
| wasm.addExport( |
| Builder::makeExport(exportName, name, ExternalKind::Global)); |
| } |
| } |
| } |
| |
| void TranslateToFuzzReader::setupTags() { |
| // 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() && !preserveImportsAndExports) { |
| tag->module = tag->base = Name(); |
| } |
| if (tag->results() == Type::none) { |
| exceptionTags.push_back(tag.get()); |
| } |
| } |
| |
| // Add some random tags. |
| Index num = upTo(3); |
| for (size_t i = 0; i < num; i++) { |
| addTag(); |
| } |
| |
| // Add the fuzzing support tags manually sometimes. |
| if (!preserveImportsAndExports && oneIn(2) && !random.finished()) { |
| 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)); |
| } |
| } |
| |
| void TranslateToFuzzReader::addTag() { |
| auto tag = builder.makeTag(Names::getValidTagName(wasm, "tag$"), |
| Signature(getControlFlowType(), Type::none)); |
| auto* tagg = wasm.addTag(std::move(tag)); |
| exceptionTags.push_back(tagg); |
| } |
| |
| void TranslateToFuzzReader::finalizeMemory() { |
| auto& memory = wasm.memories[0]; |
| for (auto& segment : wasm.dataSegments) { |
| Address maxOffset = segment->data.size(); |
| if (!segment->isPassive) { |
| if (!wasm.features.hasGC()) { |
| // Using a non-imported global in a segment offset is not valid in wasm |
| // unless GC is enabled. This can occur due to us adding a local |
| // definition to what used to be an imported global in initial contents. |
| // To fix that, replace such invalid offsets with a constant. |
| for ([[maybe_unused]] auto* get : |
| FindAll<GlobalGet>(segment->offset).list) { |
| // No imported globals should remain. |
| assert(!wasm.getGlobal(get->name)->imported()); |
| // TODO: It would be better to avoid segment overlap so that |
| // MemoryPacking can run. |
| segment->offset = |
| builder.makeConst(Literal::makeFromInt32(0, memory->addressType)); |
| } |
| } |
| if (auto* offset = segment->offset->dynCast<Const>()) { |
| maxOffset = maxOffset + offset->value.getInteger(); |
| } |
| } |
| |
| // Ensure the initial memory can fit the segment (so we don't just trap), |
| // but only do so when the segment is at a reasonable offset (to avoid |
| // validation errors on the initial size >= 4GB in wasm32, but also to |
| // avoid OOM errors on trying to allocate too much initial memory, which is |
| // annoying in the fuzzer). |
| Address ONE_GB = 1024 * 1024 * 1024; |
| if (maxOffset <= ONE_GB) { |
| memory->initial = std::max( |
| memory->initial, |
| Address((maxOffset + Memory::kPageSize - 1) / Memory::kPageSize)); |
| } |
| } |
| 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. |
| if (memory->max == Memory::kUnlimitedSize) { |
| memory->max = memory->initial; |
| } |
| if (memory->max <= memory->initial) { |
| // To allow growth to work (which a testcase may assume), try to make the |
| // maximum larger than the initial. |
| // TODO: scan the wasm for grow instructions? |
| memory->max = |
| std::min(Address(memory->initial + 1), Address(Memory::kMaxSize32)); |
| } |
| |
| if (!preserveImportsAndExports) { |
| // Avoid an imported memory (which the fuzz harness would need to handle). |
| for (auto& memory : wasm.memories) { |
| memory->module = memory->base = Name(); |
| } |
| } |
| } |
| |
| void TranslateToFuzzReader::finalizeTable() { |
| for (auto& table : wasm.tables) { |
| ModuleUtils::iterTableSegments( |
| wasm, table->name, [&](ElementSegment* segment) { |
| // If the offset contains a global that was imported (which is ok) but |
| // no longer is (not ok unless GC is enabled), we may need to change |
| // that. |
| if (!wasm.features.hasGC()) { |
| for ([[maybe_unused]] auto* get : |
| FindAll<GlobalGet>(segment->offset).list) { |
| // No imported globals should remain. |
| assert(!wasm.getGlobal(get->name)->imported()); |
| // TODO: the segments must not overlap... |
| segment->offset = |
| builder.makeConst(Literal::makeFromInt32(0, table->addressType)); |
| } |
| } |
| Address maxOffset = segment->data.size(); |
| if (auto* offset = segment->offset->dynCast<Const>()) { |
| maxOffset = maxOffset + offset->value.getInteger(); |
| } |
| table->initial = std::max(table->initial, maxOffset); |
| }); |
| |
| // The code above raises table->initial to a size large enough to accomodate |
| // all of its segments, with the intention of avoiding a trap during |
| // startup. However a single segment of (say) size 4GB would have a table of |
| // that size, which will use a lot of memory and execute very slowly, so we |
| // prefer in the fuzzer to trap on such a thing. To achieve that, set a |
| // reasonable limit for the maximum table size. |
| // |
| // This also avoids an issue that arises from table->initial being an |
| // Address (64 bits) but Table::kMaxSize being an Index (32 bits), as a |
| // result of which we need to clamp to Table::kMaxSize as well in order for |
| // the module to validate (but since we are clamping to a smaller value, |
| // there is no need). |
| const Address ReasonableMaxTableSize = 10000; |
| table->initial = std::min(table->initial, ReasonableMaxTableSize); |
| assert(ReasonableMaxTableSize <= Table::kMaxSize); |
| |
| table->max = oneIn(2) ? Address(Table::kUnlimitedSize) : table->initial; |
| |
| if (!preserveImportsAndExports) { |
| // Avoid an imported table (which the fuzz harness would need to handle). |
| table->module = table->base = Name(); |
| } |
| } |
| } |
| |
| void TranslateToFuzzReader::shuffleExports() { |
| // Randomly ordering the exports is useful for a few reasons. First, initial |
| // content may have a natural order in which to execute things (an "init" |
| // export first, for example), and changing that order may lead to very |
| // different execution. Second, even in the fuzzer's own random content there |
| // is a "direction", since we generate as we go (e.g. no function calls a |
| // later function that does not exist yet / will be created later), and also |
| // we emit invokes for a function right after it (so we end up calling the |
| // same code several times in succession, but interleaving it with others may |
| // find more things). But we also keep a good chance for the natural order |
| // here, as it may help some initial content. Note we cannot do this if we are |
| // preserving the exports, as their order is something we must maintain. |
| if (wasm.exports.empty() || preserveImportsAndExports || oneIn(2) || |
| random.finished()) { |
| return; |
| } |
| |
| // Sort the exports in the simple Fisher-Yates manner. |
| // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm |
| for (Index i = 0; i < wasm.exports.size() - 1; i++) { |
| // Pick the index of the item to place at index |i|. The number of items to |
| // pick from begins at the full length, then decreases with i. |
| auto j = i + upTo(wasm.exports.size() - i); |
| |
| // Swap the item over here. |
| if (j != i) { |
| std::swap(wasm.exports[i], wasm.exports[j]); |
| } |
| } |
| |
| wasm.updateMaps(); |
| } |
| |
| void TranslateToFuzzReader::prepareHangLimitSupport() { |
| HANG_LIMIT_GLOBAL = Names::getValidGlobalName(wasm, "hangLimit"); |
| } |
| |
| void TranslateToFuzzReader::addHangLimitSupport() { |
| auto glob = |
| builder.makeGlobal(HANG_LIMIT_GLOBAL, |
| Type::i32, |
| builder.makeConst(int32_t(fuzzParams->HANG_LIMIT)), |
| Builder::Mutable); |
| wasm.addGlobal(std::move(glob)); |
| } |
| |
| void TranslateToFuzzReader::addImportLoggingSupport() { |
| for (auto type : loggableTypes) { |
| auto func = std::make_unique<Function>(); |
| Name baseName = std::string("log-") + type.toString(); |
| func->name = Names::getValidFunctionName(wasm, baseName); |
| logImportNames[type] = func->name; |
| if (!preserveImportsAndExports) { |
| func->module = "fuzzing-support"; |
| func->base = baseName; |
| func->type = Type(Signature(type, Type::none), NonNullable, Inexact); |
| } else { |
| // We cannot add an import, so just make it a trivial function (this is |
| // simpler than avoiding calls to logging in all the rest of the logic). |
| func->body = builder.makeNop(); |
| func->type = Type(Signature(type, Type::none), NonNullable, Exact); |
| } |
| wasm.addFunction(std::move(func)); |
| } |
| } |
| |
| void TranslateToFuzzReader::addImportCallingSupport() { |
| if (preserveImportsAndExports) { |
| return; |
| } |
| |
| if (wasm.features.hasReferenceTypes() && closedWorld) { |
| // In closed world mode we must *remove* the call-ref* imports, if they |
| // exist in the initial content. These are not valid to call in closed-world |
| // mode as they call function references. (Another solution here would be to |
| // make closed-world issue validation errors on these imports, but that |
| // would require changes to the general-purpose validator.) |
| for (auto& func : wasm.functions) { |
| if (func->imported() && func->module == "fuzzing-support" && |
| func->base.startsWith("call-ref")) { |
| // Make it non-imported, and with a simple body. |
| func->module = func->base = Name(); |
| func->type = func->type.with(Exact); |
| auto results = func->getResults(); |
| func->body = |
| results.isConcrete() ? makeConst(results) : makeNop(Type::none); |
| } |
| } |
| } |
| |
| // Only add these some of the time, as they inhibit some fuzzing (things like |
| // wasm-ctor-eval and wasm-merge are sensitive to the wasm being able to call |
| // its own exports, and to care about the indexes of the exports). |
| if (oneIn(2) || random.finished()) { |
| return; |
| } |
| |
| auto choice = upTo(16); |
| |
| if (choice & 1) { |
| // Given an export index, call it from JS. |
| // A second parameter has flags. The first bit determines whether we catch |
| // and rethrow all exceptions. (This ends up giving us the same signature |
| // and behavior as when we do not rethrow, so we just add the flags here |
| // rather than another export.) |
| callExportImportName = Names::getValidFunctionName(wasm, "call-export"); |
| auto func = std::make_unique<Function>(); |
| func->name = callExportImportName; |
| func->module = "fuzzing-support"; |
| func->base = "call-export"; |
| func->type = |
| Type(Signature({Type::i32, Type::i32}, Type::none), NonNullable, Inexact); |
| wasm.addFunction(std::move(func)); |
| } |
| |
| if (choice & 2) { |
| // Given an export index, call it from JS and catch all exceptions. Return |
| // whether we caught. Exceptions are common (if the index is invalid, in |
| // particular), so a variant that catches is useful to avoid halting. |
| callExportCatchImportName = |
| Names::getValidFunctionName(wasm, "call-export-catch"); |
| auto func = std::make_unique<Function>(); |
| func->name = callExportCatchImportName; |
| func->module = "fuzzing-support"; |
| func->base = "call-export-catch"; |
| func->type = Type(Signature(Type::i32, Type::i32), NonNullable, Inexact); |
| wasm.addFunction(std::move(func)); |
| } |
| |
| // If the wasm will be used for closed-world testing, we cannot use the |
| // call-ref variants, as mentioned before. |
| if (wasm.features.hasReferenceTypes() && !closedWorld) { |
| if (choice & 4) { |
| // Given an funcref, call it from JS. |
| callRefImportName = Names::getValidFunctionName(wasm, "call-ref"); |
| auto func = std::make_unique<Function>(); |
| func->name = callRefImportName; |
| func->module = "fuzzing-support"; |
| func->base = "call-ref"; |
| // As call-export, there is a flags param that allows us to catch+rethrow |
| // all exceptions. |
| func->type = |
| Type(Signature({Type(HeapType::func, Nullable), Type::i32}, Type::none), |
| NonNullable, |
| Inexact); |
| wasm.addFunction(std::move(func)); |
| } |
| |
| if (choice & 8) { |
| // Given an funcref, call it from JS and catch all exceptions (similar |
| // to callExportCatch), return 1 if we caught). |
| callRefCatchImportName = |
| Names::getValidFunctionName(wasm, "call-ref-catch"); |
| auto func = std::make_unique<Function>(); |
| func->name = callRefCatchImportName; |
| func->module = "fuzzing-support"; |
| func->base = "call-ref-catch"; |
| func->type = Type(Signature(Type(HeapType::func, Nullable), Type::i32), |
| NonNullable, |
| Inexact); |
| wasm.addFunction(std::move(func)); |
| } |
| } |
| } |
| |
| void TranslateToFuzzReader::addImportThrowingSupport() { |
| if (random.finished()) { |
| return; |
| } |
| // 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; |
| if (!preserveImportsAndExports) { |
| func->module = "fuzzing-support"; |
| func->base = "throw"; |
| func->type = Type(Signature(Type::i32, Type::none), NonNullable, Inexact); |
| } else { |
| // As with logging, implement in a trivial way when we cannot add imports. |
| func->body = builder.makeNop(); |
| func->type = Type(Signature(Type::i32, Type::none), NonNullable, Exact); |
| } |
| wasm.addFunction(std::move(func)); |
| } |
| |
| void TranslateToFuzzReader::addImportTableSupport() { |
| // For the table imports to be able to do anything, we must export a table |
| // for them. For simplicity, use the funcref table we use internally, though |
| // we could pick one at random, support non-funcref ones, and even export |
| // multiple ones TODO |
| if (!funcrefTableName || random.finished()) { |
| return; |
| } |
| |
| // If a "table" export already exists, skip fuzzing these imports, as the |
| // current export may not contain a valid table for it. We also skip if we are |
| // not adding imports or exports. |
| if (wasm.getExportOrNull("table") || preserveImportsAndExports) { |
| return; |
| } |
| |
| // Do not always export the table even if we can, as it inhibits some things |
| // in the fuzzer (with this export, the wasm becomes more sensitive to |
| // otherwise inconsequential changes: calling the table-get/set imports is |
| // influenced by the existence of this export). |
| if (!random.oneIn(3)) { |
| return; |
| } |
| |
| // Export the table. |
| wasm.addExport( |
| builder.makeExport("table", funcrefTableName, ExternalKind::Table)); |
| |
| // Get from the table. |
| { |
| tableGetImportName = Names::getValidFunctionName(wasm, "table-get"); |
| auto func = std::make_unique<Function>(); |
| func->name = tableGetImportName; |
| func->module = "fuzzing-support"; |
| func->base = "table-get"; |
| func->type = Type(Signature({Type::i32}, Type(HeapType::func, Nullable)), |
| NonNullable, |
| Inexact); |
| wasm.addFunction(std::move(func)); |
| } |
| |
| // Set into the table. |
| { |
| tableSetImportName = Names::getValidFunctionName(wasm, "table-set"); |
| auto func = std::make_unique<Function>(); |
| func->name = tableSetImportName; |
| func->module = "fuzzing-support"; |
| func->base = "table-set"; |
| func->type = |
| Type(Signature({Type::i32, Type(HeapType::func, Nullable)}, Type::none), |
| NonNullable, |
| Inexact); |
| wasm.addFunction(std::move(func)); |
| } |
| } |
| |
| void TranslateToFuzzReader::addImportSleepSupport() { |
| // Fuzz this somewhat rarely, as it may be slow, and only when we can add |
| // imports. |
| if (preserveImportsAndExports || !oneIn(4) || random.finished()) { |
| return; |
| } |
| |
| // An import that sleeps for a given number of milliseconds, and also receives |
| // an integer id. It returns that integer id (useful for tracking separate |
| // sleeps). |
| sleepImportName = Names::getValidFunctionName(wasm, "sleep"); |
| auto func = std::make_unique<Function>(); |
| func->name = sleepImportName; |
| func->module = "fuzzing-support"; |
| func->base = "sleep"; |
| func->type = |
| Type(Signature({Type::i32, Type::i32}, Type::i32), NonNullable, Inexact); |
| wasm.addFunction(std::move(func)); |
| } |
| |
| void TranslateToFuzzReader::addHashMemorySupport() { |
| // Don't always add this. |
| if (oneIn(2) || random.finished()) { |
| return; |
| } |
| |
| // Add memory hasher helper (for the hash, see hash.h). The function looks |
| // like: |
| // function hashMemory() { |
| // hash = 5381; |
| // hash = ((hash << 5) + hash) ^ mem[0]; |
| // hash = ((hash << 5) + hash) ^ mem[1]; |
| // .. |
| // return hash; |
| // } |
| std::vector<Expression*> contents; |
| 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++) { |
| contents.push_back(builder.makeLocalSet( |
| 0, |
| builder.makeBinary( |
| XorInt32, |
| builder.makeBinary( |
| AddInt32, |
| builder.makeBinary(ShlInt32, |
| builder.makeLocalGet(0, Type::i32), |
| builder.makeConst(uint32_t(5))), |
| builder.makeLocalGet(0, Type::i32)), |
| builder.makeLoad(1, |
| false, |
| i, |
| 1, |
| builder.makeConst(zero), |
| Type::i32, |
| wasm.memories[0]->name)))); |
| } |
| contents.push_back(builder.makeLocalGet(0, Type::i32)); |
| auto* body = builder.makeBlock(contents); |
| hashMemoryName = Names::getValidFunctionName(wasm, "hashMemory"); |
| auto* hasher = wasm.addFunction(builder.makeFunction( |
| hashMemoryName, |
| Type(Signature(Type::none, Type::i32), NonNullable, Exact), |
| {Type::i32}, |
| body)); |
| |
| if (!preserveImportsAndExports && !wasm.getExportOrNull("hashMemory")) { |
| wasm.addExport( |
| builder.makeExport("hashMemory", hasher->name, ExternalKind::Function)); |
| // Export memory so JS fuzzing can use it |
| if (!wasm.getExportOrNull("memory")) { |
| wasm.addExport(builder.makeExport( |
| "memory", wasm.memories[0]->name, ExternalKind::Memory)); |
| } |
| } |
| } |
| |
| void TranslateToFuzzReader::setImportedModule(std::string importedModuleName) { |
| importedModule.emplace(); |
| |
| importedModule->features = FeatureSet::All; |
| ModuleReader().read(importedModuleName, *importedModule); |
| } |
| |
| void TranslateToFuzzReader::useImportedFunctions() { |
| if (!importedModule) { |
| return; |
| } |
| |
| // Add some of the module's exported functions as imports, at a random rate. |
| auto rate = upTo(11); |
| for (auto& exp : importedModule->exports) { |
| if (exp->kind != ExternalKind::Function || upTo(10) >= rate) { |
| continue; |
| } |
| |
| auto* func = importedModule->getFunction(*exp->getInternalName()); |
| auto name = |
| Names::getValidFunctionName(wasm, "primary_" + exp->name.toString()); |
| // We can import it as its own type, or any (declared) supertype. |
| auto type = getSuperType(func->type).with(NonNullable).with(Inexact); |
| auto import = builder.makeFunction(name, type, {}); |
| import->module = "primary"; |
| import->base = exp->name; |
| wasm.addFunction(std::move(import)); |
| } |
| |
| // TODO: All other imports: memories, tables, etc. We must, as we do |
| // with functions, take care to run this *after* the removal of those |
| // imports (as normally we remove them all, as the fuzzer harness will |
| // not provide them, but an imported module is the exception). |
| } |
| |
| void TranslateToFuzzReader::useImportedGlobals() { |
| if (!importedModule) { |
| return; |
| } |
| |
| // Add some of the module's exported globals as imports, at a random rate. |
| auto rate = upTo(11); |
| for (auto& exp : importedModule->exports) { |
| if (exp->kind != ExternalKind::Global || upTo(10) >= rate) { |
| continue; |
| } |
| |
| auto* global = importedModule->getGlobal(*exp->getInternalName()); |
| auto name = |
| Names::getValidGlobalName(wasm, "primary_" + exp->name.toString()); |
| // We can import it as its own type, or if immutable, any (declared) |
| // supertype. |
| Type type; |
| Builder::Mutability mutability; |
| if (global->mutable_) { |
| mutability = Builder::Mutable; |
| type = global->type; |
| } else { |
| mutability = Builder::Immutable; |
| type = getSuperType(global->type); |
| } |
| auto import = builder.makeGlobal(name, type, nullptr, mutability); |
| import->module = "primary"; |
| import->base = exp->name; |
| useGlobalLater(wasm.addGlobal(std::move(import))); |
| } |
| } |
| |
| TranslateToFuzzReader::FunctionCreationContext::FunctionCreationContext( |
| TranslateToFuzzReader& parent, Function* func) |
| : parent(parent), func(func) { |
| parent.funcContext = this; |
| |
| // Note the types of all locals. |
| computeTypeLocals(); |
| |
| // Find the right index for labelIndex: we emit names like label$5, so we need |
| // the index to be larger than all currently existing. |
| if (!func->body) { |
| return; |
| } |
| |
| struct Finder : public PostWalker<Finder, UnifiedExpressionVisitor<Finder>> { |
| Index maxIndex = 0; |
| |
| void visitExpression(Expression* curr) { |
| // Note all scope names, and fix up all uses. |
| BranchUtils::operateOnScopeNameDefs(curr, [&](Name& name) { |
| if (name.is()) { |
| if (name.startsWith("label$")) { |
| auto str = name.toString(); |
| str = str.substr(6); |
| Index index = atoi(str.c_str()); |
| maxIndex = std::max(maxIndex, index + 1); |
| } |
| } |
| }); |
| } |
| } finder; |
| finder.walk(func->body); |
| labelIndex = finder.maxIndex; |
| } |
| |
| TranslateToFuzzReader::FunctionCreationContext::~FunctionCreationContext() { |
| // We must ensure non-nullable locals validate. Later down we'll run |
| // TypeUpdating::handleNonDefaultableLocals which will make them validate by |
| // turning them nullable + add ref.as_non_null to fix up types. That has the |
| // downside of making them trap at runtime, however, and also we lose the non- |
| // nullability in the type, so we prefer to do a manual fixup that avoids a |
| // trap, which we do by writing a non-nullable value into the local at the |
| // function entry. |
| // TODO: We could be more precise and use a LocalGraph here, at the cost of |
| // doing more work. |
| LocalStructuralDominance info( |
| func, parent.wasm, LocalStructuralDominance::NonNullableOnly); |
| for (auto index : info.nonDominatingIndices) { |
| // Do not always do this, but with high probability, to reduce the amount of |
| // traps. |
| if (!parent.oneIn(5)) { |
| auto* value = parent.makeTrivial(func->getLocalType(index)); |
| func->body = parent.builder.makeSequence( |
| parent.builder.makeLocalSet(index, value), func->body); |
| } |
| } |
| |
| // Then, to handle remaining cases we did not just fix up, do the general |
| // fixup to ensure we validate. |
| TypeUpdating::handleNonDefaultableLocals(func, parent.wasm); |
| |
| assert(breakableStack.empty()); |
| assert(hangStack.empty()); |
| parent.funcContext = nullptr; |
| } |
| |
| Expression* TranslateToFuzzReader::makeHangLimitCheck() { |
| // If the hang limit global reaches 0 then we trap and reset it. That allows |
| // calls to other exports to proceed, with hang checking, after the trap halts |
| // the currently called export. |
| return builder.makeSequence( |
| 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.makeGlobalSet( |
| HANG_LIMIT_GLOBAL, |
| builder.makeBinary(BinaryOp::SubInt32, |
| builder.makeGlobalGet(HANG_LIMIT_GLOBAL, Type::i32), |
| builder.makeConst(int32_t(1))))); |
| } |
| |
| Expression* TranslateToFuzzReader::makeImportLogging() { |
| auto type = getLoggableType(); |
| return builder.makeCall(logImportNames[type], {make(type)}, Type::none); |
| } |
| |
| 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); |
| |
| // 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() { |
| assert(tableGetImportName); |
| return builder.makeCall( |
| tableGetImportName, {make(Type::i32)}, Type(HeapType::func, Nullable)); |
| } |
| |
| Expression* TranslateToFuzzReader::makeImportTableSet(Type type) { |
| assert(type == Type::none); |
| assert(tableSetImportName); |
| return builder.makeCall( |
| tableSetImportName, |
| {make(Type::i32), makeBasicRef(Type(HeapType::func, Nullable))}, |
| Type::none); |
| } |
| |
| Expression* TranslateToFuzzReader::makeImportCallCode(Type type) { |
| // Call code: either an export or a ref. Each has a catching and non-catching |
| // variant. The catching variants return i32, the others none. |
| assert(type == Type::none || type == Type::i32); |
| auto catching = type == Type::i32; |
| auto exportTarget = |
| catching ? callExportCatchImportName : callExportImportName; |
| auto refTarget = catching ? callRefCatchImportName : callRefImportName; |
| |
| // We want to call a ref less often, as refs are more likely to error (a |
| // function reference can have arbitrary params and results, including things |
| // that error on the JS boundary; an export is already filtered for such |
| // things in some cases - when we legalize the boundary - and even if not, we |
| // emit lots of void(void) functions - all the invoke_foo functions - that are |
| // safe to call). |
| if (refTarget) { |
| // This matters a lot more in the variants that do *not* catch (in the |
| // catching ones, we just get a result of 1, but when not caught it halts |
| // execution). |
| if ((catching && (!exportTarget || oneIn(2))) || (!catching && oneIn(4))) { |
| // Most of the time make a non-nullable funcref, to avoid errors. |
| auto refType = Type(HeapType::func, oneIn(10) ? Nullable : NonNullable); |
| std::vector<Expression*> args = {make(refType)}; |
| if (!catching) { |
| // Only the first bit matters here, so we can send anything (this is |
| // future-proof for later bits, and has no downside now). |
| args.push_back(make(Type::i32)); |
| } |
| return builder.makeCall(refTarget, args, type); |
| } |
| } |
| |
| if (!exportTarget) { |
| // We decided not to emit a call-ref here, due to fear of erroring, and |
| // there is no call-export, so just emit something trivial. |
| return makeTrivial(type); |
| } |
| |
| // Pick the maximum export index to call. |
| Index maxIndex = wasm.exports.size(); |
| if (type == Type::i32) { |
| // This swallows errors, so we can be less careful, but we do still want to |
| // avoid swallowing a lot as executing code is more interesting. (Note that |
| // even though we double here, the risk is not that great: we are still |
| // adding functions as we go, so the first half of functions/exports can |
| // double here and still end up in bounds by the time we've added them all.) |
| maxIndex = (maxIndex + 1) * 2; |
| } |
| |
| // Most of the time, call a valid export index in the range we picked, but |
| // sometimes allow anything at all. |
| auto* index = make(Type::i32); |
| if (!allowOOB || !oneIn(10)) { |
| index = builder.makeBinary( |
| RemUInt32, index, builder.makeConst(int32_t(maxIndex))); |
| } |
| |
| // The non-catching variants send a flags argument, which says whether to |
| // catch+rethrow. |
| std::vector<Expression*> args = {index}; |
| if (!catching) { |
| // Only the first bit matters here, so we can send anything (this is |
| // future-proof for later bits, and has no downside now). |
| args.push_back(make(Type::i32)); |
| } |
| return builder.makeCall(exportTarget, args, type); |
| } |
| |
| Expression* TranslateToFuzzReader::makeImportSleep(Type type) { |
| // Sleep for some ms, and return a given id. |
| auto* ms = make(Type::i32); |
| auto id = make(Type::i32); |
| return builder.makeCall(sleepImportName, {ms, id}, Type::i32); |
| } |
| |
| Expression* TranslateToFuzzReader::makeMemoryHashLogging() { |
| auto* hash = builder.makeCall(hashMemoryName, {}, Type::i32); |
| return builder.makeCall(logImportNames[Type::i32], {hash}, Type::none); |
| } |
| |
| void TranslateToFuzzReader::processFunctions() { |
| // Functions that are eligible for being modded. We only do so once to each |
| // function, at most, so once we do we remove it from here. |
| std::vector<Function*> moddable; |
| |
| // Defined initial functions are moddable. |
| for (auto& func : wasm.functions) { |
| if (!func->imported()) { |
| moddable.push_back(func.get()); |
| } |
| } |
| |
| auto numInitialExports = wasm.exports.size(); |
| |
| // Add invocations, which can help execute the code here even if the function |
| // was not exported (or was exported but with a signature that traps |
| // immediately, like receiving a non-nullable ref, that the fuzzer can't |
| // provide from JS). Note we cannot iterate on wasm.functions because |
| // addInvocations modifies that. |
| for (auto* func : moddable) { |
| addInvocations(func); |
| } |
| |
| // We do not want to always mod in the same frequency. Pick a chance to mod a |
| // function. When the chance is maximal we will mod every single function, and |
| // immediately after creating it; when the chance is minimal we will not mod |
| // anything; values in the middle will end up randomly modding some functions, |
| // at random times (random times are useful because we might create function |
| // A, then B, then mod A, and since B has already been created, the modding of |
| // A may lead to calls to B). |
| const int RESOLUTION = 10; |
| auto chance = upTo(RESOLUTION + 1); |
| |
| // We do not want to always add new functions, if there are initial ones: |
| // adding many additional functions will cause a lot of global properties to |
| // change, e.g., if the initial content was a carefully crafted testcase |
| // showing some situation of reads and writes between struct fields, adding |
| // many new functions will likely add reads and writes to all the fields, |
| // preventing global operations like field removal or immutabilification. |
| auto allowNew = !haveInitialFunctions || !oneIn(10); |
| |
| // Keep working while we have random data. |
| while (!random.finished()) { |
| if (!moddable.empty() && upTo(RESOLUTION) < chance) { |
| // Mod an existing function. |
| auto index = upTo(moddable.size()); |
| auto* func = moddable[index]; |
| modFunction(func); |
| |
| // Remove this function from the vector by swapping the last item to its |
| // place, and truncating. |
| moddable[index] = moddable.back(); |
| moddable.pop_back(); |
| } else if (allowNew) { |
| // Add a new function |
| auto* func = addFunction(); |
| addInvocations(func); |
| |
| // It may be modded later, if we allow out-of-bounds: we emit OOB checks |
| // in the code we just generated, and any changes could break that. |
| if (allowOOB) { |
| moddable.push_back(func); |
| } |
| } else { |
| // If we found nothing to do, consume some data so that we make progress |
| // towards the loop ending. |
| get(); |
| } |
| } |
| |
| // Interpose on initial exports. When initial content contains exports, it can |
| // be useful to add new code that executes in them, rather than just adding |
| // new exports later. To some extent modifying the initially-exported function |
| // gives us that, but typically these are small changes, not calls to entirely |
| // new code (and this is especially important when preserveImportsAndExports, |
| // as in that mode we do not add new exports, so this interposing is our main |
| // chance to run new code using the existing exports). |
| // |
| // Interpose with a call before the old code. We do a call here so that we end |
| // up running a useful amount of new code (rather than just make(none) which |
| // would only emit something local in the current function, and which depends |
| // on its contents). |
| // TODO: We could also interpose after, either in functions without results, |
| // or by saving the results to a temp local as we call. |
| // |
| // Specifically, we will call functions, for simplicity, with no params or |
| // results. Such functions exist in abundance in general, because the |
| // invocations we add look exactly that way. First, find all such functions, |
| // and then find places to interpose calls to them. |
| std::vector<Name> noParamsOrResultFuncs; |
| for (auto& func : wasm.functions) { |
| if (func->getParams() == Type::none && func->getResults() == Type::none) { |
| noParamsOrResultFuncs.push_back(func->name); |
| } |
| } |
| if (!noParamsOrResultFuncs.empty()) { |
| for (Index i = 0; i < numInitialExports; i++) { |
| auto& exp = wasm.exports[i]; |
| if (exp->kind == ExternalKind::Function && upTo(RESOLUTION) < chance) { |
| auto* func = wasm.getFunction(*exp->getInternalName()); |
| if (!func->imported()) { |
| auto* call = |
| builder.makeCall(pick(noParamsOrResultFuncs), {}, Type::none); |
| func->body = builder.makeSequence(call, func->body); |
| } |
| } |
| } |
| } |
| |
| // At the very end, add hang limit checks (so no modding can override them). |
| if (fuzzParams->HANG_LIMIT > 0) { |
| for (auto& func : wasm.functions) { |
| if (!func->imported()) { |
| addHangLimitChecks(func.get()); |
| } |
| } |
| } |
| } |
| |
| // TODO: return std::unique_ptr<Function> |
| Function* TranslateToFuzzReader::addFunction() { |
| LOGGING_PERCENT = upToSquared(100); |
| auto allocation = std::make_unique<Function>(); |
| auto* func = allocation.get(); |
| func->name = Names::getValidFunctionName(wasm, "func"); |
| |
| // Pick params and results. There may be an interesting heap type we can use. |
| std::optional<HeapType> funcType; |
| auto& funcTypes = interestingHeapSubTypes[HeapTypes::func]; |
| if (!funcTypes.empty() && oneIn(2)) { |
| auto type = pick(funcTypes); |
| 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); |
| std::vector<Type> params; |
| params.reserve(numParams); |
| for (Index i = 0; i < numParams; i++) { |
| auto type = getSingleConcreteType(); |
| params.push_back(type); |
| } |
| auto paramType = Type(params); |
| auto resultType = getControlFlowType(); |
| funcType = Signature(paramType, resultType); |
| } |
| func->type = Type(*funcType, NonNullable, Exact); |
| |
| Index numVars = upToSquared(fuzzParams->MAX_VARS); |
| for (Index i = 0; i < numVars; i++) { |
| auto type = getConcreteType(); |
| if (!TypeUpdating::canHandleAsLocal(type)) { |
| type = Type::i32; |
| } |
| func->vars.push_back(type); |
| } |
| // Generate the function creation context after we filled in locals, which it |
| // will scan. |
| FunctionCreationContext context(*this, func); |
| // with small chance, make the body unreachable |
| auto bodyType = func->getResults(); |
| if (oneIn(10)) { |
| bodyType = Type::unreachable; |
| } |
| // with reasonable chance make the body a block |
| if (oneIn(2)) { |
| func->body = makeBlock(bodyType); |
| } else { |
| func->body = make(bodyType); |
| } |
| |
| // Add hang limit checks after all other operations on the function body. |
| 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. Avoid non- |
| // nullable params, as those cannot be constructed by the fuzzer on the |
| // outside. |
| auto paramType = func->getParams(); |
| auto resultType = func->getResults(); |
| bool validExportParams = |
| std::all_of(paramType.begin(), paramType.end(), [&](Type t) { |
| return t.isDefaultable() && isValidPublicType(t); |
| }); |
| if (!preserveImportsAndExports && validExportParams && |
| isValidPublicType(resultType) && (numAddedFunctions == 0 || oneIn(2)) && |
| !wasm.getExportOrNull(func->name)) { |
| wasm.addExport( |
| Builder::makeExport(func->name, func->name, ExternalKind::Function)); |
| } |
| // add some to an elem segment TODO we could do this for imported funcs too |
| while (oneIn(3) && !random.finished()) { |
| auto type = func->type; |
| std::vector<ElementSegment*> compatibleSegments; |
| ModuleUtils::iterActiveElementSegments(wasm, [&](ElementSegment* segment) { |
| if (Type::isSubType(type, segment->type)) { |
| compatibleSegments.push_back(segment); |
| } |
| }); |
| auto& randomElem = compatibleSegments[upTo(compatibleSegments.size())]; |
| randomElem->data.push_back(builder.makeRefFunc(func->name)); |
| } |
| numAddedFunctions++; |
| return func; |
| } |
| |
| void TranslateToFuzzReader::modFunction(Function* func) { |
| FunctionCreationContext context(*this, func); |
| |
| dropToLog(func); |
| // TODO: if we add OOB checks after creation, then we can do it on |
| // initial contents too, and it may be nice to *not* run these |
| // passes, like we don't run them on new functions. But, we may |
| // still want to run them some of the time, at least, so that we |
| // check variations on initial testcases even at the risk of OOB. |
| recombine(func); |
| mutate(func); |
| fixAfterChanges(func); |
| } |
| |
| void TranslateToFuzzReader::addHangLimitChecks(Function* func) { |
| // loop limit |
| for (auto* loop : FindAll<Loop>(func->body).list) { |
| loop->body = |
| builder.makeSequence(makeHangLimitCheck(), loop->body, loop->type); |
| } |
| // recursion limit |
| func->body = |
| builder.makeSequence(makeHangLimitCheck(), func->body, func->getResults()); |
| // ArrayNew can hang the fuzzer if the array size is massive. This doesn't |
| // cause an OOM (which the fuzzer knows how to ignore) but it just works for |
| // many seconds on building the array. To avoid that, limit the size with high |
| // probability. |
| for (auto* arrayNew : FindAll<ArrayNew>(func->body).list) { |
| if (!oneIn(100)) { |
| arrayNew->size = builder.makeBinary( |
| AndInt32, arrayNew->size, builder.makeConst(int32_t(1024 - 1))); |
| } |
| } |
| } |
| |
| void TranslateToFuzzReader::recombine(Function* func) { |
| // Don't always do this. |
| if (oneIn(2)) { |
| return; |
| } |
| // First, scan and group all expressions by type. |
| struct Scanner |
| : public PostWalker<Scanner, UnifiedExpressionVisitor<Scanner>> { |
| TranslateToFuzzReader& parent; |
| // A map of all expressions, categorized by type. |
| InsertOrderedMap<Type, std::vector<Expression*>> exprsByType; |
| Scanner(TranslateToFuzzReader& parent) : parent(parent) {} |
| |
| void visitExpression(Expression* curr) { |
| if (parent.canBeArbitrarilyReplaced(curr)) { |
| for (auto type : getRelevantTypes(curr->type)) { |
| exprsByType[type].push_back(curr); |
| } |
| } |
| } |
| |
| std::vector<Type> getRelevantTypes(Type type) { |
| // Given an expression of a type, we can replace not only other |
| // expressions with the same type, but also supertypes - since then we'd |
| // be replacing with a subtype, which is valid. |
| if (!type.isRef()) { |
| return {type}; |
| } |
| |
| std::vector<Type> ret; |
| ret.push_back(type); |
| |
| if (type.isNonNullable()) { |
| auto nullable = getRelevantTypes(type.with(Nullable)); |
| ret.insert(ret.end(), nullable.begin(), nullable.end()); |
| } |
| if (type.isExact()) { |
| auto inexact = getRelevantTypes(type.with(Inexact)); |
| ret.insert(ret.end(), inexact.begin(), inexact.end()); |
| // Do not consider exact references to supertypes. |
| return ret; |
| } |
| |
| for (auto heapType = type.getHeapType().getSuperType(); heapType; |
| heapType = heapType->getSuperType()) { |
| ret.push_back(type.with(*heapType)); |
| } |
| |
| return ret; |
| } |
| }; |
| Scanner scanner(*this); |
| scanner.walk(func->body); |
| // Potentially trim the list of possible picks, so replacements are more |
| // likely to collide. |
| for (auto& pair : scanner.exprsByType) { |
| if (oneIn(2)) { |
| continue; |
| } |
| auto& list = pair.second; |
| std::vector<Expression*> trimmed; |
| size_t num = upToSquared(list.size()); |
| for (size_t i = 0; i < num; i++) { |
| trimmed.push_back(pick(list)); |
| } |
| if (trimmed.empty()) { |
| trimmed.push_back(pick(list)); |
| } |
| list.swap(trimmed); |
| } |
| // Replace them with copies, to avoid a copy into one altering another copy |
| for (auto& pair : scanner.exprsByType) { |
| for (auto*& item : pair.second) { |
| item = ExpressionManipulator::copy(item, wasm); |
| } |
| } |
| // Second, with some probability replace an item with another item having |
| // a proper type. (This is not always valid due to nesting of labels, but |
| // we'll fix that up later.) |
| struct Modder : public PostWalker<Modder, UnifiedExpressionVisitor<Modder>> { |
| Module& wasm; |
| Scanner& scanner; |
| TranslateToFuzzReader& parent; |
| |
| Modder(Module& wasm, Scanner& scanner, TranslateToFuzzReader& parent) |
| : wasm(wasm), scanner(scanner), parent(parent) {} |
| |
| void visitExpression(Expression* curr) { |
| if (parent.oneIn(10) && parent.canBeArbitrarilyReplaced(curr)) { |
| // Replace it! |
| auto& candidates = scanner.exprsByType[curr->type]; |
| assert(!candidates.empty()); // this expression itself must be there |
| auto* rep = parent.pick(candidates); |
| replaceCurrent(ExpressionManipulator::copy(rep, wasm)); |
| } |
| } |
| }; |
| Modder modder(wasm, scanner, *this); |
| modder.walk(func->body); |
| // TODO: A specific form of recombination we should perhaps do more often is |
| // to recombine among an expression's children, and in particular to |
| // reorder them. |
| } |
| |
| // Given two expressions, try to replace one of the children of the first with |
| // the second. For example, given i32.add and an i32.const, the i32.const could |
| // be placed as either of the children of the i32.add. This tramples the |
| // existing content there. Returns true if we found a place. |
| static bool replaceChildWith(Expression* expr, Expression* with) { |
| for (auto*& child : ChildIterator(expr)) { |
| // To replace, we must have an appropriate type, and we cannot replace a |
| // Pop under any circumstances. |
| if (Type::isSubType(with->type, child->type) && |
| FindAll<Pop>(child).list.empty()) { |
| child = with; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void TranslateToFuzzReader::mutate(Function* func) { |
| // We want a 50% chance to not do this at all, and otherwise, we want to pick |
| // a different frequency to do it in each function. That gives us more |
| // diversity between fuzzings of the same initial content (once we might |
| // mutate with 5%, and only change one or two places, while another time we |
| // might mutate with 50% and change quite a lot; without this type of |
| // mechanism, in a large function the amount of mutations will generally be |
| // very close to the mean due to the central limit theorem). |
| auto r = upTo(200); |
| if (r > 100) { |
| return; |
| } |
| |
| // Prefer lower numbers: We want something like a 10% chance to mutate on |
| // average. To achieve that, we raise r/100, which is in the range [0, 1], to |
| // the 9th power, giving us a number also in the range [0, 1] with a mean of |
| // \integral_0^1 t^9 dx = 0.1 * t^10 |_0^1 = 0.1 |
| // As a result, we get a value in the range of 0-100%. (Note that 100% is ok |
| // since we can't replace everything anyhow, see below.) |
| double t = r; |
| t = t / 100; |
| t = pow(t, 9); |
| Index percentChance = t * 100; |
| // Adjust almost-zero frequencies to at least a few %, just so we have some |
| // reasonable chance of making some changes. |
| percentChance = std::max(percentChance, Index(3)); |
| |
| // First, find things to replace and their types. SubtypingDiscoverer needs to |
| // do this in a single, full walk (as types of children depend on parents, and |
| // even block targets). |
| struct Finder |
| : public ControlFlowWalker<Finder, SubtypingDiscoverer<Finder>> { |
| // Maps children we can replace to the types we can replace them with. We |
| // only store nontrivial ones (i.e., where the type is not just the child's |
| // type). |
| std::unordered_map<Expression*, GLBFinder> childTypes; |
| |
| // We only care about constraints on Expression* things. |
| void noteSubtype(Type sub, Type super) {} |
| void noteSubtype(HeapType sub, HeapType super) {} |
| |
| void noteSubtype(Type sub, Expression* super) { |
| // The expression must be a supertype of a fixed type. Nothing to do. |
| } |
| void noteSubtype(Expression* sub, Type super) { |
| if (super.isRef()) { |
| childTypes[sub].note(super); |
| } |
| } |
| void noteSubtype(Expression* sub, Expression* super) { |
| noteSubtype(sub, super->type); |
| } |
| void noteNonFlowSubtype(Expression* sub, Type super) { |
| noteSubtype(sub, super); |
| } |
| |
| // TODO: Many casts can accept the top type. We may need to use visit*(), to |
| // handle each expression class separately. |
| void noteCast(HeapType src, Type dst) {} |
| void noteCast(Expression* src, Type dst) {} |
| void noteCast(Expression* src, Expression* dst) {} |
| } finder; |
| |
| if (oneIn(2)) { |
| // |finder| reads the IR, and it must be totally valid - e.g. breaks have a |
| // proper break target - or else we'd hit internal errors. Fix it up first. |
| // (Otherwise, fixing it up is done once after mutation, and in that case |
| // we can mutate the IR in simple ways but not read it using |
| // useSubtypingDiscoverer). We avoid always doing this second fixup as it |
| // may bias the code in some ways. |
| fixAfterChanges(func); |
| finder.walkFunctionInModule(func, &wasm); |
| } |
| |
| // Next, modify things. |
| struct Modder : public PostWalker<Modder, UnifiedExpressionVisitor<Modder>> { |
| TranslateToFuzzReader& parent; |
| Index percentChance; |
| Finder& finder; |
| |
| // Whether to replace with unreachable. This can lead to less code getting |
| // executed, so we don't want to do it all the time even in a big function. |
| bool allowUnreachable; |
| |
| Modder(TranslateToFuzzReader& parent, Index percentChance, Finder& finder) |
| : parent(parent), percentChance(percentChance), finder(finder) { |
| // If the parent allows it then sometimes replace with an unreachable, and |
| // sometimes not. Even if we allow it, only do it in certain functions |
| // (half the time) and only do it rarely (see below). |
| allowUnreachable = parent.allowAddingUnreachableCode && parent.oneIn(2); |
| } |
| |
| void visitExpression(Expression* curr) { |
| // See if we want to replace it. |
| if (!parent.canBeArbitrarilyReplaced(curr) || |
| parent.upTo(100) >= percentChance) { |
| return; |
| } |
| |
| // Find the type to replace with. |
| auto type = curr->type; |
| if (type.isRef()) { |
| auto& glb = finder.childTypes[curr]; |
| if (glb.noted()) { |
| type = glb.getGLB(); |
| // We can only be given a less-refined type (certainly we can replace |
| // curr with its own type). |
| assert(Type::isSubType(curr->type, type)); |
| } |
| } |
| |
| // We can replace in various modes, see below. Generate a random number |
| // up to 100 to help us there. |
| int mode = parent.upTo(100); |
| |
| if (allowUnreachable && mode < 5) { |
| replaceCurrent(parent.make(Type::unreachable)); |
| return; |
| } |
| |
| // For constants, perform only a small tweaking in some cases. |
| // TODO: more minor tweaks to immediates, like making a load atomic or |
| // not, changing an offset, etc. |
| if (auto* c = curr->dynCast<Const>()) { |
| if (mode < 50) { |
| c->value = parent.tweak(c->value); |
| } else { |
| // Just replace the entire thing. |
| replaceCurrent(parent.make(type)); |
| } |
| return; |
| } |
| |
| // Generate a replacement for the expression, and by default replace all |
| // of |curr| (including children) with that replacement, but in some |
| // cases we can do more subtle things. |
| // |
| // Note that such a replacement is not always valid due to nesting of |
| // labels, but we'll fix that up later. Note also that make() picks a |
| // subtype, so this has a chance to replace us with anything that is |
| // valid to put here. |
| auto* rep = parent.make(type); |
| if (mode < 33 && rep->type != Type::none) { |
| // This has a non-none type. Replace the output, keeping the |
| // expression and its children in a drop. This "interposes" between |
| // this expression and its parent, something like this: |
| // |
| // (D |
| // (A |
| // (B) |
| // (C) |
| // ) |
| // ) |
| //// |
| // => ;; keep A, replace it in the parent |
| // |
| // (D |
| // (block |
| // (drop |
| // (A |
| // (B) |
| // (C) |
| // ) |
| // ) |
| // (NEW) |
| // ) |
| // ) |
| // |
| // We also sometimes try to insert A as a child of NEW, so we actually |
| // interpose directly: |
| // |
| // (D |
| // (NEW |
| // (A |
| // (B) |
| // (C) |
| // ) |
| // ) |
| // ) |
| // |
| // We do not do that all the time, as inserting a drop is actually an |
| // important situation to test: the drop makes the output of A unused, |
| // which may let optimizations remove it. |
| if ((mode & 1) && replaceChildWith(rep, curr)) { |
| // We managed to replace one of the children with curr, and have |
| // nothing more to do. |
| } else { |
| // Drop curr and append. |
| rep = parent.builder.makeSequence(parent.builder.makeDrop(curr), rep); |
| } |
| } else if (mode >= 66 && !Properties::isControlFlowStructure(curr)) { |
| ChildIterator children(curr); |
| auto numChildren = children.getNumChildren(); |
| if (numChildren > 0 && numChildren < 5) { |
| // This is a normal (non-control-flow) expression with at least one |
| // child (and not an excessive amount of them; see the processing |
| // below). "Interpose" between the children and this expression by |
| // keeping them and replacing the parent |curr|. We do this by |
| // generating drops of the children, like this: |
| // |
| // (A |
| // (B) |
| // (C) |
| // ) |
| // |
| // => ;; keep children, replace A |
| // |
| // (block |
| // (drop (B)) |
| // (drop (C)) |
| // (NEW) |
| // ) |
| // |
| auto* block = parent.builder.makeBlock(); |
| for (auto* child : children) { |
| // Only drop the child if we can't replace it as one of NEW's |
| // children. This does a linear scan of |rep| which is the reason |
| // for the above limit on the number of children. |
| if (!replaceChildWith(rep, child)) { |
| block->list.push_back(parent.builder.makeDrop(child)); |
| } |
| } |
| |
| if (!block->list.empty()) { |
| // We need the block, that is, we did not find a place for all the |
| // children. |
| block->list.push_back(rep); |
| block->finalize(); |
| rep = block; |
| } |
| } |
| } |
| replaceCurrent(rep); |
| } |
| }; |
| |
| Modder modder(*this, percentChance, finder); |
| modder.walkFunctionInModule(func, &wasm); |
| } |
| |
| void TranslateToFuzzReader::fixAfterChanges(Function* func) { |
| struct Fixer |
| : public ExpressionStackWalker<Fixer, UnifiedExpressionVisitor<Fixer>> { |
| Module& wasm; |
| TranslateToFuzzReader& parent; |
| |
| Fixer(Module& wasm, TranslateToFuzzReader& parent) |
| : wasm(wasm), parent(parent) {} |
| |
| // Track seen names to find duplication, which is invalid. |
| std::set<Name> seen; |
| |
| void visitExpression(Expression* curr) { |
| // Note all scope names, and fix up all uses. |
| BranchUtils::operateOnScopeNameDefs(curr, [&](Name& name) { |
| if (name.is()) { |
| if (seen.count(name)) { |
| replace(); |
| } else { |
| seen.insert(name); |
| } |
| } |
| }); |
| BranchUtils::operateOnScopeNameUses(curr, [&](Name& name) { |
| if (name.is()) { |
| replaceIfInvalid(name); |
| } |
| }); |
| } |
| |
| void replaceIfInvalid(Name target) { |
| if (!hasBreakTarget(target)) { |
| // There is no valid parent, replace with something trivially safe. |
| replace(); |
| } |
| } |
| |
| void replace() { replaceCurrent(parent.makeTrivial(getCurrent()->type)); } |
| |
| bool hasBreakTarget(Name name) { |
| // The break must be on top. |
| assert(!expressionStack.empty()); |
| if (expressionStack.size() < 2) { |
| // There must be a scope for this break to be valid. |
| return false; |
| } |
| Index i = expressionStack.size() - 2; |
| while (1) { |
| auto* curr = expressionStack[i]; |
| bool has = false; |
| BranchUtils::operateOnScopeNameDefs(curr, [&](Name& def) { |
| if (def == name) { |
| has = true; |
| } |
| }); |
| if (has) { |
| return true; |
| } |
| if (i == 0) { |
| return false; |
| } |
| i--; |
| } |
| } |
| |
| void visitRethrow(Rethrow* curr) { |
| if (!isValidTryRef(curr->target, curr)) { |
| replace(); |
| } else { |
| visitExpression(curr); |
| } |
| } |
| |
| void visitTry(Try* curr) { |
| if (curr->delegateTarget.is() && |
| !isValidTryRef(curr->delegateTarget, curr)) { |
| replace(); |
| } else { |
| visitExpression(curr); |
| } |
| } |
| |
| // Check if a reference to a try is valid. |
| bool isValidTryRef(Name target, Expression* curr) { |
| // The rethrow or try must be on top. |
| assert(!expressionStack.empty()); |
| assert(expressionStack.back() == curr); |
| if (expressionStack.size() < 2) { |
| // There must be a parent try for this to be valid. |
| return false; |
| } |
| Index i = expressionStack.size() - 2; |
| // Rethrows and try-delegates must target a try. Find it. |
| while (1) { |
| if (auto* tryy = expressionStack[i]->dynCast<Try>()) { |
| // A rethrow must be nested in a catch of that try, not the body. A |
| // try-delegate is the reverse. Look at the child above us to check, |
| // when we find the proper try. |
| if (tryy->name == target) { |
| assert(i + 1 < expressionStack.size()); |
| auto* child = expressionStack[i + 1]; |
| if (curr->is<Rethrow>()) { |
| return child != tryy->body; |
| } |
| assert(curr->is<Try>()); |
| return child == tryy->body; |
| } |
| } |
| if (i == 0) { |
| // We never found our try. |
| return false; |
| } |
| i--; |
| } |
| } |
| } fixer(wasm, *this); |
| fixer.walk(func->body); |
| |
| // Refinalize at the end, after labels are all fixed up. |
| ReFinalize().walkFunctionInModule(func, &wasm); |
| } |
| |
| void TranslateToFuzzReader::modifyInitialFunctions() { |
| if (wasm.functions.empty()) { |
| return; |
| } |
| // Do not iterate directly on wasm.functions itself (that is, avoid |
| // for (x : wasm.functions) |
| // ) as we may add to it as we go through the functions - make() can add new |
| // functions to implement a RefFunc. Instead, use an index. This avoids an |
| // iterator invalidation, and also we will process those new functions at |
| // 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 (or, if we are |
| // preserving imports, keep them all). |
| if (func->imported() && |
| (func->module == "fuzzing-support" || preserveImportsAndExports)) { |
| continue; |
| } |
| if (func->imported()) { |
| FunctionCreationContext context(*this, func); |
| func->module = func->base = Name(); |
| func->type = func->type.with(Exact); |
| func->body = make(func->getResults()); |
| } |
| } |
| |
| // Remove a start function - the fuzzing harness expects code to run only |
| // from exports. |
| wasm.start = Name(); |
| } |
| |
| void TranslateToFuzzReader::dropToLog(Function* func) { |
| // Don't always do this. |
| if (oneIn(2)) { |
| return; |
| } |
| struct Modder : public PostWalker<Modder> { |
| Module& wasm; |
| TranslateToFuzzReader& parent; |
| |
| Modder(Module& wasm, TranslateToFuzzReader& parent) |
| : wasm(wasm), parent(parent) {} |
| |
| void visitDrop(Drop* curr) { |
| if (parent.isLoggableType(curr->value->type) && parent.oneIn(2)) { |
| auto target = parent.logImportNames[curr->value->type]; |
| replaceCurrent( |
| parent.builder.makeCall(target, {curr->value}, Type::none)); |
| } |
| } |
| }; |
| Modder modder(wasm, *this); |
| modder.walk(func->body); |
| } |
| |
| void TranslateToFuzzReader::addInvocations(Function* func) { |
| Name name = func->name.toString() + std::string("_invoker"); |
| if (wasm.getFunctionOrNull(name) || wasm.getExportOrNull(name)) { |
| return; |
| } |
| auto invoker = |
| builder.makeFunction(name, Type(Signature(), NonNullable, Exact), {}); |
| Block* body = builder.makeBlock(); |
| invoker->body = body; |
| FunctionCreationContext context(*this, invoker.get()); |
| std::vector<Expression*> invocations; |
| while (oneIn(2) && !random.finished()) { |
| std::vector<Expression*> args; |
| for (const auto& type : func->getParams()) { |
| args.push_back(makeConst(type)); |
| } |
| Expression* invoke = builder.makeCall(func->name, args, func->getResults()); |
| if (func->getResults().isConcrete()) { |
| invoke = builder.makeDrop(invoke); |
| } |
| invocations.push_back(invoke); |
| // log out memory in some cases |
| if (hashMemoryName && oneIn(2)) { |
| invocations.push_back(makeMemoryHashLogging()); |
| } |
| } |
| if (invocations.empty()) { |
| return; |
| } |
| body->list.set(invocations); |
| wasm.addFunction(std::move(invoker)); |
| |
| // Most of the benefit of invocations is lost when we do not add exports for |
| // them, but still, they might be called by existing functions. |
| if (!preserveImportsAndExports) { |
| wasm.addExport(builder.makeExport(name, name, ExternalKind::Function)); |
| } |
| } |
| |
| Expression* TranslateToFuzzReader::make(Type type) { |
| type = getSubType(type); |
| if (trivialNesting) { |
| // We are nested under a makeTrivial call, so only emit something trivial. |
| return makeTrivial(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))) { |
| if (type.isConcrete()) { |
| if (!funcContext || oneIn(2)) { |
| return makeConst(type); |
| } else { |
| return makeLocalGet(type); |
| } |
| } else if (type == Type::none) { |
| assert(funcContext); |
| if (oneIn(2)) { |
| return makeNop(type); |
| } else { |
| return makeLocalSet(type); |
| } |
| } |
| assert(type == Type::unreachable); |
| return makeTrivial(type); |
| } |
| nesting++; |
| Expression* ret = nullptr; |
| if (!funcContext) { |
| // We are in a global init or similar, and can only emit constants. |
| ret = makeConst(type); |
| } else if (type.isConcrete()) { |
| ret = _makeConcrete(type); |
| } else if (type == Type::none) { |
| ret = _makenone(); |
| } else { |
| assert(type == Type::unreachable); |
| ret = _makeunreachable(); |
| } |
| if (!Type::isSubType(ret->type, type)) { |
| Fatal() << "Did not generate the right subtype of " |
| << ModuleType(wasm, type) << ", instead we have " |
| << ModuleType(wasm, ret->type) << " : " |
| << ModuleExpression(wasm, ret) << '\n'; |
| } |
| nesting--; |
| return ret; |
| } |
| |
| Expression* TranslateToFuzzReader::_makeConcrete(Type type) { |
| bool canMakeControlFlow = !type.isTuple() || wasm.features.hasMultivalue(); |
| using Self = TranslateToFuzzReader; |
| FeatureOptions<Expression* (Self::*)(Type)> options; |
| using WeightedOption = decltype(options)::WeightedOption; |
| options.add(FeatureSet::MVP, |
| WeightedOption{&Self::makeLocalGet, VeryImportant}, |
| WeightedOption{&Self::makeLocalSet, VeryImportant}, |
| WeightedOption{&Self::makeGlobalGet, Important}, |
| WeightedOption{&Self::makeConst, Important}); |
| if (canMakeControlFlow) { |
| options |
| .add(FeatureSet::MVP, |
| WeightedOption{&Self::makeBlock, Important}, |
| WeightedOption{&Self::makeIf, Important}, |
| WeightedOption{&Self::makeLoop, Important}, |
| WeightedOption{&Self::makeBreak, Important}, |
| &Self::makeCall, |
| &Self::makeCallIndirect) |
| .add(FeatureSet::ExceptionHandling, &Self::makeTry) |
| .add(FeatureSet::ExceptionHandling, &Self::makeTryTable) |
| .add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeCallRef) |
| .add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeBrOn); |
| } |
| if (type.isSingle()) { |
| options |
| .add(FeatureSet::MVP, |
| WeightedOption{&Self::makeUnary, Important}, |
| WeightedOption{&Self::makeBinary, Important}, |
| &Self::makeSelect) |
| .add(FeatureSet::Multivalue, &Self::makeTupleExtract); |
| } |
| if (type.isSingle() && !type.isRef()) { |
| options.add(FeatureSet::MVP, {&Self::makeLoad, Important}); |
| options.add(FeatureSet::SIMD, &Self::makeSIMD); |
| } |
| if (type.isInteger()) { |
| options.add(FeatureSet::Atomics, &Self::makeAtomic); |
| } |
| if (type == Type::i32) { |
| if (callExportCatchImportName || callRefCatchImportName) { |
| options.add(FeatureSet::MVP, &Self::makeImportCallCode); |
| } |
| if (sleepImportName) { |
| options.add(FeatureSet::MVP, &Self::makeImportSleep); |
| } |
| options.add(FeatureSet::ReferenceTypes, &Self::makeRefIsNull); |
| options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| // Prioritize ref.eq heavily as it is the one instruction that |
| // tests reference identity. |
| {&Self::makeRefEq, VeryImportant}, |
| &Self::makeRefTest, |
| &Self::makeI31Get); |
| options.add(FeatureSet::ReferenceTypes | FeatureSet::GC | |
| FeatureSet::Strings, |
| &Self::makeStringEncode, |
| &Self::makeStringEq, |
| &Self::makeStringMeasure, |
| &Self::makeStringGet); |
| } |
| if (type.isTuple()) { |
| options.add(FeatureSet::Multivalue, &Self::makeTupleMake); |
| } |
| if (type.isRef()) { |
| auto heapType = type.getHeapType(); |
| if (heapType.isBasic()) { |
| options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| &Self::makeBasicRef); |
| if (type.isNullable() && funcContext) { |
| if (heapType == HeapType::func) { |
| options.add(FeatureSet::ReferenceTypes, &Self::makeTableGet); |
| } |
| if (heapType == HeapType::exn) { |
| options.add(FeatureSet::ExceptionHandling, &Self::makeTableGet); |
| } |
| } |
| } else { |
| options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| &Self::makeCompoundRef); |
| } |
| if (type.isCastable()) { |
| // Exact casts are only allowed with custom descriptors enabled. |
| if (type.isInexact() || wasm.features.hasCustomDescriptors()) { |
| options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| &Self::makeRefCast); |
| } |
| } |
| if (heapType.getDescribedType()) { |
| options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| &Self::makeRefGetDesc); |
| } |
| } |
| if (wasm.features.hasGC()) { |
| if (typeStructFields.find(type) != typeStructFields.end()) { |
| options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| &Self::makeStructGet); |
| } |
| if (typeArrays.find(type) != typeArrays.end()) { |
| options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| &Self::makeArrayGet); |
| } |
| } |
| return (this->*pick(options))(type); |
| } |
| |
| Expression* TranslateToFuzzReader::_makenone() { |
| auto choice = upTo(100); |
| if (choice < LOGGING_PERCENT) { |
| if (!hashMemoryName || choice < LOGGING_PERCENT / 2) { |
| return makeImportLogging(); |
| } else { |
| return makeMemoryHashLogging(); |
| } |
| } |
| using Self = TranslateToFuzzReader; |
| auto options = FeatureOptions<Expression* (Self::*)(Type)>(); |
| using WeightedOption = decltype(options)::WeightedOption; |
| options |
| .add(FeatureSet::MVP, |
| WeightedOption{&Self::makeLocalSet, VeryImportant}, |
| WeightedOption{&Self::makeBlock, Important}, |
| WeightedOption{&Self::makeIf, Important}, |
| WeightedOption{&Self::makeLoop, Important}, |
| WeightedOption{&Self::makeBreak, Important}, |
| WeightedOption{&Self::makeStore, Important}, |
| &Self::makeCall, |
| &Self::makeCallIndirect, |
| &Self::makeDrop, |
| &Self::makeNop, |
| &Self::makeGlobalSet) |
| .add(FeatureSet::BulkMemory, &Self::makeBulkMemory) |
| .add(FeatureSet::Atomics, &Self::makeAtomic) |
| .add(FeatureSet::ReferenceTypes, &Self::makeTableSet) |
| .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) |
| .add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeBrOn) |
| .add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| &Self::makeArrayBulkMemoryOp); |
| if (tableSetImportName) { |
| options.add(FeatureSet::ReferenceTypes, &Self::makeImportTableSet); |
| } |
| if (callExportImportName || callRefImportName) { |
| options.add(FeatureSet::MVP, &Self::makeImportCallCode); |
| } |
| return (this->*pick(options))(Type::none); |
| } |
| |
| Expression* TranslateToFuzzReader::_makeunreachable() { |
| using Self = TranslateToFuzzReader; |
| auto options = FeatureOptions<Expression* (Self::*)(Type)>(); |
| using WeightedOption = decltype(options)::WeightedOption; |
| // Many instructions can become unreachable if a child is unreachable. We |
| // create such code in mutate() (see |allowUnreachable| there). The list of |
| // instructions here are those that necessarily have unreachable type, and are |
| // only created here (though they might have other variations that are |
| // reachable, like br has br_if that is created elsewhere, and we have call |
| // here because of return calls, etc.). |
| options |
| .add(FeatureSet::MVP, |
| WeightedOption{&Self::makeBreak, Important}, |
| &Self::makeUnreachable, |
| &Self::makeCall, |
| &Self::makeCallIndirect, |
| &Self::makeSwitch, |
| &Self::makeReturn) |
| .add(FeatureSet::ExceptionHandling, &Self::makeThrow, &Self::makeThrowRef) |
| .add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeCallRef); |
| return (this->*pick(options))(Type::unreachable); |
| } |
| |
| Expression* TranslateToFuzzReader::makeTrivial(Type type) { |
| struct TrivialNester { |
| TranslateToFuzzReader& parent; |
| TrivialNester(TranslateToFuzzReader& parent) : parent(parent) { |
| parent.trivialNesting++; |
| } |
| ~TrivialNester() { parent.trivialNesting--; } |
| } nester(*this); |
| |
| if (type.isConcrete()) { |
| // If we have a function context, use a local half the time. Use a local |
| // less often if the local is non-nullable, however, as then we might be |
| // using it before it was set, which would trap. |
| if (funcContext && oneIn(type.isNonNullable() ? 10 : 2)) { |
| return makeLocalGet(type); |
| } |
| |
| // Either make a const, or a global.get (which may fail to find a suitable |
| // global, especially in a non-function context, and if so it will make a |
| // constant instead). |
| return oneIn(2) ? makeConst(type) : makeGlobalGet(type); |
| } else if (type == Type::none) { |
| return makeNop(type); |
| } |
| assert(type == Type::unreachable); |
| Expression* ret = nullptr; |
| if (funcContext->func->getResults().isConcrete()) { |
| ret = makeTrivial(funcContext->func->getResults()); |
| } |
| return builder.makeReturn(ret); |
| } |
| |
| Expression* TranslateToFuzzReader::makeBlock(Type type) { |
| auto* ret = builder.makeBlock(); |
| 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) { |
| // smaller blocks past the limit |
| num /= 2; |
| if (nesting >= fuzzParams->NESTING_LIMIT && oneIn(2)) { |
| // smaller blocks past the limit |
| num /= 2; |
| } |
| } |
| // not likely to have a block of size 1 |
| if (num == 0 && !oneIn(10)) { |
| num++; |
| } |
| while (num > 0 && !random.finished()) { |
| ret->list.push_back(make(Type::none)); |
| num--; |
| } |
| // give a chance to make the final element an unreachable break, instead |
| // of concrete - a common pattern (branch to the top of a loop etc.) |
| if (!random.finished() && type.isConcrete() && oneIn(2)) { |
| ret->list.push_back(makeBreak(Type::unreachable)); |
| } else { |
| ret->list.push_back(make(type)); |
| } |
| funcContext->breakableStack.pop_back(); |
| if (type.isConcrete()) { |
| ret->finalize(type); |
| } else { |
| ret->finalize(); |
| } |
| if (ret->type != type) { |
| // e.g. we might want an unreachable block, but a child breaks to it |
| assert(type == Type::unreachable && ret->type == Type::none); |
| return builder.makeSequence(ret, make(Type::unreachable)); |
| } |
| return ret; |
| } |
| |
| Expression* TranslateToFuzzReader::makeLoop(Type type) { |
| auto* ret = wasm.allocator.alloc<Loop>(); |
| ret->type = type; // So we have it during child creation |
| ret->name = makeLabel(); |
| funcContext->breakableStack.push_back(ret); |
| funcContext->hangStack.push_back(ret); |
| // Either create random content, or do something more targeted |
| if (oneIn(2)) { |
| ret->body = makeMaybeBlock(type); |
| } else { |
| // Ensure a branch back. Also optionally create some loop vars. |
| std::vector<Expression*> list; |
| list.push_back(makeMaybeBlock(Type::none)); // Primary contents |
| // Possible branch back |
| list.push_back(builder.makeBreak(ret->name, nullptr, makeCondition())); |
| list.push_back(make(type)); // Final element, so we have the right type |
| ret->body = builder.makeBlock(list, type); |
| } |
| funcContext->breakableStack.pop_back(); |
| funcContext->hangStack.pop_back(); |
| ret->finalize(type); |
| return ret; |
| } |
| |
| Expression* TranslateToFuzzReader::makeCondition() { |
| // We want a 50-50 chance for the condition to be taken, for interesting |
| // execution paths. by itself, there is bias (e.g. most consts are "yes") so |
| // even that out with noise |
| auto* ret = make(Type::i32); |
| if (oneIn(2)) { |
| ret = builder.makeUnary(UnaryOp::EqZInt32, ret); |
| } |
| return ret; |
| } |
| |
| Expression* TranslateToFuzzReader::makeMaybeBlock(Type type) { |
| // if past the limit, prefer not to emit blocks |
| if (nesting >= fuzzParams->NESTING_LIMIT || oneIn(3)) { |
| return make(type); |
| } else { |
| return makeBlock(type); |
| } |
| } |
| |
| Expression* TranslateToFuzzReader::buildIf(const struct ThreeArgs& args, |
| Type type) { |
| return builder.makeIf(args.a, args.b, args.c, type); |
| } |
| |
| Expression* TranslateToFuzzReader::makeIf(Type type) { |
| auto* condition = makeCondition(); |
| funcContext->hangStack.push_back(nullptr); |
| |
| Expression* ret; |
| if (type == Type::none && oneIn(2)) { |
| // Just an ifTrue arm. |
| ret = buildIf({condition, makeMaybeBlock(type), nullptr}, type); |
| } else { |
| // Also an ifFalse arm. |
| |
| // Some of the time make one arm unreachable (but not both, as then the if |
| // as a whole would be unreachable). |
| auto trueType = type; |
| auto falseType = type; |
| switch (upTo(20)) { |
| case 0: |
| trueType = Type::unreachable; |
| break; |
| case 1: |
| falseType = Type::unreachable; |
| break; |
| } |
| ret = buildIf( |
| {condition, makeMaybeBlock(trueType), makeMaybeBlock(falseType)}, type); |
| } |
| |
| funcContext->hangStack.pop_back(); |
| return ret; |
| } |
| |
| Expression* TranslateToFuzzReader::makeTry(Type type) { |
| auto* body = make(type); |
| std::vector<Name> catchTags; |
| std::vector<Expression*> catchBodies; |
| auto numTags = upTo(fuzzParams->MAX_TRY_CATCHES); |
| std::unordered_set<Tag*> usedTags; |
| for (Index i = 0; i < numTags; i++) { |
| if (exceptionTags.empty()) { |
| addTag(); |
| } |
| auto* tag = pick(exceptionTags); |
| if (usedTags.count(tag)) { |
| continue; |
| } |
| usedTags.insert(tag); |
| catchTags.push_back(tag->name); |
| } |
| // The number of tags in practice may be fewer than we planned. |
| numTags = catchTags.size(); |
| auto numCatches = numTags; |
| if (numTags == 0 || oneIn(2)) { |
| // Add a catch-all. |
| numCatches++; |
| } |
| for (Index i = 0; i < numCatches; i++) { |
| // Catch bodies (aside from a catch-all) begin with a pop. |
| Expression* prefix = nullptr; |
| if (i < numTags) { |
| auto tagType = wasm.getTag(catchTags[i])->params(); |
| if (tagType != Type::none) { |
| auto* pop = builder.makePop(tagType); |
| // Capture the pop in a local, so that it can be used later. |
| // TODO: add a good chance for using this particular local in this catch |
| // TODO: reuse an existing var if there is one |
| auto index = builder.addVar(funcContext->func, tagType); |
| prefix = builder.makeLocalSet(index, pop); |
| } |
| } |
| auto* catchBody = make(type); |
| if (prefix) { |
| catchBody = builder.makeSequence(prefix, catchBody); |
| } |
| catchBodies.push_back(catchBody); |
| } |
| // TODO: delegate stuff |
| return builder.makeTry(body, catchTags, catchBodies); |
| } |
| |
| Expression* TranslateToFuzzReader::makeTryTable(Type type) { |
| auto* body = make(type); |
| |
| if (funcContext->breakableStack.empty()) { |
| // Nothing to break to, emit a trivial TryTable. |
| // TODO: Perhaps generate a block wrapping us? |
| return builder.makeTryTable(body, {}, {}, {}); |
| } |
| |
| if (exceptionTags.empty()) { |
| addTag(); |
| } |
| |
| // Add catches of specific tags, and possibly a catch_all at the end. We use |
| // the last iteration of the loop for that. |
| std::vector<Name> catchTags; |
| std::vector<Name> catchDests; |
| std::vector<bool> catchRefs; |
| auto numCatches = upTo(fuzzParams->MAX_TRY_CATCHES); |
| for (Index i = 0; i <= numCatches; i++) { |
| Name tagName; |
| Type tagType; |
| if (i < numCatches) { |
| // Look for a specific tag. |
| auto* tag = pick(exceptionTags); |
| tagName = tag->name; |
| tagType = tag->params(); |
| } else { |
| // Add a catch_all at the end, some of the time (but all of the time if we |
| // have nothing else). |
| if (!catchTags.empty() && oneIn(2)) { |
| break; |
| } |
| tagType = Type::none; |
| } |
| |
| // We need to find a proper target to break to, which means a target that |
| // has the type of the tag, or the tag + an exnref at the end. |
| std::vector<Type> vec(tagType.begin(), tagType.end()); |
| // Use a non-nullable exnref here, and then the subtyping check below will |
| // also accept a target that is nullable. |
| vec.push_back(Type(HeapType::exn, NonNullable)); |
| auto tagTypeWithExn = Type(vec); |
| int tries = fuzzParams->TRIES; |
| while (tries-- > 0) { |
| auto* target = pick(funcContext->breakableStack); |
| auto dest = getTargetName(target); |
| auto valueType = getTargetType(target); |
| auto subOfTagType = Type::isSubType(tagType, valueType); |
| auto subOfTagTypeWithExn = Type::isSubType(tagTypeWithExn, valueType); |
| if (subOfTagType || subOfTagTypeWithExn) { |
| catchTags.push_back(tagName); |
| catchDests.push_back(dest); |
| catchRefs.push_back(subOfTagTypeWithExn); |
| break; |
| } |
| } |
| // TODO: Perhaps generate a block wrapping us, if we fail to find a target? |
| // TODO: It takes a bit of luck to find a target with an exnref - perhaps |
| // generate those? |
| } |
| |
| return builder.makeTryTable(body, catchTags, catchDests, catchRefs); |
| } |
| |
| Expression* TranslateToFuzzReader::makeBreak(Type type) { |
| if (funcContext->breakableStack.empty()) { |
| return makeTrivial(type); |
| } |
| Expression* condition = nullptr; |
| if (type != Type::unreachable) { |
| funcContext->hangStack.push_back(nullptr); |
| condition = makeCondition(); |
| } |
| // we need to find a proper target to break to; try a few times |
| int tries = fuzzParams->TRIES; |
| while (tries-- > 0) { |
| auto* target = pick(funcContext->breakableStack); |
| auto name = getTargetName(target); |
| auto valueType = getTargetType(target); |
| if (type.isConcrete()) { |
| // we are flowing out a value |
| if (valueType != type) { |
| // we need to break to a proper place |
| continue; |
| } |
| auto* ret = builder.makeBreak(name, make(type), condition); |
| funcContext->hangStack.pop_back(); |
| return ret; |
| } else if (type == Type::none) { |
| if (valueType != Type::none) { |
| // we need to break to a proper place |
| continue; |
| } |
| auto* ret = builder.makeBreak(name, nullptr, condition); |
| funcContext->hangStack.pop_back(); |
| return ret; |
| } else { |
| assert(type == Type::unreachable); |
| if (valueType != Type::none) { |
| // we need to break to a proper place |
| continue; |
| } |
| // we are about to make an *un*conditional break. if it is |
| // to a loop, we prefer there to be a condition along the |
| // way, to reduce the chance of infinite looping |
| size_t conditions = 0; |
| int i = funcContext->hangStack.size(); |
| while (--i >= 0) { |
| auto* item = funcContext->hangStack[i]; |
| if (item == nullptr) { |
| conditions++; |
| } else if (auto* loop = item->cast<Loop>()) { |
| if (loop->name == name) { |
| // we found the target, no more conditions matter |
| break; |
| } |
| } |
| } |
| switch (conditions) { |
| case 0: { |
| if (!oneIn(4)) { |
| continue; |
| } |
| break; |
| } |
| case 1: { |
| if (!oneIn(2)) { |
| continue; |
| } |
| break; |
| } |
| default: { |
| if (oneIn(conditions + 1)) { |
| continue; |
| } |
| } |
| } |
| return builder.makeBreak(name); |
| } |
| } |
| // we failed to find something |
| if (type != Type::unreachable) { |
| funcContext->hangStack.pop_back(); |
| } |
| return makeTrivial(type); |
| } |
| |
| Expression* TranslateToFuzzReader::makeCall(Type type) { |
| int tries = fuzzParams->TRIES; |
| bool isReturn; |
| while (tries-- > 0) { |
| Function* target = funcContext->func; |
| if (!wasm.functions.empty() && !oneIn(wasm.functions.size())) { |
| target = pick(wasm.functions).get(); |
| } |
| isReturn = type == Type::unreachable && wasm.features.hasTailCall() && |
| funcContext->func->getResults() == target->getResults(); |
| if (target->getResults() != type && !isReturn) { |
| continue; |
| } |
| // we found one! |
| std::vector<Expression*> args; |
| for (const auto& argType : target->getParams()) { |
| args.push_back(make(argType)); |
| } |
| return builder.makeCall(target->name, args, type, isReturn); |
| } |
| // we failed to find something |
| return makeTrivial(type); |
| } |
| |
| Expression* TranslateToFuzzReader::makeCallIndirect(Type type) { |
| auto& randomElem = wasm.elementSegments[upTo(wasm.elementSegments.size())]; |
| auto& data = randomElem->data; |
| if (data.empty()) { |
| return make(type); |
| } |
| // look for a call target with the right type |
| Index start = upTo(data.size()); |
| Index i = start; |
| Function* targetFn; |
| bool isReturn; |
| while (1) { |
| // TODO: handle unreachable |
| if (auto* get = data[i]->dynCast<RefFunc>()) { |
| targetFn = wasm.getFunction(get->func); |
| isReturn = type == Type::unreachable && wasm.features.hasTailCall() && |
| funcContext->func->getResults() == targetFn->getResults(); |
| if (targetFn->getResults() == type || isReturn) { |
| break; |
| } |
| } |
| i++; |
| if (i == data.size()) { |
| i = 0; |
| } |
| if (i == start) { |
| return makeTrivial(type); |
| } |
| } |
| // with high probability, make sure the type is valid - otherwise, most are |
| // going to trap |
| auto addressType = wasm.getTable(funcrefTableName)->addressType; |
| Expression* target; |
| if (!allowOOB || !oneIn(10)) { |
| target = builder.makeConst(Literal::makeFromInt32(i, addressType)); |
| } else { |
| target = make(addressType); |
| } |
| std::vector<Expression*> args; |
| for (const auto& type : targetFn->getParams()) { |
| args.push_back(make(type)); |
| } |
| // TODO: use a random table |
| return builder.makeCallIndirect( |
| funcrefTableName, target, args, targetFn->type.getHeapType(), isReturn); |
| } |
| |
| Expression* TranslateToFuzzReader::makeCallRef(Type type) { |
| // look for a call target with the right type |
| Function* target; |
| bool isReturn; |
| decltype(fuzzParams->TRIES) i = 0; |
| while (1) { |
| if (i == fuzzParams->TRIES || wasm.functions.empty()) { |
| // We can't find a proper target, give up. |
| return makeTrivial(type); |
| } |
| // TODO: handle unreachable |
| target = wasm.functions[upTo(wasm.functions.size())].get(); |
| isReturn = type == Type::unreachable && wasm.features.hasTailCall() && |
| funcContext->func->getResults() == target->getResults(); |
| if (target->getResults() == type || isReturn) { |
| break; |
| } |
| i++; |
| } |
| std::vector<Expression*> args; |
| for (const auto& type : target->getParams()) { |
| args.push_back(make(type)); |
| } |
| // TODO: half the time make a completely random item with that type. |
| return builder.makeCallRef( |
| builder.makeRefFunc(target->name), args, type, isReturn); |
| } |
| |
| Expression* TranslateToFuzzReader::makeLocalGet(Type type) { |
| auto& locals = funcContext->typeLocals[type]; |
| if (!locals.empty()) { |
| return builder.makeLocalGet(pick(locals), type); |
| } |
| // No existing local. When we want something trivial, just give up and emit a |
| // constant. |
| if (trivialNesting) { |
| return makeConst(type); |
| } |
| |
| // Otherwise, we have 3 cases: a const, as above (we do this randomly some of |
| // the time), or emit a local.get of a new local, or emit a local.tee of a new |
| // local. |
| auto choice = upTo(3); |
| if (choice == 0 || !TypeUpdating::canHandleAsLocal(type)) { |
| return makeConst(type); |
| } |
| // Otherwise, add a new local. If the type is not non-nullable then we may |
| // just emit a get for it (which, as this is a brand-new local, will read the |
| // default value, unless we are in a loop; for that reason for a non- |
| // nullable local we prefer a tee later down.). |
| auto index = builder.addVar(funcContext->func, type); |
| LocalSet* tee = nullptr; |
| if (choice == 1 || type.isNonNullable()) { |
| // Create the tee here before adding the local to typeLocals (or else we |
| // might end up using it prematurely inside the make() call). |
| tee = builder.makeLocalTee(index, make(type), type); |
| } |
| funcContext->typeLocals[type].push_back(index); |
| if (tee) { |
| return tee; |
| } |
| return builder.makeLocalGet(index, type); |
| } |
| |
| Expression* TranslateToFuzzReader::makeLocalSet(Type type) { |
| bool tee = type != Type::none; |
| Type valueType; |
| if (tee) { |
| valueType = type; |
| } else { |
| valueType = getConcreteType(); |
| } |
| auto& locals = funcContext->typeLocals[valueType]; |
| if (locals.empty()) { |
| return makeTrivial(type); |
| } |
| auto* value = make(valueType); |
| if (tee) { |
| return builder.makeLocalTee(pick(locals), value, valueType); |
| } else { |
| return builder.makeLocalSet(pick(locals), value); |
| } |
| } |
| |
| Expression* TranslateToFuzzReader::makeGlobalGet(Type type) { |
| // In a non-function context, like in another global, we can only get from an |
| // immutable global. Whether GC is enabled also matters, as it allows getting |
| // from a non-import. |
| auto& relevantGlobals = |
| funcContext ? globalsByType |
| : (wasm.features.hasGC() ? immutableGlobalsByType |
| : importedImmutableGlobalsByType); |
| auto it = relevantGlobals.find(type); |
| // If we have no such relevant globals give up and emit a constant instead. |
| if (it == relevantGlobals.end() || it->second.empty()) { |
| return makeConst(type); |
| } |
| |
| auto name = pick(it->second); |
| // We don't want random fuzz code to use the hang limit global. |
| assert(name != HANG_LIMIT_GLOBAL); |
| return builder.makeGlobalGet(name, type); |
| } |
| |
| Expression* TranslateToFuzzReader::makeGlobalSet(Type type) { |
| assert(type == Type::none); |
| type = getConcreteType(); |
| auto it = mutableGlobalsByType.find(type); |
| if (it == mutableGlobalsByType.end() || it->second.empty()) { |
| return makeTrivial(Type::none); |
| } |
| |
| auto name = pick(it->second); |
| // We don't want random fuzz code to use the hang limit global. |
| assert(name != HANG_LIMIT_GLOBAL); |
| return builder.makeGlobalSet(name, make(type)); |
| } |
| |
| Expression* TranslateToFuzzReader::makeTupleMake(Type type) { |
| assert(wasm.features.hasMultivalue()); |
| assert(type.isTuple()); |
| std::vector<Expression*> elements; |
| for (const auto& t : type) { |
| elements.push_back(make(t)); |
| } |
| return builder.makeTupleMake(std::move(elements)); |
| } |
| |
| Expression* TranslateToFuzzReader::makeTupleExtract(Type type) { |
| // Tuples can require locals in binary format conversions. |
| if (!type.isDefaultable()) { |
| return makeTrivial(type); |
| } |
| assert(wasm.features.hasMultivalue()); |
| assert(type.isSingle() && type.isConcrete()); |
| Type tupleType = getTupleType(); |
| |
| // Find indices from which we can extract `type` |
| std::vector<size_t> extractIndices; |
| size_t i = 0; |
| for (const auto& t : tupleType) { |
| if (t == type) { |
| extractIndices.push_back(i); |
| } |
| ++i; |
| } |
| |
| // If there are none, inject one |
| if (extractIndices.size() == 0) { |
| std::vector<Type> newElements(tupleType.begin(), tupleType.end()); |
| size_t injected = upTo(newElements.size()); |
| newElements[injected] = type; |
| tupleType = Type(newElements); |
| extractIndices.push_back(injected); |
| } |
| |
| Index index = pick(extractIndices); |
| Expression* child = make(tupleType); |
| return builder.makeTupleExtract(child, index); |
| } |
| |
| Expression* TranslateToFuzzReader::makePointer() { |
| auto* ret = make(wasm.memories[0]->addressType); |
| // with high probability, mask the pointer so it's in a reasonable |
| // range. otherwise, most pointers are going to be out of range and |
| // most memory ops will just trap |
| if (!allowOOB || !oneIn(10)) { |
| if (wasm.memories[0]->is64()) { |
| ret = builder.makeBinary( |
| AndInt64, |
| ret, |
| builder.makeConst(int64_t(fuzzParams->USABLE_MEMORY - 1))); |
| } else { |
| ret = builder.makeBinary( |
| AndInt32, |
| ret, |
| builder.makeConst(int32_t(fuzzParams->USABLE_MEMORY - 1))); |
| } |
| } |
| return ret; |
| } |
| |
| Expression* TranslateToFuzzReader::makeNonAtomicLoad(Type type) { |
| auto offset = logify(get()); |
| auto ptr = makePointer(); |
| switch (type.getBasic()) { |
| case Type::i32: { |
| bool signed_ = get() & 1; |
| switch (upTo(3)) { |
| case 0: |
| return builder.makeLoad( |
| 1, signed_, offset, 1, ptr, type, wasm.memories[0]->name); |
| case 1: |
| return builder.makeLoad( |
| 2, signed_, offset, pick(1, 2), ptr, type, wasm.memories[0]->name); |
| case 2: |
| return builder.makeLoad(4, |
| signed_, |
| offset, |
| pick(1, 2, 4), |
| ptr, |
| type, |
| wasm.memories[0]->name); |
| } |
| WASM_UNREACHABLE("unexpected value"); |
| } |
| case Type::i64: { |
| bool signed_ = get() & 1; |
| switch (upTo(4)) { |
| case 0: |
| return builder.makeLoad( |
| 1, signed_, offset, 1, ptr, type, wasm.memories[0]->name); |
| case 1: |
| return builder.makeLoad( |
| 2, signed_, offset, pick(1, 2), ptr, type, wasm.memories[0]->name); |
| case 2: |
| return builder.makeLoad(4, |
| signed_, |
| offset, |
| pick(1, 2, 4), |
| ptr, |
| type, |
| wasm.memories[0]->name); |
| case 3: |
| return builder.makeLoad(8, |
| signed_, |
| offset, |
| pick(1, 2, 4, 8), |
| ptr, |
| type, |
| wasm.memories[0]->name); |
| } |
| WASM_UNREACHABLE("unexpected value"); |
| } |
| case Type::f32: { |
| return builder.makeLoad( |
| 4, false, offset, pick(1, 2, 4), ptr, type, wasm.memories[0]->name); |
| } |
| case Type::f64: { |
| return builder.makeLoad( |
| 8, false, offset, pick(1, 2, 4, 8), ptr, type, wasm.memories[0]->name); |
| } |
| case Type::v128: { |
| if (!wasm.features.hasSIMD()) { |
| return makeTrivial(type); |
| } |
| return builder.makeLoad(16, |
| false, |
| offset, |
| pick(1, 2, 4, 8, 16), |
| ptr, |
| type, |
| wasm.memories[0]->name); |
| } |
| case Type::none: |
| case Type::unreachable: |
| WASM_UNREACHABLE("invalid type"); |
| } |
| WASM_UNREACHABLE("invalid type"); |
| } |
| |
| Expression* TranslateToFuzzReader::makeLoad(Type type) { |
| // reference types cannot be stored in memory |
| if (!allowMemory || type.isRef()) { |
| return makeTrivial(type); |
| } |
| auto* ret = makeNonAtomicLoad(type); |
| if (type != Type::i32 && type != Type::i64) { |
| return ret; |
| } |
| if (!wasm.features.hasAtomics() || oneIn(2)) { |
| return ret; |
| } |
| // make it atomic |
| auto* load = ret->cast<Load>(); |
| wasm.memories[0]->shared = true; |
| load->isAtomic = true; |
| load->signed_ = false; |
| load->align = load->bytes; |
| return load; |
| } |
| |
| Expression* TranslateToFuzzReader::makeNonAtomicStore(Type type) { |
| if (type == Type::unreachable) { |
| // make a normal store, then make it unreachable |
| auto* ret = makeNonAtomicStore(getStorableType()); |
| auto* store = ret->dynCast<Store>(); |
| if (!store) { |
| return ret; |
| } |
| switch (upTo(3)) { |
| case 0: |
| store->ptr = make(Type::unreachable); |
| break; |
| case 1: |
| store->value = make(Type::unreachable); |
| break; |
| case 2: |
| store->ptr = make(Type::unreachable); |
| store->value = make(Type::unreachable); |
| break; |
| } |
| store->memory = wasm.memories[0]->name; |
| store->finalize(); |
| return store; |
| } |
| // the type is none or unreachable. we also need to pick the value |
| // type. |
| if (type == Type::none) { |
| type = getStorableType(); |
| } |
| auto offset = logify(get()); |
| auto ptr = makePointer(); |
| auto value = make(type); |
| switch (type.getBasic()) { |
| case Type::i32: { |
| switch (upTo(3)) { |
| case 0: |
| return builder.makeStore( |
| 1, offset, 1, ptr, value, type, wasm.memories[0]->name); |
| case 1: |
| return builder.makeStore( |
| 2, offset, pick(1, 2), ptr, value, type, wasm.memories[0]->name); |
| case 2: |
| return builder.makeStore( |
| 4, offset, pick(1, 2, 4), ptr, value, type, wasm.memories[0]->name); |
| } |
| WASM_UNREACHABLE("invalid value"); |
| } |
| case Type::i64: { |
| switch (upTo(4)) { |
| case 0: |
| return builder.makeStore( |
| 1, offset, 1, ptr, value, type, wasm.memories[0]->name); |
| case 1: |
| return builder.makeStore( |
| 2, offset, pick(1, 2), ptr, value, type, wasm.memories[0]->name); |
| case 2: |
| return builder.makeStore( |
| 4, offset, pick(1, 2, 4), ptr, value, type, wasm.memories[0]->name); |
| case 3: |
| return builder.makeStore(8, |
| offset, |
| pick(1, 2, 4, 8), |
| ptr, |
| value, |
| type, |
| wasm.memories[0]->name); |
| } |
| WASM_UNREACHABLE("invalid value"); |
| } |
| case Type::f32: { |
| return builder.makeStore( |
| 4, offset, pick(1, 2, 4), ptr, value, type, wasm.memories[0]->name); |
| } |
| case Type::f64: { |
| return builder.makeStore( |
| 8, offset, pick(1, 2, 4, 8), ptr, value, type, wasm.memories[0]->name); |
| } |
| case Type::v128: { |
| if (!wasm.features.hasSIMD()) { |
| return makeTrivial(type); |
| } |
| return builder.makeStore(16, |
| offset, |
| pick(1, 2, 4, 8, 16), |
| ptr, |
| value, |
| type, |
| wasm.memories[0]->name); |
| } |
| case Type::none: |
| case Type::unreachable: |
| WASM_UNREACHABLE("invalid type"); |
| } |
| WASM_UNREACHABLE("invalid type"); |
| } |
| |
| Expression* TranslateToFuzzReader::makeStore(Type type) { |
| if (!allowMemory || type.isRef()) { |
| return makeTrivial(type); |
| } |
| auto* ret = makeNonAtomicStore(type); |
| auto* store = ret->dynCast<Store>(); |
| if (!store) { |
| return ret; |
| } |
| if (store->value->type != Type::i32 && store->value->type != Type::i64) { |
| return store; |
| } |
| if (!wasm.features.hasAtomics() || oneIn(2)) { |
| return store; |
| } |
| // make it atomic |
| wasm.memories[0]->shared = true; |
| store->isAtomic = true; |
| store->align = store->bytes; |
| return store; |
| } |
| |
| // Makes a small change to a constant value. |
| Literal TranslateToFuzzReader::tweak(Literal value) { |
| auto type = value.type; |
| if (type.isVector()) { |
| // TODO: tweak each lane? |
| return value; |
| } |
| // +- 1 |
| switch (upTo(5)) { |
| case 0: |
| value = value.add(Literal::makeNegOne(type)); |
| break; |
| case 1: |
| value = value.add(Literal::makeOne(type)); |
| break; |
| default: { |
| } |
| } |
| // For floats, optionally add a non-integer adjustment in +- [-1, 1] |
| if (type.isFloat() && oneIn(2)) { |
| const int RANGE = 1000; |
| auto RANGE_LITERAL = Literal::makeFromInt32(RANGE, type); |
| // adjustment -> [0, 2 * RANGE] |
| auto adjustment = Literal::makeFromInt32(upTo(2 * RANGE + 1), type); |
| // adjustment -> [-RANGE, RANGE] |
| adjustment = adjustment.sub(RANGE_LITERAL); |
| // adjustment -> [-1, 1] |
| adjustment = adjustment.div(RANGE_LITERAL); |
| value = value.add(adjustment); |
| } |
| // Flip sign. |
| if (oneIn(2)) { |
| value = value.mul(Literal::makeNegOne(type)); |
| } |
| return value; |
| } |
| |
| Literal TranslateToFuzzReader::makeLiteral(Type type) { |
| if (type == Type::v128) { |
| // generate each lane individually for random lane interpretation |
| switch (upTo(6)) { |
| case 0: |
| return Literal(std::array<Literal, 16>{{makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32)}}); |
| case 1: |
| return Literal(std::array<Literal, 8>{{makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32)}}); |
| case 2: |
| return Literal(std::array<Literal, 4>{{makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32), |
| makeLiteral(Type::i32)}}); |
| case 3: |
| return Literal(std::array<Literal, 2>{ |
| {makeLiteral(Type::i64), makeLiteral(Type::i64)}}); |
| case 4: |
| return Literal(std::array<Literal, 4>{{makeLiteral(Type::f32), |
| makeLiteral(Type::f32), |
| makeLiteral(Type::f32), |
| makeLiteral(Type::f32)}}); |
| case 5: |
| return Literal(std::array<Literal, 2>{ |
| {makeLiteral(Type::f64), makeLiteral(Type::f64)}}); |
| default: |
| WASM_UNREACHABLE("unexpected value"); |
| } |
| } |
| |
| switch (upTo(4)) { |
| case 0: { |
| // totally random, entire range |
| switch (type.getBasic()) { |
| case Type::i32: |
| return Literal(get32()); |
| case Type::i64: |
| return Literal(get64()); |
| case Type::f32: |
| return Literal(getFloat()); |
| case Type::f64: |
| return Literal(getDouble()); |
| case Type::v128: |
| case Type::none: |
| case Type::unreachable: |
| WASM_UNREACHABLE("invalid type"); |
| } |
| break; |
| } |
| case 1: { |
| // small range |
| int64_t small; |
| switch (upTo(6)) { |
| case 0: |
| small = int8_t(get()); |
| break; |
| case 1: |
| small = uint8_t(get()); |
| break; |
| case 2: |
| small = int16_t(get16()); |
| break; |
| case 3: |
| small = uint16_t(get16()); |
| break; |
| case 4: |
| small = int32_t(get32()); |
| break; |
| case 5: |
| small = uint32_t(get32()); |
| break; |
| default: |
| WASM_UNREACHABLE("invalid value"); |
| } |
| switch (type.getBasic()) { |
| case Type::i32: |
| return Literal(int32_t(small)); |
| case Type::i64: |
| return Literal(int64_t(small)); |
| case Type::f32: |
| return Literal(float(small)); |
| case Type::f64: |
| return Literal(double(small)); |
| case Type::v128: |
| case Type::none: |
| case Type::unreachable: |
| WASM_UNREACHABLE("unexpected type"); |
| } |
| break; |
| } |
| case 2: { |
| // special values |
| Literal value; |
| switch (type.getBasic()) { |
| case Type::i32: |
| value = Literal(pick<int32_t>(0, |
| std::numeric_limits<int8_t>::min(), |
| std::numeric_limits<int8_t>::max(), |
| std::numeric_limits<int16_t>::min(), |
| std::numeric_limits<int16_t>::max(), |
| std::numeric_limits<int32_t>::min(), |
| std::numeric_limits<int32_t>::max(), |
| std::numeric_limits<uint8_t>::max(), |
| std::numeric_limits<uint16_t>::max(), |
| std::numeric_limits<uint32_t>::max())); |
| break; |
| case Type::i64: |
| value = Literal(pick<int64_t>(0, |
| std::numeric_limits<int8_t>::min(), |
| std::numeric_limits<int8_t>::max(), |
| std::numeric_limits<int16_t>::min(), |
| std::numeric_limits<int16_t>::max(), |
| std::numeric_limits<int32_t>::min(), |
| std::numeric_limits<int32_t>::max(), |
| std::numeric_limits<int64_t>::min(), |
| std::numeric_limits<int64_t>::max(), |
| std::numeric_limits<uint8_t>::max(), |
| std::numeric_limits<uint16_t>::max(), |
| std::numeric_limits<uint32_t>::max(), |
| std::numeric_limits<uint64_t>::max())); |
| break; |
| case Type::f32: |
| value = Literal(pick<float>(0.0f, |
| -0.0f, |
| std::numeric_limits<float>::min(), |
| std::numeric_limits<float>::max(), |
| std::numeric_limits<int32_t>::min(), |
| std::numeric_limits<int32_t>::max(), |
| std::numeric_limits<int64_t>::min(), |
| std::numeric_limits<int64_t>::max(), |
| std::numeric_limits<uint32_t>::max(), |
| std::numeric_limits<uint64_t>::max())); |
| break; |
| case Type::f64: |
| value = Literal(pick<double>(0.0, |
| -0.0, |
| std::numeric_limits<float>::min(), |
| std::numeric_limits<float>::max(), |
| std::numeric_limits<double>::min(), |
| std::numeric_limits<double>::max(), |
| std::numeric_limits<int32_t>::min(), |
| std::numeric_limits<int32_t>::max(), |
| std::numeric_limits<int64_t>::min(), |
| std::numeric_limits<int64_t>::max(), |
| std::numeric_limits<uint32_t>::max(), |
| std::numeric_limits<uint64_t>::max())); |
| break; |
| case Type::v128: |
| case Type::none: |
| case Type::unreachable: |
| WASM_UNREACHABLE("unexpected type"); |
| } |
| return tweak(value); |
| } |
| case 3: { |
| // powers of 2 |
| Literal value; |
| switch (type.getBasic()) { |
| case Type::i32: |
| value = Literal(int32_t(1) << upTo(32)); |
| break; |
| case Type::i64: |
| value = Literal(int64_t(1) << upTo(64)); |
| break; |
| case Type::f32: |
| value = Literal(float(int64_t(1) << upTo(64))); |
| break; |
| case Type::f64: |
| value = Literal(double(int64_t(1) << upTo(64))); |
| break; |
| case Type::v128: |
| case Type::none: |
| case Type::unreachable: |
| WASM_UNREACHABLE("unexpected type"); |
| } |
| return tweak(value); |
| } |
| } |
| WASM_UNREACHABLE("invalid value"); |
| } |
| |
| Expression* TranslateToFuzzReader::makeRefFuncConst(Type type) { |
| auto heapType = type.getHeapType(); |
| auto share = heapType.getShared(); |
| if (heapType.isBasic()) { |
| assert(heapType.getBasic(Unshared) == HeapType::func); |
| // With high probability, use the last created function if possible. |
| // Otherwise, continue on to select some other function. |
| if (funcContext && |
| funcContext->func->type.getHeapType().getShared() == share && |
| !oneIn(4)) { |
| auto* target = funcContext->func; |
| return builder.makeRefFunc(target->name, target->type); |
| } |
| } |
| // Look for a proper function starting from a random location, and loop from |
| // there, wrapping around to 0. |
| if (!wasm.functions.empty()) { |
| Index start = upTo(wasm.functions.size()); |
| Index i = start; |
| do { |
| auto& func = wasm.functions[i]; |
| if (Type::isSubType(func->type, type)) { |
| return builder.makeRefFunc(func->name); |
| } |
| i = (i + 1) % wasm.functions.size(); |
| } while (i != start); |
| } |
| // We don't have a matching function. Create a null some of the time here, |
| // but only rarely if the type is non-nullable (because in that case we'd need |
| // to add a ref.as_non_null to validate, and the code will trap when we get |
| // here). |
| if ((type.isNullable() && oneIn(2)) || |
| (type.isNonNullable() && oneIn(16) && funcContext)) { |
| Expression* ret = builder.makeRefNull(HeapTypes::nofunc.getBasic(share)); |
| if (!type.isNullable()) { |
| assert(funcContext); |
| ret = builder.makeRefAs(RefAsNonNull, ret); |
| } |
| return ret; |
| } |
| // As a final option, create a new function with the correct signature. If it |
| // returns a value, write a trap as we do not want to create any more code |
| // here (we might end up recursing). Note that a trap in the function lets us |
| // execute more code then the ref.as_non_null path just before us, which traps |
| // even if we never call the function. |
| if (heapType.isBasic()) { |
| // We need a specific signature type to create a function. Pick an arbitrary |
| // signature if we only had generic 'func' here. |
| TypeBuilder builder(1); |
| builder[0] = Signature(Type::none, Type::none); |
| builder[0].setShared(share); |
| heapType = (*builder.build())[0]; |
| } |
| auto* body = heapType.getSignature().results == Type::none |
| ? (Expression*)builder.makeNop() |
| : (Expression*)builder.makeUnreachable(); |
| auto* func = wasm.addFunction( |
| builder.makeFunction(Names::getValidFunctionName(wasm, "ref_func_target"), |
| Type(heapType, NonNullable, Exact), |
| {}, |
| body)); |
| return builder.makeRefFunc(func->name); |
| } |
| |
| Expression* TranslateToFuzzReader::makeConst(Type type) { |
| if (type.isRef()) { |
| assert(wasm.features.hasReferenceTypes()); |
| // With a low chance, just emit a null if that is valid. |
| if (type.isNullable() && oneIn(8)) { |
| return builder.makeRefNull(type.getHeapType()); |
| } |
| if (type.getHeapType().isString()) { |
| return makeStringConst(); |
| } |
| if (type.getHeapType().isBasic()) { |
| return makeBasicRef(type); |
| } else { |
| return makeCompoundRef(type); |
| } |
| } else if (type.isTuple()) { |
| std::vector<Expression*> operands; |
| for (const auto& t : type) { |
| operands.push_back(makeConst(t)); |
| } |
| return builder.makeTupleMake(std::move(operands)); |
| } else { |
| assert(type.isBasic()); |
| return builder.makeConst(makeLiteral(type)); |
| } |
| } |
| |
| Expression* TranslateToFuzzReader::makeBasicRef(Type type) { |
| assert(type.isRef()); |
| auto heapType = type.getHeapType(); |
| assert(heapType.isBasic()); |
| assert(wasm.features.hasReferenceTypes()); |
| auto share = heapType.getShared(); |
| switch (heapType.getBasic(Unshared)) { |
| case HeapType::ext: { |
| if (wasm.features.hasStrings() && share == Unshared && oneIn(2)) { |
| // Shared strings not yet supported. |
| return makeConst(Type(HeapType::string, NonNullable)); |
| } |
| auto null = builder.makeRefNull(HeapTypes::ext.getBasic(share)); |
| // TODO: support actual non-nullable externrefs via imported globals or |
| // similar. |
| if (!type.isNullable()) { |
| return builder.makeRefAs(RefAsNonNull, null); |
| } |
| return null; |
| } |
| case HeapType::func: { |
| // Rarely, emit a call to imported table.get (when nullable, unshared, and |
| // where we can emit a call). |
| if (type.isNullable() && share == Unshared && funcContext && |
| tableGetImportName && !oneIn(3)) { |
| return makeImportTableGet(); |
| } |
| return makeRefFuncConst(type); |
| } |
| case HeapType::cont: { |
| // Most of the time, avoid null continuations, as they will trap. |
| if (type.isNullable() && oneIn(4)) { |
| return builder.makeRefNull(HeapTypes::cont.getBasic(share)); |
| } |
| // Emit the simplest possible continuation. |
| auto funcSig = Signature(Type::none, Type::none); |
| auto funcType = Type(funcSig, NonNullable); |
| auto contType = Continuation(funcSig); |
| return builder.makeContNew(contType, makeRefFuncConst(funcType)); |
| } |
| case HeapType::any: { |
| // Choose a subtype we can materialize a constant for. We cannot |
| // materialize non-nullable refs to func or i31 in global contexts. |
| Nullability nullability = getSubType(type.getNullability()); |
| assert(wasm.features.hasGC()); |
| auto subtype = pick(HeapTypes::i31, HeapTypes::struct_, HeapTypes::array); |
| return makeConst(Type(subtype.getBasic(share), nullability)); |
| } |
| case HeapType::eq: { |
| if (!wasm.features.hasGC()) { |
| // Without wasm GC all we have is an "abstract" eqref type, which is |
| // a subtype of anyref, but we cannot create constants of it, except |
| // for null. |
| assert(type.isNullable()); |
| return builder.makeRefNull(HeapTypes::none.getBasic(share)); |
| } |
| auto nullability = getSubType(type.getNullability()); |
| // ref.i31 is not allowed in initializer expressions. |
| HeapType subtype; |
| switch (upTo(3)) { |
| case 0: |
| subtype = HeapType::i31; |
| break; |
| case 1: |
| subtype = HeapType::struct_; |
| break; |
| case 2: |
| subtype = HeapType::array; |
| break; |
| } |
| return makeConst(Type(subtype.getBasic(share), nullability)); |
| } |
| case HeapType::i31: { |
| assert(wasm.features.hasGC()); |
| if (type.isNullable() && oneIn(4)) { |
| return builder.makeRefNull(HeapTypes::none.getBasic(share)); |
| } |
| return builder.makeRefI31(makeConst(Type::i32), share); |
| } |
| case HeapType::struct_: { |
| assert(wasm.features.hasGC()); |
| // TODO: Construct nontrivial types. For now just create a hard coded |
| // struct. |
| // Use a local static to avoid the expense of canonicalizing a new type |
| // every time. |
| static HeapType trivialStruct = HeapType(Struct()); |
| static HeapType sharedTrivialStruct = []() { |
| TypeBuilder builder(1); |
| builder[0] = Struct{}; |
| builder[0].setShared(); |
| return (*builder.build())[0]; |
| }(); |
| auto ht = share == Shared ? sharedTrivialStruct : trivialStruct; |
| return builder.makeStructNew(ht, std::vector<Expression*>{}); |
| } |
| case HeapType::array: { |
| static HeapType trivialArray = |
| HeapType(Array(Field(Field::PackedType::i8, Immutable))); |
| static HeapType sharedTrivialArray = []() { |
| TypeBuilder builder(1); |
| builder[0] = Array(Field(Field::PackedType::i8, Immutable)); |
| builder[0].setShared(); |
| return (*builder.build())[0]; |
| }(); |
| auto ht = share == Shared ? sharedTrivialArray : trivialArray; |
| return builder.makeArrayNewFixed(ht, {}); |
| } |
| case HeapType::exn: { |
| // If nullable, sometimes emit a null. If not in a function context, see |
| // below, we need a null as well regardless of the type. |
| if ((type.isNullable() && oneIn(2)) || !funcContext) { |
| auto* null = builder.makeRefNull(HeapTypes::exn.getBasic(share)); |
| if (type.isNullable()) { |
| return null; |
| } |
| // The type is non-nullable, so we are here because we are in a non- |
| // function context, with nothing valid to emit. "Fix" it with a cast, |
| // which is not valid IR, but which the calling code will handle. |
| assert(!funcContext); |
| return builder.makeRefAs(RefAsNonNull, null); |
| } |
| |
| // Emit a throw in a block. |
| auto* throww = makeThrow(Type::unreachable); |
| auto label = makeLabel(); |
| auto* tryy = builder.makeTryTable(throww, {Name()}, {label}, {true}); |
| return builder.makeBlock(label, tryy); |
| } |
| case HeapType::string: { |
| // In non-function contexts all we can do is string.const. |
| assert(share == Unshared && "shared strings not supported"); |
| if (!funcContext) { |
| return makeStringConst(); |
| } |
| switch (upTo(11)) { |
| case 0: |
| case 1: |
| case 2: |
| return makeStringConst(); |
| case 3: |
| case 4: |
| case 5: |
| return makeStringNewCodePoint(); |
| case 6: |
| case 7: |
| // We less frequently make string.new_array as it may generate a lot |
| // of code for the array in some cases. |
| return makeStringNewArray(); |
| case 8: |
| // We much less frequently make string.concat as it will recursively |
| // generate two string children, i.e., it can lead to exponential |
| // growth. |
| return makeStringConcat(); |
| case 9: |
| case 10: |
| return makeStringSlice(); |
| } |
| WASM_UNREACHABLE("bad switch"); |
| } |
| case HeapType::none: |
| case HeapType::noext: |
| case HeapType::nofunc: |
| case HeapType::nocont: |
| case HeapType::noexn: { |
| auto null = builder.makeRefNull(heapType.getBasic(share)); |
| if (!type.isNullable()) { |
| assert(funcContext); |
| return builder.makeRefAs(RefAsNonNull, null); |
| } |
| return null; |
| } |
| } |
| WASM_UNREACHABLE("invalid basic ref type"); |
| } |
| |
| Expression* TranslateToFuzzReader::makeCompoundRef(Type type) { |
| assert(type.isRef()); |
| auto heapType = type.getHeapType(); |
| assert(!heapType.isBasic()); |
| assert(wasm.features.hasReferenceTypes()); |
| |
| // Prefer not to emit a null, in general, as we can trap from them. If it is |
| // nullable, give a small chance to do so; if we hit the nesting limit then we |
| // really have no choice and must emit a null (or else we could infinitely |
| // recurse). For the nesting limit, use a bound that is higher than the normal |
| // one, so that the normal mechanisms should prevent us from getting here; |
| // this limit is really a last resort we want to never reach. Also, increase |
| // the chance to emit a null as |nesting| rises, to avoid deep recursive |
| // structures. |
| // |
| // Note that we might have cycles of types where some are non-nullable. We |
| // 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; |
| AutoNester nester(*this); |
| if (type.isNullable() && |
| (random.finished() || nesting >= LIMIT || oneIn(LIMIT - nesting + 1))) { |
| return builder.makeRefNull(heapType); |
| } |
| |
| // If the type is non-nullable, but we've run out of input, then we need to do |
| // something here to avoid infinite recursion. In the worse case we'll emit a |
| // cast to non-null of a null, which validates, but it will trap at runtime |
| // which is not ideal. |
| if (type.isNonNullable() && (random.finished() || nesting >= LIMIT)) { |
| // If we have a function context then we can at least emit a local.get, |
| // sometimes, which is less bad. Note that we need to check typeLocals |
| // manually here to avoid infinite recursion (as makeLocalGet will fall back |
| // to us, if there is no local). |
| // TODO: we could also look for locals containing subtypes |
| if (funcContext) { |
| if (!funcContext->typeLocals[type].empty()) { |
| return makeLocalGet(type); |
| } |
| // No local, but we are in a function context so RefAsNonNull is valid. |
| return builder.makeRefAs(RefAsNonNull, builder.makeRefNull(heapType)); |
| } |
| // No function context, so we are in quite the pickle. Continue onwards, as |
| // we may succeed to emit something more complex (like a struct.new). |
| } |
| |
| // When we make children, they must be trivial if we are not in a function |
| // context. |
| auto makeChild = [&](Type type) { |
| return funcContext ? make(type) : makeTrivial(type); |
| }; |
| |
| switch (heapType.getKind()) { |
| case HeapTypeKind::Func: |
| return makeRefFuncConst(type); |
| case HeapTypeKind::Struct: { |
| auto& fields = heapType.getStruct().fields; |
| std::vector<Expression*> values; |
| // If there is a nondefaultable field, we must provide the value and not |
| // depend on defaults. Also do that randomly half the time. |
| if (std::any_of( |
| fields.begin(), |
| fields.end(), |
| [&](const Field& field) { return !field.type.isDefaultable(); }) || |
| oneIn(2)) { |
| for (auto& field : fields) { |
| values.push_back(makeChild(field.type)); |
| } |
| // Add more nesting manually, as we can easily get exponential blowup |
| // here. This nesting makes it much less likely for a recursive data |
| // structure to end up as a massive tree of struct.news, since the |
| // nesting limitation code at the top of this function will kick in. |
| if (!values.empty()) { |
| // Subtract 1 since if there is a single value there cannot be |
| // exponential blowup. |
| nester.add(values.size() - 1); |
| } |
| } |
| Expression* descriptor = nullptr; |
| if (auto descType = heapType.getDescriptorType()) { |
| descriptor = makeTrappingRefUse(Type(*descType, Nullable, Exact)); |
| } |
| return builder.makeStructNew(heapType, values, descriptor); |
| } |
| case HeapTypeKind::Array: { |
| auto element = heapType.getArray().element; |
| Expression* init = nullptr; |
| if (!element.type.isDefaultable() || oneIn(2)) { |
| init = makeChild(element.type); |
| } |
| auto* count = |
| builder.makeConst(int32_t(upTo(fuzzParams->MAX_ARRAY_SIZE))); |
| return builder.makeArrayNew(type.getHeapType(), count, init); |
| } |
| case HeapTypeKind::Cont: { |
| auto funcType = heapType.getContinuation().type; |
| return builder.makeContNew(heapType, makeTrappingRefUse(funcType)); |
| } |
| case HeapTypeKind::Basic: |
| break; |
| } |
| WASM_UNREACHABLE("unexpected kind"); |
| } |
| |
| Expression* TranslateToFuzzReader::makeStringNewArray() { |
| auto* array = makeTrappingRefUse(HeapTypes::getMutI16Array()); |
| auto* start = make(Type::i32); |
| auto* end = make(Type::i32); |
| return builder.makeStringNew(StringNewWTF16Array, array, start, end); |
| } |
| |
| Expression* TranslateToFuzzReader::makeStringNewCodePoint() { |
| auto codePoint = make(Type::i32); |
| return builder.makeStringNew(StringNewFromCodePoint, codePoint); |
| } |
| |
| Expression* TranslateToFuzzReader::makeStringConst() { |
| // Construct an interesting WTF-8 string from parts and use string.const. |
| std::stringstream wtf8; |
| bool lastWasLeadingSurrogate = false; |
| for (size_t i = 0, end = upTo(4); i < end; ++i) { |
| switch (upTo(6)) { |
| case 0: |
| // A simple ascii string. |
| wtf8 << std::to_string(upTo(1024)); |
| break; |
| case 1: |
| // '£' |
| wtf8 << "\xC2\xA3"; |
| break; |
| case 2: |
| // '€' |
| wtf8 << "\xE2\x82\xAC"; |
| break; |
| case 3: |
| // '𐍈' |
| wtf8 << "\xF0\x90\x8D\x88"; |
| break; |
| case 4: |
| // The leading surrogate in '𐍈' |
| wtf8 << "\xED\xA0\x80"; |
| lastWasLeadingSurrogate = true; |
| continue; |
| case 5: |
| if (lastWasLeadingSurrogate) { |
| // Avoid invalid WTF-8. |
| continue; |
| } |
| // The trailing surrogate in '𐍈' |
| wtf8 << "\xED\xBD\x88"; |
| break; |
| } |
| lastWasLeadingSurrogate = false; |
| } |
| std::stringstream wtf16; |
| // TODO: Use wtf16.view() once we have C++20. |
| String::convertWTF8ToWTF16(wtf16, wtf8.str()); |
| return builder.makeStringConst(wtf16.str()); |
| } |
| |
| Expression* TranslateToFuzzReader::makeStringConcat() { |
| auto* left = makeTrappingRefUse(HeapType::string); |
| auto* right = makeTrappingRefUse(HeapType::string); |
| return builder.makeStringConcat(left, right); |
| } |
| |
| Expression* TranslateToFuzzReader::makeStringSlice() { |
| auto* ref = makeTrappingRefUse(HeapType::string); |
| auto* start = make(Type::i32); |
| auto* end = make(Type::i32); |
| return builder.makeStringSliceWTF(ref, start, end); |
| } |
| |
| Expression* TranslateToFuzzReader::makeStringEq(Type type) { |
| assert(type == Type::i32); |
| |
| if (oneIn(2)) { |
| auto* left = make(Type(HeapType::string, getNullability())); |
| auto* right = make(Type(HeapType::string, getNullability())); |
| return builder.makeStringEq(StringEqEqual, left, right); |
| } |
| |
| // string.compare may trap if the either input is null. |
| auto* left = makeTrappingRefUse(HeapType::string); |
| auto* right = makeTrappingRefUse(HeapType::string); |
| return builder.makeStringEq(StringEqCompare, left, right); |
| } |
| |
| Expression* TranslateToFuzzReader::makeStringMeasure(Type type) { |
| assert(type == Type::i32); |
| |
| auto* ref = makeTrappingRefUse(HeapType::string); |
| return builder.makeStringMeasure(StringMeasureWTF16, ref); |
| } |
| |
| Expression* TranslateToFuzzReader::makeStringGet(Type type) { |
| assert(type == Type::i32); |
| |
| auto* ref = makeTrappingRefUse(HeapType::string); |
| auto* pos = make(Type::i32); |
| return builder.makeStringWTF16Get(ref, pos); |
| } |
| |
| Expression* TranslateToFuzzReader::makeTrappingRefUse(HeapType type) { |
| return makeTrappingRefUse(Type(type, Nullable)); |
| } |
| |
| Expression* TranslateToFuzzReader::makeTrappingRefUse(Type type) { |
| auto percent = upTo(100); |
| // Only give a low probability to emit a nullable reference. |
| if (percent < 5) { |
| return make(type.with(Nullable)); |
| } |
| // Otherwise, usually emit a non-nullable one. |
| auto nonNull = type.with(NonNullable); |
| if (percent < 70 || !funcContext) { |
| return make(nonNull); |
| } |
| // With significant probability, try to use an existing value, that is, to |
| // get a value using local.get, as it is better to have patterns like this: |
| // |
| // (local.set $ref (struct.new $.. |
| // (struct.get (local.get $ref)) |
| // |
| // Rather than constantly operating on new data each time: |
| // |
| // (local.set $ref (struct.new $.. |
| // (struct.get (struct.new $.. |
| // |
| // By using local values more, we get more coverage of interesting sequences |
| // of reads and writes to the same objects. |
| // |
| // Note that makeLocalGet will add a local if necessary, and even tee that |
| // value so it is usable later as well. |
| return makeLocalGet(nonNull); |
| } |
| |
| Expression* TranslateToFuzzReader::buildUnary(const UnaryArgs& args) { |
| return builder.makeUnary(args.a, args.b); |
| } |
| |
| Expression* TranslateToFuzzReader::makeUnary(Type type) { |
| assert(!type.isTuple()); |
| // There are no unary ops for reference types. |
| // TODO: not quite true if you count struct.new and array.new. |
| if (type.isRef()) { |
| return makeTrivial(type); |
| } |
| switch (type.getBasic()) { |
| case Type::i32: { |
| auto singleConcreteType = getSingleConcreteType(); |
| if (singleConcreteType.isRef()) { |
| // TODO: Do something more interesting here. |
| return makeTrivial(type); |
| } |
| switch (singleConcreteType.getBasic()) { |
| case Type::i32: { |
| auto op = pick( |
| FeatureOptions<UnaryOp>() |
| .add(FeatureSet::MVP, EqZInt32, ClzInt32, CtzInt32, PopcntInt32) |
| .add(FeatureSet::SignExt, ExtendS8Int32, ExtendS16Int32)); |
| return buildUnary({op, make(Type::i32)}); |
| } |
| case Type::i64: |
| return buildUnary({pick(EqZInt64, WrapInt64), make(Type::i64)}); |
| case Type::f32: { |
| auto op = pick(FeatureOptions<UnaryOp>() |
| .add(FeatureSet::MVP, |
| TruncSFloat32ToInt32, |
| TruncUFloat32ToInt32, |
| ReinterpretFloat32) |
| .add(FeatureSet::TruncSat, |
| TruncSatSFloat32ToInt32, |
| TruncSatUFloat32ToInt32)); |
| return buildUnary({op, make(Type::f32)}); |
| } |
| case Type::f64: { |
| auto op = pick( |
| FeatureOptions<UnaryOp>() |
| .add(FeatureSet::MVP, TruncSFloat64ToInt32, TruncUFloat64ToInt32) |
| .add(FeatureSet::TruncSat, |
| TruncSatSFloat64ToInt32, |
| TruncSatUFloat64ToInt32)); |
| return buildUnary({op, make(Type::f64)}); |
| } |
| case Type::v128: { |
| assert(wasm.features.hasSIMD()); |
| auto op = pick(FeatureOptions<UnaryOp>().add(FeatureSet::SIMD, |
| AnyTrueVec128, |
| AllTrueVecI8x16, |
| AllTrueVecI16x8, |
| AllTrueVecI32x4, |
| AllTrueVecI64x2, |
| BitmaskVecI8x16, |
| BitmaskVecI16x8, |
| BitmaskVecI32x4, |
| BitmaskVecI64x2)); |
| return buildUnary({op, make(Type::v128)}); |
| } |
| case Type::none: |
| case Type::unreachable: |
| WASM_UNREACHABLE("unexpected type"); |
| } |
| WASM_UNREACHABLE("invalid type"); |
| } |
| case Type::i64: { |
| switch (upTo(4)) { |
| case 0: { |
| auto op = |
| pick(FeatureOptions<UnaryOp>() |
| .add(FeatureSet::MVP, ClzInt64, CtzInt64, PopcntInt64) |
| .add(FeatureSet::SignExt, |
| ExtendS8Int64, |
| ExtendS16Int64, |
| ExtendS32Int64)); |
| return buildUnary({op, make(Type::i64)}); |
| } |
| case 1: |
| return buildUnary( |
| {pick(ExtendSInt32, ExtendUInt32), make(Type::i32)}); |
| case 2: { |
| auto op = pick( |
| FeatureOptions<UnaryOp>() |
| .add(FeatureSet::MVP, TruncSFloat32ToInt64, TruncUFloat32ToInt64) |
| .add(FeatureSet::TruncSat, |
| TruncSatSFloat32ToInt64, |
| TruncSatUFloat32ToInt64)); |
| return buildUnary({op, make(Type::f32)}); |
| } |
| case 3: { |
| auto op = pick(FeatureOptions<UnaryOp>() |
| .add(FeatureSet::MVP, |
| TruncSFloat64ToInt64, |
| TruncUFloat64ToInt64, |
| ReinterpretFloat64) |
| .add(FeatureSet::TruncSat, |
| TruncSatSFloat64ToInt64, |
| TruncSatUFloat64ToInt64)); |
| return buildUnary({op, make(Type::f64)}); |
| } |
| } |
| WASM_UNREACHABLE("invalid value"); |
| } |
| case Type::f32: { |
| switch (upTo(4)) { |
| case 0: |
| return buildUnary({pick(NegFloat32, |
| AbsFloat32, |
| CeilFloat32, |
| FloorFloat32, |
| TruncFloat32, |
| NearestFloat32, |
| SqrtFloat32), |
| make(Type::f32)}); |
| case 1: |
| return buildUnary({pick(ConvertUInt32ToFloat32, |
| ConvertSInt32ToFloat32, |
| ReinterpretInt32), |
| make(Type::i32)}); |
| case 2: |
| return buildUnary( |
| {pick(ConvertUInt64ToFloat32, ConvertSInt64ToFloat32), |
| make(Type::i64)}); |
| case 3: |
| return buildUnary({DemoteFloat64, make(Type::f64)}); |
| } |
| WASM_UNREACHABLE("invalid value"); |
| } |
| case Type::f64: { |
| switch (upTo(4)) { |
| case 0: |
| return buildUnary({pick(NegFloat64, |
| AbsFloat64, |
| CeilFloat64, |
| FloorFloat64, |
| TruncFloat64, |
| NearestFloat64, |
| SqrtFloat64), |
| make(Type::f64)}); |
| case 1: |
| return buildUnary( |
| {pick(ConvertUInt32ToFloat64, ConvertSInt32ToFloat64), |
| make(Type::i32)}); |
| case 2: |
| return buildUnary({pick(ConvertUInt64ToFloat64, |
| ConvertSInt64ToFloat64, |
| ReinterpretInt64), |
| make(Type::i64)}); |
| case 3: |
| return buildUnary({PromoteFloat32, make(Type::f32)}); |
| } |
| WASM_UNREACHABLE("invalid value"); |
| } |
| case Type::v128: { |
| assert(wasm.features.hasSIMD()); |
| switch (upTo(5)) { |
| case 0: |
| return buildUnary({pick(SplatVecI8x16, SplatVecI16x8, SplatVecI32x4), |
| make(Type::i32)}); |
| case 1: |
| return buildUnary({SplatVecI64x2, make(Type::i64)}); |
| case 2: |
| return buildUnary({SplatVecF32x4, make(Type::f32)}); |
| case 3: |
| return buildUnary({SplatVecF64x2, make(Type::f64)}); |
| case 4: |
| return buildUnary({pick(FeatureOptions<UnaryOp>() |
| .add(FeatureSet::SIMD, |
| NotVec128, |
| AbsVecI8x16, |
| AbsVecI16x8, |
| AbsVecI32x4, |
| AbsVecI64x2, |
| PopcntVecI8x16, |
| NegVecI8x16, |
| NegVecI16x8, |
| NegVecI32x4, |
| NegVecI64x2, |
| AbsVecF32x4, |
| NegVecF32x4, |
| SqrtVecF32x4, |
| CeilVecF32x4, |
| FloorVecF32x4, |
| TruncVecF32x4, |
| NearestVecF32x4, |
| AbsVecF64x2, |
| NegVecF64x2, |
| SqrtVecF64x2, |
| CeilVecF64x2, |
| FloorVecF64x2, |
| TruncVecF64x2, |
| NearestVecF64x2, |
| ExtAddPairwiseSVecI8x16ToI16x8, |
| ExtAddPairwiseUVecI8x16ToI16x8, |
| ExtAddPairwiseSVecI16x8ToI32x4, |
| ExtAddPairwiseUVecI16x8ToI32x4, |
| TruncSatSVecF32x4ToVecI32x4, |
| TruncSatUVecF32x4ToVecI32x4, |
| ConvertSVecI32x4ToVecF32x4, |
| ConvertUVecI32x4ToVecF32x4, |
| ExtendLowSVecI8x16ToVecI16x8, |
| ExtendHighSVecI8x16ToVecI16x8, |
| ExtendLowUVecI8x16ToVecI16x8, |
| ExtendHighUVecI8x16ToVecI16x8, |
| ExtendLowSVecI16x8ToVecI32x4, |
| ExtendHighSVecI16x8ToVecI32x4, |
| ExtendLowUVecI16x8ToVecI32x4, |
| ExtendHighUVecI16x8ToVecI32x4, |
| ExtendLowSVecI32x4ToVecI64x2, |
| ExtendHighSVecI32x4ToVecI64x2, |
| ExtendLowUVecI32x4ToVecI64x2, |
| ExtendHighUVecI32x4ToVecI64x2, |
| ConvertLowSVecI32x4ToVecF64x2, |
| ConvertLowUVecI32x4ToVecF64x2, |
| TruncSatZeroSVecF64x2ToVecI32x4, |
| TruncSatZeroUVecF64x2ToVecI32x4, |
| DemoteZeroVecF64x2ToVecF32x4, |
| PromoteLowVecF32x4ToVecF64x2) |
| .add(FeatureSet::RelaxedSIMD, |
| RelaxedTruncSVecF32x4ToVecI32x4, |
| RelaxedTruncUVecF32x4ToVecI32x4, |
| RelaxedTruncZeroSVecF64x2ToVecI32x4, |
| RelaxedTruncZeroUVecF64x2ToVecI32x4) |
| .add(FeatureSet::FP16, |
| AbsVecF16x8, |
| NegVecF16x8, |
| SqrtVecF16x8, |
| CeilVecF16x8, |
| FloorVecF16x8, |
| TruncVecF16x8, |
| NearestVecF16x8, |
| TruncSatSVecF16x8ToVecI16x8, |
| TruncSatUVecF16x8ToVecI16x8, |
| ConvertSVecI16x8ToVecF16x8, |
| ConvertUVecI16x8ToVecF16x8)), |
| make(Type::v128)}); |
| } |
| WASM_UNREACHABLE("invalid value"); |
| } |
| case Type::none: |
| case Type::unreachable: |
| WASM_UNREACHABLE("unexpected type"); |
| } |
| WASM_UNREACHABLE("invalid type"); |
| } |
| |
| Expression* TranslateToFuzzReader::buildBinary(const BinaryArgs& args) { |
| return builder.makeBinary(args.a, args.b, args.c); |
| } |
| |
| Expression* TranslateToFuzzReader::makeBinary(Type type) { |
| assert(!type.isTuple()); |
| // There are no binary ops for reference types. |
| // TODO: Use struct.new |
| if (type.isRef()) { |
| return makeTrivial(type); |
| } |
| switch (type.getBasic()) { |
| case Type::i32: { |
| switch (upTo(4)) { |
| case 0: |
| return buildBinary({pick(AddInt32, |
| SubInt32, |
| MulInt32, |
| DivSInt32, |
| DivUInt32, |
| RemSInt32, |
| RemUInt32, |
| AndInt32, |
| OrInt32, |
| XorInt32, |
| ShlInt32, |
| ShrUInt32, |
| ShrSInt32, |
| RotLInt32, |
| RotRInt32, |
| EqInt32, |
| NeInt32, |
| LtSInt32, |
| LtUInt32, |
| LeSInt32, |
| LeUInt32, |
| GtSInt32, |
| GtUInt32, |
| GeSInt32, |
| GeUInt32), |
| make(Type::i32), |
| make(Type::i32)}); |
| case 1: |
| return buildBinary({pick(EqInt64, |
| NeInt64, |
| LtSInt64, |
| LtUInt64, |
| LeSInt64, |
| LeUInt64, |
| GtSInt64, |
| GtUInt64, |
| GeSInt64, |
| GeUInt64), |
| make(Type::i64), |
| make(Type::i64)}); |
| case 2: |
| return buildBinary({pick(EqFloat32, |
| NeFloat32, |
| LtFloat32, |
| LeFloat32, |
| GtFloat32, |
| GeFloat32), |
| make(Type::f32), |
| make(Type::f32)}); |
| case 3: |
| return buildBinary({pick(EqFloat64, |
| NeFloat64, |
| LtFloat64, |
| LeFloat64, |
| GtFloat64, |
| GeFloat64), |
| make(Type::f64), |
| make(Type::f64)}); |
| } |
| WASM_UNREACHABLE("invalid value"); |
| } |
| case Type::i64: { |
| return buildBinary({pick(AddInt64, |
| SubInt64, |
| MulInt64, |
| DivSInt64, |
| DivUInt64, |
| RemSInt64, |
| RemUInt64, |
| AndInt64, |
| OrInt64, |
| XorInt64, |
| ShlInt64, |
| ShrUInt64, |
| ShrSInt64, |
| RotLInt64, |
| RotRInt64), |
| make(Type::i64), |
| make(Type::i64)}); |
| } |
| case Type::f32: { |
| return buildBinary({pick(AddFloat32, |
| SubFloat32, |
| MulFloat32, |
| DivFloat32, |
| CopySignFloat32, |
| MinFloat32, |
| MaxFloat32), |
| make(Type::f32), |
| make(Type::f32)}); |
| } |
| case Type::f64: { |
| return buildBinary({pick(AddFloat64, |
| SubFloat64, |
| MulFloat64, |
| DivFloat64, |
| CopySignFloat64, |
| MinFloat64, |
| MaxFloat64), |
| make(Type::f64), |
| make(Type::f64)}); |
| } |
| case Type::v128: { |
| assert(wasm.features.hasSIMD()); |
| return buildBinary({pick(FeatureOptions<BinaryOp>() |
| .add(FeatureSet::SIMD, |
| EqVecI8x16, |
| NeVecI8x16, |
| LtSVecI8x16, |
| LtUVecI8x16, |
| GtSVecI8x16, |
| GtUVecI8x16, |
| LeSVecI8x16, |
| LeUVecI8x16, |
| GeSVecI8x16, |
| GeUVecI8x16, |
| EqVecI16x8, |
| NeVecI16x8, |
| LtSVecI16x8, |
| LtUVecI16x8, |
| GtSVecI16x8, |
| GtUVecI16x8, |
| LeSVecI16x8, |
| LeUVecI16x8, |
| GeSVecI16x8, |
| GeUVecI16x8, |
| EqVecI32x4, |
| NeVecI32x4, |
| LtSVecI32x4, |
| LtUVecI32x4, |
| GtSVecI32x4, |
| GtUVecI32x4, |
| LeSVecI32x4, |
| LeUVecI32x4, |
| GeSVecI32x4, |
| GeUVecI32x4, |
| EqVecI64x2, |
| NeVecI64x2, |
| LtSVecI64x2, |
| GtSVecI64x2, |
| LeSVecI64x2, |
| GeSVecI64x2, |
| EqVecF32x4, |
| NeVecF32x4, |
| LtVecF32x4, |
| GtVecF32x4, |
| LeVecF32x4, |
| GeVecF32x4, |
| EqVecF64x2, |
| NeVecF64x2, |
| LtVecF64x2, |
| GtVecF64x2, |
| LeVecF64x2, |
| GeVecF64x2, |
| |
| // SIMD arithmetic |
| AndVec128, |
| OrVec128, |
| XorVec128, |
| AndNotVec128, |
| AddVecI8x16, |
| AddSatSVecI8x16, |
| AddSatUVecI8x16, |
| SubVecI8x16, |
| SubSatSVecI8x16, |
| SubSatUVecI8x16, |
| MinSVecI8x16, |
| MinUVecI8x16, |
| MaxSVecI8x16, |
| MaxUVecI8x16, |
| AvgrUVecI8x16, |
| AddVecI16x8, |
| AddSatSVecI16x8, |
| AddSatUVecI16x8, |
| SubVecI16x8, |
| SubSatSVecI16x8, |
| SubSatUVecI16x8, |
| MulVecI16x8, |
| MinSVecI16x8, |
| MinUVecI16x8, |
| MaxSVecI16x8, |
| MaxUVecI16x8, |
| AvgrUVecI16x8, |
| Q15MulrSatSVecI16x8, |
| ExtMulLowSVecI16x8, |
| ExtMulHighSVecI16x8, |
| ExtMulLowUVecI16x8, |
| ExtMulHighUVecI16x8, |
| AddVecI32x4, |
| SubVecI32x4, |
| MulVecI32x4, |
| MinSVecI32x4, |
| MinUVecI32x4, |
| MaxSVecI32x4, |
| MaxUVecI32x4, |
| DotSVecI16x8ToVecI32x4, |
| ExtMulLowSVecI32x4, |
| ExtMulHighSVecI32x4, |
| ExtMulLowUVecI32x4, |
| ExtMulHighUVecI32x4, |
| AddVecI64x2, |
| SubVecI64x2, |
| MulVecI64x2, |
| ExtMulLowSVecI64x2, |
| ExtMulHighSVecI64x2, |
| ExtMulLowUVecI64x2, |
| ExtMulHighUVecI64x2, |
| AddVecF32x4, |
| SubVecF32x4, |
| MulVecF32x4, |
| DivVecF32x4, |
| MinVecF32x4, |
| MaxVecF32x4, |
| PMinVecF32x4, |
| PMaxVecF32x4, |
| AddVecF64x2, |
| SubVecF64x2, |
| MulVecF64x2, |
| DivVecF64x2, |
| MinVecF64x2, |
| MaxVecF64x2, |
| PMinVecF64x2, |
| PMaxVecF64x2, |
| |
| // SIMD Conversion |
| NarrowSVecI16x8ToVecI8x16, |
| NarrowUVecI16x8ToVecI8x16, |
| NarrowSVecI32x4ToVecI16x8, |
| NarrowUVecI32x4ToVecI16x8, |
| |
| // SIMD Swizzle |
| SwizzleVecI8x16) |
| .add(FeatureSet::FP16, |
| EqVecF16x8, |
| EqVecF16x8, |
| NeVecF16x8, |
| LtVecF16x8, |
| GtVecF16x8, |
| LeVecF16x8, |
| GeVecF16x8, |
| AddVecF16x8, |
| SubVecF16x8, |
| MulVecF16x8, |
| DivVecF16x8, |
| MinVecF16x8, |
| MaxVecF16x8, |
| PMinVecF16x8, |
| PMaxVecF16x8)), |
| make(Type::v128), |
| make(Type::v128)}); |
| } |
| case Type::none: |
| case Type::unreachable: |
| WASM_UNREACHABLE("unexpected type"); |
| } |
| WASM_UNREACHABLE("invalid type"); |
| } |
| |
| Expression* TranslateToFuzzReader::buildSelect(const ThreeArgs& args) { |
| return builder.makeSelect(args.a, args.b, args.c); |
| } |
| |
| Expression* TranslateToFuzzReader::makeSelect(Type type) { |
| Type subType1 = getSubType(type); |
| Type subType2 = getSubType(type); |
| return buildSelect({make(Type::i32), make(subType1), make(subType2)}); |
| } |
| |
| Expression* TranslateToFuzzReader::makeSwitch(Type type) { |
| assert(type == Type::unreachable); |
| if (funcContext->breakableStack.empty()) { |
| return make(type); |
| } |
| // we need to find proper targets to break to; try a bunch |
| int tries = fuzzParams->TRIES; |
| std::vector<Name> names; |
| Type valueType = Type::unreachable; |
| while (tries-- > 0) { |
| auto* target = pick(funcContext->breakableStack); |
| auto name = getTargetName(target); |
| auto currValueType = getTargetType(target); |
| if (names.empty()) { |
| valueType = currValueType; |
| } else { |
| if (valueType != currValueType) { |
| continue; // all values must be the same |
| } |
| } |
| names.push_back(name); |
| } |
| if (names.size() < 2) { |
| // we failed to find enough |
| return make(type); |
| } |
| auto default_ = names.back(); |
| names.pop_back(); |
| auto temp1 = make(Type::i32), |
| temp2 = valueType.isConcrete() ? make(valueType) : nullptr; |
| return builder.makeSwitch(names, default_, temp1, temp2); |
| } |
| |
| Expression* TranslateToFuzzReader::makeDrop(Type type) { |
| return builder.makeDrop(make(getConcreteType())); |
| } |
| |
| Expression* TranslateToFuzzReader::makeReturn(Type type) { |
| return builder.makeReturn(funcContext->func->getResults().isConcrete() |
| ? make(funcContext->func->getResults()) |
| : nullptr); |
| } |
| |
| Expression* TranslateToFuzzReader::makeNop(Type type) { |
| assert(type == Type::none); |
| return builder.makeNop(); |
| } |
| |
| Expression* TranslateToFuzzReader::makeUnreachable(Type type) { |
| assert(type == Type::unreachable); |
| return builder.makeUnreachable(); |
| } |
| |
| Expression* TranslateToFuzzReader::makeAtomic(Type type) { |
| assert(wasm.features.hasAtomics()); |
| if (!allowMemory) { |
| return makeTrivial(type); |
| } |
| wasm.memories[0]->shared = true; |
| if (type == Type::none) { |
| return builder.makeAtomicFence(); |
| } |
| if (type == Type::i32 && oneIn(2)) { |
| if (ATOMIC_WAITS && oneIn(2)) { |
| auto* ptr = makePointer(); |
| auto expectedType = pick(Type::i32, Type::i64); |
| auto* expected = make(expectedType); |
| auto* timeout = make(Type::i64); |
| return builder.makeAtomicWait(ptr, |
| expected, |
| timeout, |
| expectedType, |
| logify(get()), |
| wasm.memories[0]->name); |
| } else { |
| auto* ptr = makePointer(); |
| auto* count = make(Type::i32); |
| return builder.makeAtomicNotify( |
| ptr, count, logify(get()), wasm.memories[0]->name); |
| } |
| } |
| Index bytes; |
| switch (type.getBasic()) { |
| case Type::i32: { |
| switch (upTo(3)) { |
| case 0: |
| bytes = 1; |
| break; |
| case 1: |
| bytes = pick(1, 2); |
| break; |
| case 2: |
| bytes = pick(1, 2, 4); |
| break; |
| default: |
| WASM_UNREACHABLE("invalide value"); |
| } |
| break; |
| } |
| case Type::i64: { |
| switch (upTo(4)) { |
| case 0: |
| bytes = 1; |
| break; |
| case 1: |
| bytes = pick(1, 2); |
| break; |
| case 2: |
| bytes = pick(1, 2, 4); |
| break; |
| case 3: |
| bytes = pick(1, 2, 4, 8); |
| break; |
| default: |
| WASM_UNREACHABLE("invalide value"); |
| } |
| break; |
| } |
| default: |
| WASM_UNREACHABLE("unexpected type"); |
| } |
| auto offset = logify(get()); |
| auto* ptr = makePointer(); |
| if (oneIn(2)) { |
| auto* value = make(type); |
| return builder.makeAtomicRMW( |
| pick(RMWAdd, RMWSub, RMWAnd, RMWOr, RMWXor, RMWXchg), |
| bytes, |
| offset, |
| ptr, |
| value, |
| type, |
| wasm.memories[0]->name); |
| } else { |
| auto* expected = make(type); |
| auto* replacement = make(type); |
| return builder.makeAtomicCmpxchg( |
| bytes, offset, ptr, expected, replacement, type, wasm.memories[0]->name); |
| } |
| } |
| |
| Expression* TranslateToFuzzReader::makeSIMD(Type type) { |
| assert(wasm.features.hasSIMD()); |
| if (type.isRef()) { |
| return makeTrivial(type); |
| } |
| if (type != Type::v128) { |
| return makeSIMDExtract(type); |
| } |
| // TODO: Add SIMDLoadStoreLane once it is generally available |
| switch (upTo(7)) { |
| case 0: |
| return makeUnary(Type::v128); |
| case 1: |
| return makeBinary(Type::v128); |
| case 2: |
| return makeSIMDReplace(); |
| case 3: |
| return makeSIMDShuffle(); |
| case 4: |
| return makeSIMDTernary(); |
| case 5: |
| return makeSIMDShift(); |
| case 6: |
| return makeSIMDLoad(); |
| } |
| WASM_UNREACHABLE("invalid value"); |
| } |
| |
| Expression* TranslateToFuzzReader::makeSIMDExtract(Type type) { |
| auto op = static_cast<SIMDExtractOp>(0); |
| switch (type.getBasic()) { |
| case Type::i32: |
| op = pick(ExtractLaneSVecI8x16, |
| ExtractLaneUVecI8x16, |
| ExtractLaneSVecI16x8, |
| ExtractLaneUVecI16x8, |
| ExtractLaneVecI32x4); |
| break; |
| case Type::i64: |
| op = ExtractLaneVecI64x2; |
| break; |
| case Type::f32: |
| op = pick(FeatureOptions<SIMDExtractOp>() |
| .add(FeatureSet::SIMD, ExtractLaneVecF32x4) |
| .add(FeatureSet::FP16, ExtractLaneVecF16x8)); |
| break; |
| case Type::f64: |
| op = ExtractLaneVecF64x2; |
| break; |
| case Type::v128: |
| case Type::none: |
| case Type::unreachable: |
| WASM_UNREACHABLE("unexpected type"); |
| } |
| Expression* vec = make(Type::v128); |
| uint8_t index = 0; |
| switch (op) { |
| case ExtractLaneSVecI8x16: |
| case ExtractLaneUVecI8x16: |
| index = upTo(16); |
| break; |
| case ExtractLaneSVecI16x8: |
| case ExtractLaneUVecI16x8: |
| case ExtractLaneVecF16x8: |
| index = upTo(8); |
| break; |
| case ExtractLaneVecI32x4: |
| case ExtractLaneVecF32x4: |
| index = upTo(4); |
| break; |
| case ExtractLaneVecI64x2: |
| case ExtractLaneVecF64x2: |
| index = upTo(2); |
| break; |
| } |
| return builder.makeSIMDExtract(op, vec, index); |
| } |
| |
| Expression* TranslateToFuzzReader::makeSIMDReplace() { |
| SIMDReplaceOp op = |
| pick(FeatureOptions<SIMDReplaceOp>() |
| .add(FeatureSet::SIMD, |
| ReplaceLaneVecI8x16, |
| ReplaceLaneVecI16x8, |
| ReplaceLaneVecI32x4, |
| ReplaceLaneVecI64x2, |
| ReplaceLaneVecF32x4, |
| ReplaceLaneVecF64x2) |
| .add(FeatureSet::FeatureSet::FP16, ReplaceLaneVecF16x8)); |
| Expression* vec = make(Type::v128); |
| uint8_t index; |
| Type lane_t; |
| switch (op) { |
| case ReplaceLaneVecI8x16: |
| index = upTo(16); |
| lane_t = Type::i32; |
| break; |
| case ReplaceLaneVecI16x8: |
| index = upTo(8); |
| lane_t = Type::i32; |
| break; |
| case ReplaceLaneVecI32x4: |
| index = upTo(4); |
| lane_t = Type::i32; |
| break; |
| case ReplaceLaneVecI64x2: |
| index = upTo(2); |
| lane_t = Type::i64; |
| break; |
| case ReplaceLaneVecF16x8: |
| index = upTo(8); |
| lane_t = Type::f32; |
| break; |
| case ReplaceLaneVecF32x4: |
| index = upTo(4); |
| lane_t = Type::f32; |
| break; |
| case ReplaceLaneVecF64x2: |
| index = upTo(2); |
| lane_t = Type::f64; |
| break; |
| default: |
| WASM_UNREACHABLE("unexpected op"); |
| } |
| Expression* value = make(lane_t); |
| return builder.makeSIMDReplace(op, vec, index, value); |
| } |
| |
| Expression* TranslateToFuzzReader::makeSIMDShuffle() { |
| Expression* left = make(Type::v128); |
| Expression* right = make(Type::v128); |
| std::array<uint8_t, 16> mask; |
| for (size_t i = 0; i < 16; ++i) { |
| mask[i] = upTo(32); |
| } |
| return builder.makeSIMDShuffle(left, right, mask); |
| } |
| |
| Expression* TranslateToFuzzReader::makeSIMDTernary() { |
| // TODO: Enable qfma/qfms once it is implemented in V8 and the interpreter |
| // SIMDTernaryOp op = pick(Bitselect, |
| // QFMAF32x4, |
| // QFMSF32x4, |
| // QFMAF64x2, |
| // QFMSF64x2); |
| SIMDTernaryOp op = Bitselect; |
| Expression* a = make(Type::v128); |
| Expression* b = make(Type::v128); |
| Expression* c = make(Type::v128); |
| return builder.makeSIMDTernary(op, a, b, c); |
| } |
| |
| Expression* TranslateToFuzzReader::makeSIMDShift() { |
| SIMDShiftOp op = pick(ShlVecI8x16, |
| ShrSVecI8x16, |
| ShrUVecI8x16, |
| ShlVecI16x8, |
| ShrSVecI16x8, |
| ShrUVecI16x8, |
| ShlVecI32x4, |
| ShrSVecI32x4, |
| ShrUVecI32x4, |
| ShlVecI64x2, |
| ShrSVecI64x2, |
| ShrUVecI64x2); |
| Expression* vec = make(Type::v128); |
| Expression* shift = make(Type::i32); |
| return builder.makeSIMDShift(op, vec, shift); |
| } |
| |
| Expression* TranslateToFuzzReader::makeSIMDLoad() { |
| SIMDLoadOp op = pick(Load8SplatVec128, |
| Load16SplatVec128, |
| Load32SplatVec128, |
| Load64SplatVec128, |
| Load8x8SVec128, |
| Load8x8UVec128, |
| Load16x4SVec128, |
| Load16x4UVec128, |
| Load32x2SVec128, |
| Load32x2UVec128, |
| Load32ZeroVec128, |
| Load64ZeroVec128); |
| Address offset = logify(get()); |
| Address align; |
| switch (op) { |
| case Load8SplatVec128: |
| align = 1; |
| break; |
| case Load16SplatVec128: |
| align = pick(1, 2); |
| break; |
| case Load32SplatVec128: |
| align = pick(1, 2, 4); |
| break; |
| case Load64SplatVec128: |
| case Load8x8SVec128: |
| case Load8x8UVec128: |
| case Load16x4SVec128: |
| case Load16x4UVec128: |
| case Load32x2SVec128: |
| case Load32x2UVec128: |
| align = pick(1, 2, 4, 8); |
| break; |
| case Load32ZeroVec128: |
| align = 4; |
| break; |
| case Load64ZeroVec128: |
| align = 8; |
| break; |
| } |
| Expression* ptr = makePointer(); |
| return builder.makeSIMDLoad(op, offset, align, ptr, wasm.memories[0]->name); |
| } |
| |
| Expression* TranslateToFuzzReader::makeBulkMemory(Type type) { |
| if (!allowMemory) { |
| return makeTrivial(type); |
| } |
| assert(wasm.features.hasBulkMemory()); |
| assert(type == Type::none); |
| switch (upTo(4)) { |
| case 0: |
| return makeMemoryInit(); |
| case 1: |
| return makeDataDrop(); |
| case 2: |
| return makeMemoryCopy(); |
| case 3: |
| return makeMemoryFill(); |
| } |
| WASM_UNREACHABLE("invalid value"); |
| } |
| |
| Expression* TranslateToFuzzReader::makeTableGet(Type type) { |
| // Emit a get from the funcref table (which always exists) or the exnref one |
| // (which might not, if EH is disabled). |
| auto makeTableGet = [&](Name tableName) { |
| auto* table = wasm.getTable(tableName); |
| // Usually emit in-bounds gets, to avoid trapping, but rarely allow |
| // anything. |
| Expression* index; |
| if (allowOOB && oneIn(10)) { |
| index = make(table->addressType); |
| } else { |
| index = builder.makeConst( |
| Literal::makeFromInt32(upTo(table->initial), table->addressType)); |
| } |
| return builder.makeTableGet(tableName, index, table->type); |
| }; |
| if (type.getHeapType() == HeapType::exn) { |
| return makeTableGet(exnrefTableName); |
| } else if (type.getHeapType() == HeapType::func) { |
| return makeTableGet(funcrefTableName); |
| } else { |
| WASM_UNREACHABLE("bad TableGet type"); |
| } |
| } |
| |
| Expression* TranslateToFuzzReader::makeTableSet(Type type) { |
| assert(type == Type::none); |
| |
| // Emit a set to either the funcref table (which always exists) or the exnref |
| // one (which might not, if EH is disabled). |
| auto makeTableSet = [&](Name tableName) { |
| auto* table = wasm.getTable(tableName); |
| // Usually emit in-bounds sets, to avoid trapping, but rarely allow |
| // anything. |
| Expression* index; |
| if (allowOOB && oneIn(10)) { |
| index = make(table->addressType); |
| } else { |
| index = builder.makeConst( |
| Literal::makeFromInt32(upTo(table->initial), table->addressType)); |
| } |
| auto* value = make(table->type); |
| return builder.makeTableSet(tableName, index, value); |
| }; |
| if (exnrefTableName && oneIn(2)) { |
| return makeTableSet(exnrefTableName); |
| } else { |
| assert(funcrefTableName); |
| return makeTableSet(funcrefTableName); |
| } |
| } |
| |
| Expression* TranslateToFuzzReader::makeRefIsNull(Type type) { |
| assert(type == Type::i32); |
| assert(wasm.features.hasReferenceTypes()); |
| return builder.makeRefIsNull(make(getReferenceType())); |
| } |
| |
| Expression* TranslateToFuzzReader::makeRefEq(Type type) { |
| assert(type == Type::i32); |
| assert(wasm.features.hasReferenceTypes() && wasm.features.hasGC()); |
| auto* left = make(getEqReferenceType()); |
| auto* right = make(getEqReferenceType()); |
| return builder.makeRefEq(left, right); |
| } |
| |
| Expression* TranslateToFuzzReader::makeRefTest(Type type) { |
| assert(type == Type::i32); |
| assert(wasm.features.hasReferenceTypes() && wasm.features.hasGC()); |
| // The case of the reference and the cast type having a connection is useful, |
| // so give a decent chance for one to be a subtype of the other. |
| Type refType, castType; |
| switch (upTo(3)) { |
| case 0: |
| // Totally random. |
| refType = getCastableReferenceType(); |
| castType = getCastableReferenceType(); |
| // They must share a bottom type in order to validate. |
| if (refType.getHeapType().getBottom() == |
| castType.getHeapType().getBottom()) { |
| break; |
| } |
| // Otherwise, fall through and generate things in a way that is |
| // guaranteed to validate. |
| [[fallthrough]]; |
| case 1: |
| // Cast is a subtype of ref. |
| refType = getCastableReferenceType(); |
| castType = getSubType(refType); |
| break; |
| case 2: |
| // Ref is a subtype of cast. |
| castType = getCastableReferenceType(); |
| refType = getSubType(castType); |
| break; |
| default: |
| // This unreachable avoids a warning on refType being possibly undefined. |
| WASM_UNREACHABLE("bad case"); |
| } |
| if (!wasm.features.hasCustomDescriptors()) { |
| // Exact cast targets disallowed without custom descriptors. |
| castType = castType.with(Inexact); |
| } |
| return builder.makeRefTest(make(refType), castType); |
| } |
| |
| Expression* TranslateToFuzzReader::makeRefCast(Type type) { |
| assert(type.isRef()); |
| assert(wasm.features.hasReferenceTypes() && wasm.features.hasGC()); |
| assert(type.isInexact() || wasm.features.hasCustomDescriptors()); |
| // As with RefTest, use possibly related types. Unlike there, we are given the |
| // output type, which is the cast type, so just generate the ref's type. |
| Type refType; |
| switch (upTo(3)) { |
| case 0: |
| // Totally random. |
| refType = getCastableReferenceType(); |
| // They must share a bottom type in order to validate. |
| if (refType.getHeapType().getBottom() == type.getHeapType().getBottom()) { |
| break; |
| } |
| // Otherwise, fall through and generate things in a way that is |
| // guaranteed to validate. |
| [[fallthrough]]; |
| case 1: { |
| // Cast is a subtype of ref. We can't modify |type|, so find a supertype |
| // for the ref. |
| refType = getSuperType(type); |
| break; |
| } |
| case 2: |
| // Ref is a subtype of cast. |
| refType = getSubType(type); |
| break; |
| default: |
| // This unreachable avoids a warning on refType being possibly undefined. |
| WASM_UNREACHABLE("bad case"); |
| } |
| auto* ref = make(refType); |
| |
| // Emit a non-descriptor cast if we have to, or otherwise half the time. |
| auto desc = type.getHeapType().getDescriptorType(); |
| if (!desc || oneIn(2)) { |
| return builder.makeRefCast(ref, type); |
| } |
| |
| // Emit a descriptor for a descriptor cast. |
| auto* descRef = make(type.with(*desc)); |
| auto* ret = builder.makeRefCast(ref, descRef, type); |
| // descRef may be a subtype of the type we asked make() for, and if so then |
| // it might have a different described type - perhaps even an unrelated one, |
| // if the descriptors subtype but not the describees. Use an exact type to |
| // fix that up. |
| if (!Type::isSubType(ret->type, type)) { |
| descRef = make(type.with(*desc).with(Exact)); |
| ret = builder.makeRefCast(ref, descRef, type); |
| } |
| return ret; |
| } |
| |
| Expression* TranslateToFuzzReader::makeRefGetDesc(Type type) { |
| auto described = type.getHeapType().getDescribedType(); |
| assert(described); |
| auto refType = type.with(*described); |
| return builder.makeRefGetDesc(make(refType)); |
| } |
| |
| Expression* TranslateToFuzzReader::makeBrOn(Type type) { |
| if (funcContext->breakableStack.empty()) { |
| return makeTrivial(type); |
| } |
| // We need to find a proper target to break to; try a few times. Finding the |
| // target is harder than flowing out the proper type, so focus on the target, |
| // and fix up the flowing type later. That is, once we find a target to break |
| // 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; |
| Name targetName; |
| Type targetType; |
| while (--tries >= 0) { |
| auto* target = pick(funcContext->breakableStack); |
| targetName = getTargetName(target); |
| targetType = getTargetType(target); |
| // We can send any reference type, or no value at all, but nothing else. |
| if (targetType.isRef() || targetType == Type::none) { |
| break; |
| } |
| } |
| if (tries < 0) { |
| return makeTrivial(type); |
| } |
| |
| auto fixFlowingType = [&](Expression* brOn) -> Expression* { |
| if (Type::isSubType(brOn->type, type)) { |
| // Already of the proper type. |
| return brOn; |
| } |
| if (type == Type::none) { |
| // We just need to drop whatever it is. |
| return builder.makeDrop(brOn); |
| } |
| // We need to replace the type with something else. Drop the BrOn if we need |
| // to, and append a value with the proper type. |
| if (brOn->type != Type::none) { |
| brOn = builder.makeDrop(brOn); |
| } |
| return builder.makeSequence(brOn, make(type)); |
| }; |
| |
| // We found something to break to. Figure out which BrOn variants we can |
| // send. |
| if (targetType == Type::none) { |
| // BrOnNull is the only variant that sends no value. |
| return fixFlowingType( |
| builder.makeBrOn(BrOnNull, targetName, make(getReferenceType()))); |
| } |
| |
| // We are sending a reference type to the target. All other BrOn variants can |
| // do that. |
| assert(targetType.isRef()); |
| // BrOnNonNull can handle sending any reference. The casts are more limited. |
| auto op = BrOnNonNull; |
| if (targetType.isCastable()) { |
| op = pick(BrOnNonNull, BrOnCast, BrOnCastFail); |
| } |
| Type castType = Type::none; |
| Type refType; |
| switch (op) { |
| case BrOnNonNull: { |
| // The sent type is the non-nullable version of the reference, so any ref |
| // of that type is ok, nullable or not. |
| refType = targetType.with(getNullability()); |
| break; |
| } |
| case BrOnCast: { |
| // The sent type is the heap type we cast to, with the input type's |
| // nullability, so the combination of the two must be a subtype of |
| // targetType. |
| castType = getSubType(targetType); |
| if (castType.isExact() && !wasm.features.hasCustomDescriptors()) { |
| // This exact cast is only valid if its input has the same type (or the |
| // only possible strict subtype, bottom). |
| refType = castType; |
| } else { |
| // The ref's type must be castable to castType, or we'd not validate. |
| // But it can also be a subtype, which will trivially also succeed (so |
| // do that more rarely). Pick subtypes rarely, as they make the cast |
| // trivial. |
| refType = oneIn(5) ? getSubType(castType) : getSuperType(castType); |
| } |
| if (targetType.isNonNullable()) { |
| // And it must have the right nullability for the target, as mentioned |
| // above: if the target type is non-nullable then either the ref or the |
| // cast types must be. |
| if (!refType.isNonNullable() && !castType.isNonNullable()) { |
| // Pick one to make non-nullable. |
| if (oneIn(2)) { |
| refType = Type(refType.getHeapType(), NonNullable); |
| } else { |
| castType = Type(castType.getHeapType(), NonNullable); |
| } |
| } |
| } |
| break; |
| } |
| case BrOnCastFail: { |
| // The sent type is the ref's type, with adjusted nullability (if the cast |
| // allows nulls then no null can fail the cast, and what is sent is non- |
| // nullable). First, pick a ref type that we can send to the target. |
| refType = getSubType(targetType); |
| // See above on BrOnCast, but flipped. |
| castType = oneIn(5) ? getSuperType(refType) : getSubType(refType); |
| castType = castType.withInexactIfNoCustomDescs(wasm.features); |
| // There is no nullability to adjust: if targetType is non-nullable then |
| // both refType and castType are as well, as subtypes of it. But we can |
| // also allow castType to be nullable (it is not sent to the target). |
| if (castType.isNonNullable() && oneIn(2)) { |
| castType = Type(castType.getHeapType(), Nullable); |
| } |
| } break; |
| default: { |
| WASM_UNREACHABLE("bad br_on op"); |
| } |
| } |
| return fixFlowingType( |
| builder.makeBrOn(op, targetName, make(refType), castType)); |
| } |
| |
| bool TranslateToFuzzReader::maybeSignedGet(const Field& field) { |
| if (field.isPacked()) { |
| return oneIn(2); |
| } |
| return false; |
| } |
| |
| Expression* TranslateToFuzzReader::makeStructGet(Type type) { |
| auto& structFields = typeStructFields[type]; |
| assert(!structFields.empty()); |
| auto [structType, fieldIndex] = pick(structFields); |
| auto* ref = makeTrappingRefUse(structType); |
| auto signed_ = maybeSignedGet(structType.getStruct().fields[fieldIndex]); |
| return builder.makeStructGet( |
| fieldIndex, ref, MemoryOrder::Unordered, type, signed_); |
| } |
| |
| Expression* TranslateToFuzzReader::makeStructSet(Type type) { |
| assert(type == Type::none); |
| if (mutableStructFields.empty()) { |
| return makeTrivial(type); |
| } |
| auto [structType, fieldIndex] = pick(mutableStructFields); |
| auto fieldType = structType.getStruct().fields[fieldIndex].type; |
| auto* ref = makeTrappingRefUse(structType); |
| auto* value = make(fieldType); |
| return builder.makeStructSet(fieldIndex, ref, value, MemoryOrder::Unordered); |
| } |
| |
| // Make a bounds check for an array operation, given a ref + index. An optional |
| // additional length parameter can be provided, which is added to the index if |
| // so (that is useful for something like array.fill, which operations on not a |
| // single item like array.set, but a range). |
| static auto makeArrayBoundsCheck(Expression* ref, |
| Expression* index, |
| Function* func, |
| Builder& builder, |
| Expression* length = nullptr) { |
| // The reference might be a RefNull, in which case its type is exact. But we |
| // want to avoid creating exact-typed locals until we support them more widely |
| // in the fuzzer, so adjust the type. TODO: remove this once exact references |
| // are better supported. |
| Type refType = ref->type; |
| if (refType.isExact()) { |
| refType = refType.with(Inexact); |
| } |
| auto tempRef = builder.addVar(func, refType); |
| auto tempIndex = builder.addVar(func, index->type); |
| auto* teeRef = builder.makeLocalTee(tempRef, ref, refType); |
| auto* teeIndex = builder.makeLocalTee(tempIndex, index, index->type); |
| auto* getSize = builder.makeArrayLen(teeRef); |
| |
| Expression* effectiveIndex = teeIndex; |
| |
| Expression* getLength = nullptr; |
| if (length) { |
| // Store the length so we can reuse it. |
| auto tempLength = builder.addVar(func, length->type); |
| auto* teeLength = builder.makeLocalTee(tempLength, length, length->type); |
| // The effective index will now include the length. |
| effectiveIndex = builder.makeBinary(AddInt32, effectiveIndex, teeLength); |
| getLength = builder.makeLocalGet(tempLength, length->type); |
| } |
| |
| struct BoundsCheck { |
| // A condition that checks if the index is in bounds. |
| Expression* condition; |
| // An additional use of the reference (we stored the reference in a local, |
| // so this reads from that local). |
| Expression* getRef; |
| // An additional use of the index (as with the ref, it reads from a local). |
| Expression* getIndex; |
| // An additional use of the length, if it was provided. |
| Expression* getLength = nullptr; |
| } result = {builder.makeBinary(LtUInt32, effectiveIndex, getSize), |
| builder.makeLocalGet(tempRef, refType), |
| builder.makeLocalGet(tempIndex, index->type), |
| getLength}; |
| return result; |
| } |
| |
| Expression* TranslateToFuzzReader::makeArrayGet(Type type) { |
| auto& arrays = typeArrays[type]; |
| assert(!arrays.empty()); |
| auto arrayType = pick(arrays); |
| auto* ref = makeTrappingRefUse(arrayType); |
| auto* index = make(Type::i32); |
| auto signed_ = maybeSignedGet(arrayType.getArray().element); |
| // Only rarely emit a plain get which might trap. See related logic in |
| // ::makePointer(). |
| if (allowOOB && oneIn(10)) { |
| return builder.makeArrayGet( |
| ref, index, MemoryOrder::Unordered, type, signed_); |
| } |
| // To avoid a trap, check the length dynamically using this pattern: |
| // |
| // index < array.len ? array[index] : ..some fallback value.. |
| // |
| auto check = makeArrayBoundsCheck(ref, index, funcContext->func, builder); |
| auto* get = builder.makeArrayGet( |
| check.getRef, check.getIndex, MemoryOrder::Unordered, type, signed_); |
| auto* fallback = makeTrivial(type); |
| return builder.makeIf(check.condition, get, fallback); |
| } |
| |
| Expression* TranslateToFuzzReader::makeArraySet(Type type) { |
| assert(type == Type::none); |
| if (mutableArrays.empty()) { |
| return makeTrivial(type); |
| } |
| auto arrayType = pick(mutableArrays); |
| auto elementType = arrayType.getArray().element.type; |
| auto* index = make(Type::i32); |
| auto* ref = makeTrappingRefUse(arrayType); |
| auto* value = make(elementType); |
| // Only rarely emit a plain get which might trap. See related logic in |
| // ::makePointer(). |
| if (allowOOB && oneIn(10)) { |
| return builder.makeArraySet(ref, index, value, MemoryOrder::Unordered); |
| } |
| // To avoid a trap, check the length dynamically using this pattern: |
| // |
| // if (index < array.len) array[index] = value; |
| // |
| auto check = makeArrayBoundsCheck(ref, index, funcContext->func, builder); |
| auto* set = builder.makeArraySet( |
| check.getRef, check.getIndex, value, MemoryOrder::Unordered); |
| return builder.makeIf(check.condition, set); |
| } |
| |
| Expression* TranslateToFuzzReader::makeArrayBulkMemoryOp(Type type) { |
| assert(type == Type::none); |
| if (mutableArrays.empty()) { |
| return makeTrivial(type); |
| } |
| auto arrayType = pick(mutableArrays); |
| auto element = arrayType.getArray().element; |
| auto* index = make(Type::i32); |
| auto* ref = makeTrappingRefUse(arrayType); |
| if (oneIn(2)) { |
| // ArrayFill |
| auto* value = make(element.type); |
| auto* length = make(Type::i32); |
| // Only rarely emit a plain get which might trap. See related logic in |
| // ::makePointer(). |
| if (allowOOB && oneIn(10)) { |
| return builder.makeArrayFill(ref, index, value, length); |
| } |
| auto check = |
| makeArrayBoundsCheck(ref, index, funcContext->func, builder, length); |
| auto* fill = builder.makeArrayFill( |
| check.getRef, check.getIndex, value, check.getLength); |
| return builder.makeIf(check.condition, fill); |
| } else { |
| // ArrayCopy. Here we must pick a source array whose element type is a |
| // subtype of the destination. |
| auto srcArrayType = pick(mutableArrays); |
| auto srcElement = srcArrayType.getArray().element; |
| if (!Type::isSubType(srcElement.type, element.type) || |
| element.packedType != srcElement.packedType) { |
| // TODO: A matrix of which arrays are subtypes of others. For now, if we |
| // didn't get what we want randomly, just copy from the same type to |
| // itself. |
| srcArrayType = arrayType; |
| srcElement = element; |
| } |
| auto* srcIndex = make(Type::i32); |
| auto* srcRef = makeTrappingRefUse(srcArrayType); |
| auto* length = make(Type::i32); |
| if (allowOOB && oneIn(10)) { |
| return builder.makeArrayCopy(ref, index, srcRef, srcIndex, length); |
| } |
| auto check = |
| makeArrayBoundsCheck(ref, index, funcContext->func, builder, length); |
| auto srcCheck = makeArrayBoundsCheck( |
| srcRef, srcIndex, funcContext->func, builder, check.getLength); |
| auto* copy = builder.makeArrayCopy(check.getRef, |
| check.getIndex, |
| srcCheck.getRef, |
| srcCheck.getIndex, |
| srcCheck.getLength); |
| return builder.makeIf(check.condition, |
| builder.makeIf(srcCheck.condition, copy)); |
| } |
| } |
| |
| Expression* TranslateToFuzzReader::makeStringEncode(Type type) { |
| assert(type == Type::i32); |
| |
| auto* ref = makeTrappingRefUse(HeapType::string); |
| auto* array = makeTrappingRefUse(HeapTypes::getMutI16Array()); |
| auto* start = make(Type::i32); |
| |
| // Only rarely emit without a bounds check, which might trap. See related |
| // logic in other array operations. |
| if (allowOOB || oneIn(10)) { |
| return builder.makeStringEncode(StringEncodeWTF16Array, ref, array, start); |
| } |
| |
| // Stash the string reference while computing its length for a bounds check. |
| auto refLocal = builder.addVar(funcContext->func, ref->type); |
| auto* setRef = builder.makeLocalSet(refLocal, ref); |
| auto* strLen = builder.makeStringMeasure( |
| StringMeasureWTF16, builder.makeLocalGet(refLocal, ref->type)); |
| |
| // Do a bounds check on the array. |
| auto check = |
| makeArrayBoundsCheck(array, start, funcContext->func, builder, strLen); |
| array = check.getRef; |
| start = check.getIndex; |
| auto* getRef = builder.makeLocalGet(refLocal, ref->type); |
| auto* encode = |
| builder.makeStringEncode(StringEncodeWTF16Array, getRef, array, start); |
| |
| // Emit the set of the string reference and then an if that picks which code |
| // path to visit, depending on the outcome of the bounds check. |
| auto* iff = builder.makeIf(check.condition, encode, make(Type::i32)); |
| return builder.makeSequence(setRef, iff); |
| } |
| |
| Expression* TranslateToFuzzReader::makeI31Get(Type type) { |
| assert(type == Type::i32); |
| assert(wasm.features.hasReferenceTypes() && wasm.features.hasGC()); |
| auto* i31 = makeTrappingRefUse(HeapType::i31); |
| return builder.makeI31Get(i31, bool(oneIn(2))); |
| } |
| |
| Expression* TranslateToFuzzReader::makeThrow(Type type) { |
| assert(type == Type::unreachable); |
| Tag* tag; |
| if (trivialNesting || random.finished()) { |
| // We are nested under a makeTrivial call, so only emit something trivial. |
| // Get (or create) a trivial tag, so we have no operands (and will not call |
| // make(), below). Otherwise, we might recurse very deeply if we threw a |
| // tag that contains an exnref (for which we may end up creating yet another |
| // throw in a try). |
| if (!trivialTag) { |
| auto newTag = builder.makeTag(Names::getValidTagName(wasm, "tag$"), |
| Signature(Type::none, Type::none)); |
| tag = wasm.addTag(std::move(newTag)); |
| trivialTag = tag->name; |
| } else { |
| tag = wasm.getTag(trivialTag); |
| } |
| } else { |
| // Get a random tag, adding a random one if necessary. |
| if (exceptionTags.empty()) { |
| addTag(); |
| } |
| tag = pick(exceptionTags); |
| } |
| auto tagType = tag->params(); |
| std::vector<Expression*> operands; |
| for (auto t : tagType) { |
| operands.push_back(make(t)); |
| } |
| return builder.makeThrow(tag, operands); |
| } |
| |
| Expression* TranslateToFuzzReader::makeThrowRef(Type type) { |
| assert(type == Type::unreachable); |
| // Use a nullable type here to avoid the risk of trapping (when we find no way |
| // to make a non-nullable ref, we end up fixing validation with |
| // ref.as_non_null of a null, which validates but traps). |
| auto* ref = make(Type(HeapType::exn, Nullable)); |
| return builder.makeThrowRef(ref); |
| } |
| |
| Expression* TranslateToFuzzReader::makeMemoryInit() { |
| if (!allowMemory) { |
| return makeTrivial(Type::none); |
| } |
| Index segIdx = upTo(wasm.dataSegments.size()); |
| Name segment = wasm.dataSegments[segIdx]->name; |
| size_t totalSize = wasm.dataSegments[segIdx]->data.size(); |
| size_t offsetVal = upTo(totalSize); |
| size_t sizeVal = upTo(totalSize - offsetVal); |
| Expression* dest = makePointer(); |
| Expression* offset = builder.makeConst(int32_t(offsetVal)); |
| Expression* size = builder.makeConst(int32_t(sizeVal)); |
| return builder.makeMemoryInit( |
| segment, dest, offset, size, wasm.memories[0]->name); |
| } |
| |
| Expression* TranslateToFuzzReader::makeDataDrop() { |
| if (!allowMemory) { |
| return makeTrivial(Type::none); |
| } |
| Index segIdx = upTo(wasm.dataSegments.size()); |
| Name segment = wasm.dataSegments[segIdx]->name; |
| return builder.makeDataDrop(segment); |
| } |
| |
| Expression* TranslateToFuzzReader::makeMemoryCopy() { |
| if (!allowMemory) { |
| return makeTrivial(Type::none); |
| } |
| Expression* dest = makePointer(); |
| Expression* source = makePointer(); |
| Expression* size = make(wasm.memories[0]->addressType); |
| return builder.makeMemoryCopy( |
| dest, source, size, wasm.memories[0]->name, wasm.memories[0]->name); |
| } |
| |
| Expression* TranslateToFuzzReader::makeMemoryFill() { |
| if (!allowMemory) { |
| return makeTrivial(Type::none); |
| } |
| Expression* dest = makePointer(); |
| Expression* value = make(Type::i32); |
| Expression* size = make(wasm.memories[0]->addressType); |
| return builder.makeMemoryFill(dest, value, size, wasm.memories[0]->name); |
| } |
| |
| Type TranslateToFuzzReader::getSingleConcreteType() { |
| if (wasm.features.hasReferenceTypes() && !interestingHeapTypes.empty() && |
| oneIn(3)) { |
| auto heapType = pick(interestingHeapTypes); |
| auto nullability = getNullability(); |
| return Type(heapType, nullability); |
| } |
| // Skip (ref func|extern|i31|exn) because there is no way to create them in |
| // globals. TODO |
| using WeightedOption = FeatureOptions<Type>::WeightedOption; |
| return pick(FeatureOptions<Type>() |
| .add(FeatureSet::MVP, |
| WeightedOption{Type::i32, VeryImportant}, |
| WeightedOption{Type::i64, VeryImportant}, |
| WeightedOption{Type::f32, VeryImportant}, |
| WeightedOption{Type::f64, VeryImportant}) |
| .add(FeatureSet::SIMD, WeightedOption{Type::v128, Important}) |
| .add(FeatureSet::ReferenceTypes, |
| Type(HeapType::func, Nullable), |
| Type(HeapType::ext, Nullable)) |
| .add(FeatureSet::ExceptionHandling, |
| // Type(HeapType::exn, NonNullable), |
| Type(HeapType::exn, Nullable)) |
| .add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| // Type(HeapType::func, NonNullable), |
| // Type(HeapType::ext, NonNullable), |
| Type(HeapType::any, Nullable), |
| // Type(HeapType::any, NonNullable), |
| Type(HeapType::eq, Nullable), |
| Type(HeapType::eq, NonNullable), |
| Type(HeapType::i31, Nullable), |
| // Type(HeapType::i31, NonNullable), |
| Type(HeapType::struct_, Nullable), |
| Type(HeapType::struct_, NonNullable), |
| Type(HeapType::array, Nullable), |
| Type(HeapType::array, NonNullable)) |
| .add(FeatureSet::Strings, |
| Type(HeapType::string, Nullable), |
| Type(HeapType::string, NonNullable))); |
| } |
| |
| Type TranslateToFuzzReader::getReferenceType() { |
| if (wasm.features.hasReferenceTypes() && !interestingHeapTypes.empty() && |
| oneIn(2)) { |
| auto heapType = pick(interestingHeapTypes); |
| auto nullability = getNullability(); |
| return Type(heapType, nullability); |
| } |
| return pick(FeatureOptions<Type>() |
| // TODO: Add externref here. |
| .add(FeatureSet::ReferenceTypes, Type(HeapType::func, Nullable)) |
| .add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| Type(HeapType::func, NonNullable), |
| Type(HeapType::any, NonNullable), |
| Type(HeapType::eq, Nullable), |
| Type(HeapType::eq, NonNullable), |
| Type(HeapType::i31, Nullable), |
| Type(HeapType::i31, NonNullable), |
| Type(HeapType::struct_, Nullable), |
| Type(HeapType::struct_, NonNullable), |
| Type(HeapType::array, Nullable), |
| Type(HeapType::array, NonNullable)) |
| .add(FeatureSet::Strings, |
| Type(HeapType::string, Nullable), |
| Type(HeapType::string, NonNullable))); |
| } |
| |
| Type TranslateToFuzzReader::getCastableReferenceType() { |
| int tries = fuzzParams->TRIES; |
| while (tries-- > 0) { |
| auto type = getReferenceType(); |
| if (type.isCastable()) { |
| return type; |
| } |
| } |
| // We failed to find a type using fair sampling. Do something simple that must |
| // work. |
| Type type; |
| if (oneIn(4)) { |
| type = getSubType(Type(HeapType::func, Nullable)); |
| } else { |
| type = getSubType(Type(HeapType::any, Nullable)); |
| } |
| assert(type.isCastable()); |
| return type; |
| } |
| |
| Type TranslateToFuzzReader::getEqReferenceType() { |
| if (oneIn(2) && !interestingHeapTypes.empty()) { |
| // Try to find an interesting eq-compatible type. |
| auto heapType = pick(interestingHeapTypes); |
| if (HeapType::isSubType(heapType, HeapType::eq)) { |
| auto nullability = getNullability(); |
| return Type(heapType, nullability); |
| } |
| // Otherwise continue below. |
| } |
| return pick( |
| FeatureOptions<Type>().add(FeatureSet::ReferenceTypes | FeatureSet::GC, |
| Type(HeapType::eq, Nullable), |
| Type(HeapType::eq, NonNullable), |
| Type(HeapType::i31, Nullable), |
| Type(HeapType::i31, NonNullable), |
| Type(HeapType::struct_, Nullable), |
| Type(HeapType::struct_, NonNullable), |
| Type(HeapType::array, Nullable), |
| Type(HeapType::array, NonNullable))); |
| } |
| |
| Type TranslateToFuzzReader::getMVPType() { |
| return pick(Type::i32, Type::i64, Type::f32, Type::f64); |
| } |
| |
| Type TranslateToFuzzReader::getTupleType() { |
| std::vector<Type> elements; |
| size_t maxElements = 2 + upTo(fuzzParams->MAX_TUPLE_SIZE - 1); |
| for (size_t i = 0; i < maxElements; ++i) { |
| auto type = getSingleConcreteType(); |
| // Don't add a non-defaultable type into a tuple, as currently we can't |
| // spill them into locals (that would require a "let"). |
| if (type.isDefaultable()) { |
| elements.push_back(type); |
| } |
| } |
| while (elements.size() < 2) { |
| elements.push_back(getMVPType()); |
| } |
| return Type(elements); |
| } |
| |
| Type TranslateToFuzzReader::getConcreteType() { |
| if (wasm.features.hasMultivalue() && oneIn(5)) { |
| return getTupleType(); |
| } else { |
| return getSingleConcreteType(); |
| } |
| } |
| |
| Type TranslateToFuzzReader::getControlFlowType() { |
| if (oneIn(10)) { |
| return Type::none; |
| } else { |
| return getConcreteType(); |
| } |
| } |
| |
| Type TranslateToFuzzReader::getStorableType() { |
| return pick( |
| FeatureOptions<Type>() |
| .add(FeatureSet::MVP, Type::i32, Type::i64, Type::f32, Type::f64) |
| .add(FeatureSet::SIMD, Type::v128)); |
| } |
| |
| Type TranslateToFuzzReader::getLoggableType() { return pick(loggableTypes); } |
| |
| bool TranslateToFuzzReader::isLoggableType(Type type) { |
| return std::find(loggableTypes.begin(), loggableTypes.end(), type) != |
| loggableTypes.end(); |
| } |
| |
| Nullability TranslateToFuzzReader::getNullability() { |
| // Without wasm GC, avoid non-nullable types as we cannot create any values |
| // of such types. For example, reference types adds eqref, but there is no |
| // way to create such a value, only to receive it from the outside, while GC |
| // adds i31/struct/array creation. Without GC, we will likely need to create a |
| // null of this type (unless we are lucky enough to have a non-null value |
| // arriving from an import), so avoid a non-null type if possible. |
| if (wasm.features.hasGC() && oneIn(2)) { |
| return NonNullable; |
| } |
| return Nullable; |
| } |
| |
| Exactness TranslateToFuzzReader::getExactness() { |
| // Without GC, the only heap types are func and extern, neither of which is |
| // exactly inhabitable. To avoid introducing uninhabitable types, only |
| // generate exact references when GC is enabled. We don't need custom |
| // descriptors to be enabled even though that is the feature that introduces |
| // exact references because the binary writer can always generalize the exact |
| // reference types away. |
| // |
| // if (wasm.features.hasGC() && oneIn(8)) { |
| // return Exact; |
| // } |
| // |
| // However, we cannot yet handle creating exact references in general, so for |
| // now we always generate inexact references when given the choice. TODO. |
| return Inexact; |
| } |
| |
| Nullability TranslateToFuzzReader::getSubType(Nullability nullability) { |
| if (nullability == NonNullable) { |
| return NonNullable; |
| } |
| return getNullability(); |
| } |
| |
| Exactness TranslateToFuzzReader::getSubType(Exactness exactness) { |
| if (exactness == Exact) { |
| return Exact; |
| } |
| return getExactness(); |
| } |
| |
| HeapType TranslateToFuzzReader::getSubType(HeapType type) { |
| if (oneIn(3)) { |
| return type; |
| } |
| if (type.isBasic() && oneIn(2)) { |
| auto share = type.getShared(); |
| switch (type.getBasic(Unshared)) { |
| case HeapType::func: |
| // TODO: Typed function references. |
| assert(wasm.features.hasReferenceTypes()); |
| return pick(FeatureOptions<HeapType>() |
| .add(HeapTypes::func) |
| .add(FeatureSet::GC, HeapTypes::nofunc)) |
| .getBasic(share); |
| case HeapType::cont: |
| return pick(HeapTypes::cont, HeapTypes::nocont).getBasic(share); |
| case HeapType::ext: { |
| assert(wasm.features.hasReferenceTypes()); |
| auto options = FeatureOptions<HeapType>() |
| .add(HeapTypes::ext) |
| .add(FeatureSet::GC, HeapTypes::noext); |
| if (share == Unshared) { |
| // Shared strings not yet supported. |
| options.add(FeatureSet::Strings, HeapType::string); |
| } |
| return pick(options).getBasic(share); |
| } |
| case HeapType::any: { |
| assert(wasm.features.hasReferenceTypes()); |
| assert(wasm.features.hasGC()); |
| return pick(HeapTypes::any, |
| HeapTypes::eq, |
| HeapTypes::i31, |
| HeapTypes::struct_, |
| HeapTypes::array, |
| HeapTypes::none) |
| .getBasic(share); |
| } |
| case HeapType::eq: |
| assert(wasm.features.hasReferenceTypes()); |
| assert(wasm.features.hasGC()); |
| return pick(HeapTypes::eq, |
| HeapTypes::i31, |
| HeapTypes::struct_, |
| HeapTypes::array, |
| HeapTypes::none) |
| .getBasic(share); |
| case HeapType::i31: |
| return pick(HeapTypes::i31, HeapTypes::none).getBasic(share); |
| case HeapType::struct_: |
| return pick(HeapTypes::struct_, HeapTypes::none).getBasic(share); |
| case HeapType::array: |
| return pick(HeapTypes::array, HeapTypes::none).getBasic(share); |
| case HeapType::exn: |
| assert(share == Unshared); |
| return HeapTypes::exn.getBasic(share); |
| case HeapType::string: |
| assert(share == Unshared); |
| return HeapType::string; |
| case HeapType::none: |
| case HeapType::noext: |
| case HeapType::nofunc: |
| case HeapType::nocont: |
| case HeapType::noexn: |
| break; |
| } |
| } |
| // Look for an interesting subtype. |
| auto iter = interestingHeapSubTypes.find(type); |
| if (iter != interestingHeapSubTypes.end()) { |
| auto& subTypes = iter->second; |
| if (!subTypes.empty()) { |
| return pick(subTypes); |
| } |
| } |
| // Failure to do anything interesting, return the type. |
| return type; |
| } |
| |
| Type TranslateToFuzzReader::getSubType(Type type) { |
| if (type.isTuple()) { |
| std::vector<Type> types; |
| for (const auto& t : type) { |
| types.push_back(getSubType(t)); |
| } |
| return Type(types); |
| } else if (type.isRef()) { |
| auto heapType = type.getHeapType(); |
| // Do not generate non-nullable exnrefs in global positions (they cannot be |
| // created in wasm, nor imported from JS). |
| if (!funcContext && heapType.isMaybeShared(HeapType::exn)) { |
| return type; |
| } |
| if (type.isExact()) { |
| // The only other possible heap type is bottom, but we don't want to |
| // generate too many bottom types. |
| if (!heapType.isBottom() && oneIn(20)) { |
| heapType = heapType.getBottom(); |
| } |
| } else { |
| heapType = getSubType(heapType); |
| } |
| auto nullability = getSubType(type.getNullability()); |
| auto exactness = |
| heapType.isBasic() ? Inexact : getSubType(type.getExactness()); |
| auto subType = Type(heapType, nullability, exactness); |
| // We don't want to emit lots of uninhabitable types like (ref none), so |
| // avoid them with high probability. Specifically, if the original type was |
| // inhabitable then return that; avoid adding more uninhabitability. We can |
| // never add new uninhabitability outside of functions, where we cannot |
| // use casts to generate something valid. |
| if (GCTypeUtils::isUninhabitable(subType) && |
| !GCTypeUtils::isUninhabitable(type) && (!funcContext || !oneIn(20))) { |
| return type; |
| } |
| return subType; |
| } else { |
| // This is an MVP type without subtypes. |
| assert(type.isBasic()); |
| return type; |
| } |
| } |
| |
| Nullability TranslateToFuzzReader::getSuperType(Nullability nullability) { |
| if (nullability == Nullable) { |
| return Nullable; |
| } |
| return getNullability(); |
| } |
| |
| HeapType TranslateToFuzzReader::getSuperType(HeapType type) { |
| // TODO cache these? |
| std::vector<HeapType> supers; |
| while (1) { |
| supers.push_back(type); |
| if (auto super = type.getDeclaredSuperType()) { |
| type = *super; |
| } else { |
| break; |
| } |
| } |
| return pick(supers); |
| } |
| |
| Type TranslateToFuzzReader::getSuperType(Type type) { |
| if (!type.isRef()) { |
| return type; |
| } |
| auto heapType = getSuperType(type.getHeapType()); |
| auto nullability = getSuperType(type.getNullability()); |
| auto superType = Type(heapType, nullability); |
| // As with getSubType, we want to avoid returning an uninhabitable type where |
| // possible. Here all we can do is flip the super's nullability to nullable. |
| if (GCTypeUtils::isUninhabitable(superType)) { |
| superType = Type(heapType, Nullable); |
| } |
| return superType; |
| } |
| |
| Name TranslateToFuzzReader::getTargetName(Expression* target) { |
| if (auto* block = target->dynCast<Block>()) { |
| return block->name; |
| } else if (auto* loop = target->dynCast<Loop>()) { |
| return loop->name; |
| } |
| WASM_UNREACHABLE("unexpected expr type"); |
| } |
| |
| Type TranslateToFuzzReader::getTargetType(Expression* target) { |
| if (auto* block = target->dynCast<Block>()) { |
| return block->type; |
| } else if (target->is<Loop>()) { |
| return Type::none; |
| } |
| WASM_UNREACHABLE("unexpected expr type"); |
| } |
| |
| } // namespace wasm |