Repository navigation
[Fuzzer] Handle uninhabitable types in makeCompoundRef #9282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
196b322
4a28be4
5782327
d8677ac
066a948
0d0bf3e
4a27dfe
0ed2941
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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( | ||
|
|
@@ -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 | ||
|
|
@@ -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())); | ||
| } | ||
|
Comment on lines
+6888
to
+6892
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Binaryen, bool TranslateToFuzzReader::isUninhabitable(Type type) {\n return type.isRef() &&\n (GCTypeUtils::isUninhabitable(type) ||\n (type.isNonNullable() &&\n uninhabitableHeapTypes.contains(type.getHeapType())));\n}
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is not true.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On that note it looks like the check for |
||
|
|
||
| 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; | ||
|
|
||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
Uh oh!
There was an error while loading. Please reload this page.