From 196b322d417fed526afb67c9d128c9592579138b Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 19 Oct 2026 19:25:41 -0700 Subject: [PATCH 1/5] [Fuzzer] Handle uninhabitable types in makeCompoundRef Uninhabitable heap types from the initial module are filtered out of interestingHeapTypes in setupHeapTypes, but inhabitable types in interestingHeapTypes (as well as initial module globals and functions) can still reference them. Track uninhabitableHeapTypes in TranslateToFuzzReader and check for them in makeCompoundRef, getSubType, and getSuperType so makeCompoundRef emits ref.null, local.get, or ref.as_non_null rather than infinitely recursing across non-nullable type cycles. --- src/tools/fuzzing.h | 4 ++++ src/tools/fuzzing/fuzzing.cpp | 34 +++++++++++++++++++++++++------- test/lit/fuzz-uninhabitable.wast | 25 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 test/lit/fuzz-uninhabitable.wast 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 c202c210d7e..03af38d8a1e 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -546,6 +546,13 @@ 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. interestingHeapTypes = HeapTypeGenerator::getInhabitable(possibleHeapTypes); + std::unordered_set inhabitable(interestingHeapTypes.begin(), + interestingHeapTypes.end()); + for (auto type : possibleHeapTypes) { + if (!inhabitable.contains(type)) { + uninhabitableHeapTypes.insert(type); + } + } // For GC, also generate random types. if (wasm.features.hasGC()) { @@ -4524,8 +4531,9 @@ Expression* TranslateToFuzzReader::makeCompoundRef(Type type) { // inhabitable.) const auto LIMIT = fuzzParams.NESTING_LIMIT + 1; AutoNester nester(*this); - if (type.isNullable() && - (random.finished() || nesting >= LIMIT || oneIn(LIMIT - nesting + 1))) { + if (type.isNullable() && (random.finished() || nesting >= LIMIT || + uninhabitableHeapTypes.contains(heapType) || + oneIn(LIMIT - nesting + 1))) { return builder.makeRefNull(heapType); } @@ -4533,7 +4541,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.isNonNullable() && (random.finished() || nesting >= LIMIT)) { + if (type.isNonNullable() && (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 +4556,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 +6880,12 @@ bool TranslateToFuzzReader::isLoggableType(Type type) { loggableTypes.end(); } +bool TranslateToFuzzReader::isUninhabitable(Type type) { + return GCTypeUtils::isUninhabitable(type) || + (type.isRef() && type.isNonNullable() && + 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 +7042,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 +7110,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/test/lit/fuzz-uninhabitable.wast b/test/lit/fuzz-uninhabitable.wast new file mode 100644 index 00000000000..12f9c4d9497 --- /dev/null +++ b/test/lit/fuzz-uninhabitable.wast @@ -0,0 +1,25 @@ +;; Test that translate-to-fuzz handles uninhabitable types in initial module +;; contents without infinitely recursing in makeCompoundRef. + +;; RUN: wasm-opt %s -all -ttf --initial-fuzz=%s -S -o - | filecheck %s + +(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))))) + (global $g (import "a" "b") (ref $2)) + (func $f1 (import "a" "c") (result (ref $0))) + (func $f2 (import "a" "d") (param (ref $0)) (result (ref $0))) + (func $f3 (import "a" "e") (result (ref $0))) +) + +;; CHECK: (global $g (ref $2) (struct.new $2 +;; CHECK-NEXT: (struct.new{{.*}}) +;; CHECK-NEXT: (ref.null none) +;; CHECK-NEXT: )) + +;; CHECK: (func $f1 (type ${{.*}}) (result (ref $0)) +;; CHECK: (func $f2 (type ${{.*}}) (param $0 (ref $0)) (result (ref $0)) +;; CHECK: (func $f3 (type ${{.*}}) (result (ref $0)) From 4a28be4b61b212e97dd5c1390724ebb361fec16b Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 20 Oct 2026 13:57:34 -0700 Subject: [PATCH 2/5] rename to isTriviallyUninhabitable --- src/ir/gc-type-utils.h | 5 +++-- src/tools/fuzzing/fuzzing.cpp | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ir/gc-type-utils.h b/src/ir/gc-type-utils.h index f46b1b97179..393a48915fb 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.isNonNullable() && type.getHeapType().isBottom(); } @@ -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/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index 03af38d8a1e..d100d3eb660 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -6881,7 +6881,7 @@ bool TranslateToFuzzReader::isLoggableType(Type type) { } bool TranslateToFuzzReader::isUninhabitable(Type type) { - return GCTypeUtils::isUninhabitable(type) || + return GCTypeUtils::isTriviallyUninhabitable(type) || (type.isRef() && type.isNonNullable() && uninhabitableHeapTypes.contains(type.getHeapType())); } From 57823279863781eb1e429c0bdd2b101f89bec32a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 20 Oct 2026 16:39:21 -0700 Subject: [PATCH 3/5] wip tbr --- src/tools/fuzzing/fuzzing.cpp | 33 ++++++++++++++++++-------------- test/lit/fuzz-uninhabitable.wast | 8 ++++++-- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index d100d3eb660..0d85d01f23d 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -528,32 +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); - std::unordered_set inhabitable(interestingHeapTypes.begin(), - interestingHeapTypes.end()); - for (auto type : possibleHeapTypes) { - if (!inhabitable.contains(type)) { - uninhabitableHeapTypes.insert(type); - } - } - // For GC, also generate random types. if (wasm.features.hasGC()) { auto generator = HeapTypeGenerator::create( diff --git a/test/lit/fuzz-uninhabitable.wast b/test/lit/fuzz-uninhabitable.wast index 12f9c4d9497..94b01d8cd2b 100644 --- a/test/lit/fuzz-uninhabitable.wast +++ b/test/lit/fuzz-uninhabitable.wast @@ -1,7 +1,8 @@ -;; Test that translate-to-fuzz handles uninhabitable types in initial module -;; contents without infinitely recursing in makeCompoundRef. +;; 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 +;; RUN: wasm-opt %s -all -ttf --fuzz-import=%s -S -o - | filecheck %s --check-prefix=IMPORT (module (rec @@ -23,3 +24,6 @@ ;; CHECK: (func $f1 (type ${{.*}}) (result (ref $0)) ;; CHECK: (func $f2 (type ${{.*}}) (param $0 (ref $0)) (result (ref $0)) ;; CHECK: (func $f3 (type ${{.*}}) (result (ref $0)) + +;; IMPORT: (type $[[TYPE:.*]] (struct (field (ref struct)) (field (ref null (exact ${{.*}}))))) +;; IMPORT: (struct.new $[[TYPE]] From 4a27dfe3b8ea469bebb57eca82e6d5d8c1c42900 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 21 Oct 2026 23:20:28 -0700 Subject: [PATCH 4/5] fix build and improve test --- src/passes/DeadArgumentElimination.cpp | 2 +- src/tools/fuzzing/heap-types.cpp | 2 +- test/lit/fuzz-uninhabitable.wast | 23 +++++++++++++++++------ 3 files changed, 19 insertions(+), 8 deletions(-) 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/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 index 94b01d8cd2b..92442c96ac7 100644 --- a/test/lit/fuzz-uninhabitable.wast +++ b/test/lit/fuzz-uninhabitable.wast @@ -10,10 +10,12 @@ (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. (global $g (import "a" "b") (ref $2)) - (func $f1 (import "a" "c") (result (ref $0))) - (func $f2 (import "a" "d") (param (ref $0)) (result (ref $0))) - (func $f3 (import "a" "e") (result (ref $0))) + ;; Synthesize uninhabitable (ref $0) with no local via ref.as_non_null. + (func $no-param (import "a" "c") (result (ref $0))) + ;; Synthesize uninhabitable (ref $0) from the available parameter local. + (func $with-param (import "a" "d") (param (ref $0)) (result (ref $0))) ) ;; CHECK: (global $g (ref $2) (struct.new $2 @@ -21,9 +23,18 @@ ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: )) -;; CHECK: (func $f1 (type ${{.*}}) (result (ref $0)) -;; CHECK: (func $f2 (type ${{.*}}) (param $0 (ref $0)) (result (ref $0)) -;; CHECK: (func $f3 (type ${{.*}}) (result (ref $0)) +;; CHECK: (func $no-param (type ${{.*}}) (result (ref $0)) +;; CHECK: (ref.as_non_null +;; CHECK-NEXT: (ref.null none) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $with-param (type ${{.*}}) (param $0 (ref $0)) (result (ref $0)) +;; CHECK: (local.get $0) +;; CHECK-NEXT: ) ;; IMPORT: (type $[[TYPE:.*]] (struct (field (ref struct)) (field (ref null (exact ${{.*}}))))) ;; IMPORT: (struct.new $[[TYPE]] +;; IMPORT-NEXT: (struct.new{{.*}}) +;; IMPORT-NEXT: (ref.null none) +;; IMPORT-NEXT: ) From 0ed2941f07c2be97f005b467dc563c48fac894a3 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 22 Oct 2026 11:35:59 -0700 Subject: [PATCH 5/5] fix test --- test/lit/fuzz-uninhabitable.wast | 44 ++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/test/lit/fuzz-uninhabitable.wast b/test/lit/fuzz-uninhabitable.wast index 92442c96ac7..5c7c456ec2b 100644 --- a/test/lit/fuzz-uninhabitable.wast +++ b/test/lit/fuzz-uninhabitable.wast @@ -2,7 +2,17 @@ ;; imported modules without infinitely recursing in makeCompoundRef. ;; RUN: wasm-opt %s -all -ttf --initial-fuzz=%s -S -o - | filecheck %s -;; RUN: wasm-opt %s -all -ttf --fuzz-import=%s -S -o - | filecheck %s --check-prefix=IMPORT + +;; 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 @@ -10,11 +20,24 @@ (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. + + ;; 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)) - ;; Synthesize uninhabitable (ref $0) with no local via ref.as_non_null. + + ;; 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))) - ;; Synthesize uninhabitable (ref $0) from the available parameter local. (func $with-param (import "a" "d") (param (ref $0)) (result (ref $0))) ) @@ -24,17 +47,4 @@ ;; CHECK-NEXT: )) ;; CHECK: (func $no-param (type ${{.*}}) (result (ref $0)) -;; CHECK: (ref.as_non_null -;; CHECK-NEXT: (ref.null none) -;; CHECK-NEXT: ) -;; CHECK-NEXT: ) - ;; CHECK: (func $with-param (type ${{.*}}) (param $0 (ref $0)) (result (ref $0)) -;; CHECK: (local.get $0) -;; CHECK-NEXT: ) - -;; IMPORT: (type $[[TYPE:.*]] (struct (field (ref struct)) (field (ref null (exact ${{.*}}))))) -;; IMPORT: (struct.new $[[TYPE]] -;; IMPORT-NEXT: (struct.new{{.*}}) -;; IMPORT-NEXT: (ref.null none) -;; IMPORT-NEXT: )