diff --git a/src/ir/gc-type-utils.h b/src/ir/gc-type-utils.h index e102856cde6..a20bd3502d3 100644 --- a/src/ir/gc-type-utils.h +++ b/src/ir/gc-type-utils.h @@ -21,7 +21,8 @@ namespace wasm::GCTypeUtils { -inline bool isUninhabitable(Type type) { +inline bool isTriviallyUninhabitable(Type type) { + // TODO: Include non-shared waitqueue. return type.isNonNullableRef() && type.isBottomRef(); } @@ -77,7 +78,7 @@ inline EvaluationResult evaluateCastCheck(Type refType, Type castType) { return Unknown; } - if (isUninhabitable(refType)) { + if (isTriviallyUninhabitable(refType)) { // No value can appear in the ref, so the cast cannot be reached. return Unreachable; } diff --git a/src/passes/DeadArgumentElimination.cpp b/src/passes/DeadArgumentElimination.cpp index ed948d745ad..fdc83856256 100644 --- a/src/passes/DeadArgumentElimination.cpp +++ b/src/passes/DeadArgumentElimination.cpp @@ -552,7 +552,7 @@ struct DAE : public Pass { // Do this for more complicated uninhabitable types such as non-nullable // references to structs with non-nullable reference cycles. bool wasReturnUninhabitable = - GCTypeUtils::isUninhabitable(func->getResults()); + GCTypeUtils::isTriviallyUninhabitable(func->getResults()); func->setResults(Type::none); // Remove the drops on the calls. Note that we must do this before updating // returns in ReturnUpdater, as there may be recursive calls of this diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h index 2bd3d7761c3..c0e25520096 100644 --- a/src/tools/fuzzing.h +++ b/src/tools/fuzzing.h @@ -212,6 +212,9 @@ class TranslateToFuzzReader { // The heap types we can pick from to generate instructions. std::vector interestingHeapTypes; + // Heap types from initial content that cannot be constructed. + std::unordered_set uninhabitableHeapTypes; + // A mapping of a heap type to the subset of interestingHeapTypes that are // subtypes of it. std::unordered_map> interestingHeapSubTypes; @@ -584,6 +587,7 @@ class TranslateToFuzzReader { Type getStorableType(); Type getLoggableType(); bool isLoggableType(Type type); + bool isUninhabitable(Type type); Nullability getNullability(); Exactness getExactness(); Nullability getSubType(Nullability nullability); diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index d9ed0d9e9fc..c42595a075b 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -528,25 +528,37 @@ void TranslateToFuzzReader::setupMemory() { } void TranslateToFuzzReader::setupHeapTypes() { + // Filter away uninhabitable heap types, that is, heap types that we cannot + // construct, like a type with a non-nullable reference to itself, and record + // them in uninhabitableHeapTypes. + auto filterInhabitable = [&](const std::vector& types) { + auto inhabitable = HeapTypeGenerator::getInhabitable(types); + std::unordered_set inhabitableSet(inhabitable.begin(), + inhabitable.end()); + for (auto type : types) { + if (!inhabitableSet.contains(type)) { + uninhabitableHeapTypes.insert(type); + } + } + return inhabitable; + }; + // Start with any existing heap types in the module, which may exist in any // initial content we began with. - auto possibleHeapTypes = ModuleUtils::collectHeapTypes(wasm); + interestingHeapTypes = filterInhabitable(ModuleUtils::collectHeapTypes(wasm)); // Use heap types from an imported module, if present. if (importedModule) { - auto importedHeapTypes = ModuleUtils::collectHeapTypes(*importedModule); + auto importedHeapTypes = + filterInhabitable(ModuleUtils::collectHeapTypes(*importedModule)); auto rate = upTo(11); for (auto type : importedHeapTypes) { if (upTo(10) < rate) { - possibleHeapTypes.push_back(type); + interestingHeapTypes.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( @@ -4524,8 +4536,9 @@ Expression* TranslateToFuzzReader::makeCompoundRef(Type type) { // inhabitable.) const auto LIMIT = fuzzParams.NESTING_LIMIT + 1; AutoNester nester(*this); - if (type.isNullableRef() && - (random.finished() || nesting >= LIMIT || oneIn(LIMIT - nesting + 1))) { + if (type.isNullableRef() && (random.finished() || nesting >= LIMIT || + uninhabitableHeapTypes.contains(heapType) || + oneIn(LIMIT - nesting + 1))) { return builder.makeRefNull(heapType); } @@ -4533,7 +4546,8 @@ Expression* TranslateToFuzzReader::makeCompoundRef(Type type) { // 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.isNonNullableRef() && (random.finished() || nesting >= LIMIT)) { + if (type.isNonNullableRef() && (random.finished() || nesting >= LIMIT || + uninhabitableHeapTypes.contains(heapType))) { // 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 @@ -4547,7 +4561,12 @@ Expression* TranslateToFuzzReader::makeCompoundRef(Type type) { 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). + // we may succeed to emit something more complex (like a struct.new). If the + // type is uninhabitable, though, use the ref.as_non_null escape hatch + // immediately. + if (uninhabitableHeapTypes.contains(heapType)) { + return builder.makeRefAs(RefAsNonNull, builder.makeRefNull(heapType)); + } } // When we make children, they must be trivial if we are not in a function @@ -6866,6 +6885,12 @@ bool TranslateToFuzzReader::isLoggableType(Type type) { loggableTypes.end(); } +bool TranslateToFuzzReader::isUninhabitable(Type type) { + return GCTypeUtils::isTriviallyUninhabitable(type) || + (type.isNonNullableRef() && + uninhabitableHeapTypes.contains(type.getHeapType())); +} + 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 @@ -7022,8 +7047,8 @@ Type TranslateToFuzzReader::getSubType(Type type) { // 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))) { + if (isUninhabitable(subType) && !isUninhabitable(type) && + (!funcContext || !oneIn(20))) { return type; } return subType; @@ -7090,7 +7115,7 @@ Type TranslateToFuzzReader::getSuperType(Type type) { 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)) { + if (isUninhabitable(superType)) { superType = Type(heapType, Nullable); } return superType; diff --git a/src/tools/fuzzing/heap-types.cpp b/src/tools/fuzzing/heap-types.cpp index ffe450f7f5d..cfdbd7cfd9c 100644 --- a/src/tools/fuzzing/heap-types.cpp +++ b/src/tools/fuzzing/heap-types.cpp @@ -1400,7 +1400,7 @@ bool isUninhabitable(HeapType type, bool isUninhabitable(Type type, std::unordered_set& visited, std::unordered_set& visiting) { - if (GCTypeUtils::isUninhabitable(type)) { + if (GCTypeUtils::isTriviallyUninhabitable(type)) { return true; } if (type.isNonNullableRef()) { diff --git a/test/lit/fuzz-uninhabitable.wast b/test/lit/fuzz-uninhabitable.wast new file mode 100644 index 00000000000..5c7c456ec2b --- /dev/null +++ b/test/lit/fuzz-uninhabitable.wast @@ -0,0 +1,50 @@ +;; Test that translate-to-fuzz handles uninhabitable types in initial and +;; imported modules without infinitely recursing in makeCompoundRef. + +;; RUN: wasm-opt %s -all -ttf --initial-fuzz=%s -S -o - | filecheck %s + +;; When importing from a module with uninhabitable types, setupHeapTypes records +;; all uninhabitable types from the imported module in uninhabitableHeapTypes so +;; that if an inhabitable imported type (like $2) is randomly selected into +;; interestingHeapTypes and constructed, makeCompoundRef will emit +;; (ref.null none) for its nullable reference to uninhabitable $0 rather than +;; infinitely recursing. We do not check the output here because setupHeapTypes +;; only imports a random subset of heap types and the fuzzer may not choose to +;; construct $2 for every random input seed. + +;; RUN: wasm-opt %s -all -ttf --fuzz-import=%s -S -o - + +(module + (rec + (type $0 (descriptor $1) (struct)) + (type $1 (describes $0) (struct (field (ref (exact $0))))) + ) + (type $2 (struct (field (ref struct)) (field (ref null (exact $0))))) + + ;; Nullable field referencing uninhabitable $0 in a global. Because imported + ;; globals in the initial module are initialized directly with makeConst, this + ;; deterministically constructs $2 with (ref.null none) for the second field. + (global $g (import "a" "b") (ref $2)) + + ;; Functions returning non-nullable uninhabitable (ref $0). In the common case + ;; when makeCompoundRef is reached directly to produce the result, $no-param + ;; has no local of type (ref $0) and falls back to + ;; (ref.as_non_null (ref.null none)), while $with-param uses (local.get $0). + ;; We only check the function signatures below because make() can wrap or + ;; replace the body with arbitrary control flow or unreachable instructions and + ;; processFunctions() can further mutate the bodies depending on the input + ;; bytes. + ;; + ;; Regardless, this test still proves the absence of infinite loops trying to + ;; materialize values of the uninhabitable types. + (func $no-param (import "a" "c") (result (ref $0))) + (func $with-param (import "a" "d") (param (ref $0)) (result (ref $0))) +) + +;; CHECK: (global $g (ref $2) (struct.new $2 +;; CHECK-NEXT: (struct.new{{.*}}) +;; CHECK-NEXT: (ref.null none) +;; CHECK-NEXT: )) + +;; CHECK: (func $no-param (type ${{.*}}) (result (ref $0)) +;; CHECK: (func $with-param (type ${{.*}}) (param $0 (ref $0)) (result (ref $0))