Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/ir/gc-type-utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/passes/DeadArgumentElimination.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/tools/fuzzing.h
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ class TranslateToFuzzReader {
// The heap types we can pick from to generate instructions.
std::vector<HeapType> interestingHeapTypes;

// Heap types from initial content that cannot be constructed.
std::unordered_set<HeapType> uninhabitableHeapTypes;

// A mapping of a heap type to the subset of interestingHeapTypes that are
// subtypes of it.
std::unordered_map<HeapType, std::vector<HeapType>> interestingHeapSubTypes;
Expand Down Expand Up @@ -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);
Expand Down
53 changes: 39 additions & 14 deletions src/tools/fuzzing/fuzzing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<HeapType>& types) {
auto inhabitable = HeapTypeGenerator::getInhabitable(types);
std::unordered_set<HeapType> 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(
Expand Down Expand Up @@ -4524,16 +4536,18 @@ 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);
}

// 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.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
Expand All @@ -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
Expand Down Expand Up @@ -6866,6 +6885,12 @@ bool TranslateToFuzzReader::isLoggableType(Type type) {
loggableTypes.end();
}

bool TranslateToFuzzReader::isUninhabitable(Type type) {
Comment thread
stevenfontanella marked this conversation as resolved.
return GCTypeUtils::isTriviallyUninhabitable(type) ||
(type.isNonNullableRef() &&
uninhabitableHeapTypes.contains(type.getHeapType()));
}
Comment on lines +6888 to +6892

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Binaryen, Type::isNonNullable() can return true for non-reference types (such as basic types like i32 or f64), but calling Type::getHeapType() on a non-reference type will trigger an assertion failure in debug builds. Since GCTypeUtils::isUninhabitable(type) internally calls type.getHeapType() without checking type.isRef(), calling TranslateToFuzzReader::isUninhabitable on a non-reference type would result in an assertion failure.\n\nTo make this method robust and defensively guard against non-reference types, we should check type.isRef() first before calling GCTypeUtils::isUninhabitable(type) or accessing its heap type.

bool TranslateToFuzzReader::isUninhabitable(Type type) {\n  return type.isRef() &&\n         (GCTypeUtils::isUninhabitable(type) ||\n          (type.isNonNullable() &&\n           uninhabitableHeapTypes.contains(type.getHeapType())));\n}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Binaryen, Type::isNonNullable() can return true for non-reference types (such as basic types like i32 or f64)...

This is not true. Type::isNonNullable() is defined like this:

  bool isNonNullable() const { return isRef() && !(id & NullMask); }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On that note it looks like the check for isRef here is redundant, can we remove it? Separately it'd be nice to rename isNonNullable to isNonNullableRef in another PR.


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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/tools/fuzzing/heap-types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1400,7 +1400,7 @@ bool isUninhabitable(HeapType type,
bool isUninhabitable(Type type,
std::unordered_set<HeapType>& visited,
std::unordered_set<HeapType>& visiting) {
if (GCTypeUtils::isUninhabitable(type)) {
if (GCTypeUtils::isTriviallyUninhabitable(type)) {
return true;
}
if (type.isNonNullableRef()) {
Expand Down
50 changes: 50 additions & 0 deletions test/lit/fuzz-uninhabitable.wast
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +12 to +13

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something, can we add a seed here to make it deterministic and then check the output here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of. The previous version of the test did this by using the test file contents itself as the seed. The problem was that on Windows the line endings were read differently, leading to different random choices and the CI failure you saw.

Another problem is that future fuzzer changes could also change the random choices, creating an ongoing maintenance burden if this test starts failing as a result.


;; 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))
Loading