Skip to content

Commit e13ad34

Browse files
authored
Add a fuzzer utility for ensuring types are inhabitable (#5541)
Some valid GC types, such as non-nullable references to bottom heap types and types that contain non-nullable references to themselves, are uninhabitable, meaning it is not possible to construct values of those types. This can cause problems for the fuzzer, which generally needs to be able to construct values of arbitrary types. To simplify things for the fuzzer, introduce a utility for transforming type graphs such that all their types are inhabitable. The utility performs a DFS to find cycles of non-nullable references and breaks those cycles by introducing nullability. The new utility is itself fuzzed in the type fuzzer.
1 parent dc8f514 commit e13ad34

File tree

7 files changed

+569
-3
lines changed

7 files changed

+569
-3
lines changed

src/tools/fuzzing/heap-types.cpp

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
#include <variant>
1818

19+
#include "ir/subtypes.h"
20+
#include "support/insert_ordered.h"
1921
#include "tools/fuzzing/heap-types.h"
2022
#include "tools/fuzzing/parameters.h"
2123

@@ -654,4 +656,366 @@ HeapTypeGenerator::create(Random& rand, FeatureSet features, size_t n) {
654656
return HeapTypeGeneratorImpl(rand, features, n).result;
655657
}
656658

659+
namespace {
660+
661+
// `makeInhabitable` implementation.
662+
//
663+
// There are two root causes of uninhabitability: First, a non-nullable
664+
// reference to a bottom type is always uninhabitable. Second, a cycle in the
665+
// type graph formed from non-nullable references makes all the types involved
666+
// in that cycle uninhabitable because there is no way to construct the types
667+
// one at at time. All types that reference uninhabitable types via non-nullable
668+
// references are also themselves uninhabitable, but these transitively
669+
// uninhabitable types will become inhabitable once we fix the root causes, so
670+
// we don't worry about them.
671+
//
672+
// To modify uninhabitable types to make them habitable, it suffices to make all
673+
// non-nullable references to bottom types nullable and to break all cycles of
674+
// non-nullable references by making one of the references nullable. To preserve
675+
// valid subtyping, the newly nullable fields must also be made nullable in any
676+
// supertypes in which they appear.
677+
struct Inhabitator {
678+
// Uniquely identify fields as an index into a type.
679+
using FieldPos = std::pair<HeapType, Index>;
680+
681+
// When we make a reference nullable, we typically need to make the same
682+
// reference in other types nullable to maintain valid subtyping. Which types
683+
// we need to update depends on the variance of the reference, which is
684+
// determined by how it is used in its enclosing heap type.
685+
//
686+
// An invariant field of a heaptype must have the same type in subtypes of
687+
// that heaptype. A covariant field of a heaptype must be typed with a subtype
688+
// of its original type in subtypes of the heaptype. A contravariant field of
689+
// a heap type must be typed with a supertype of its original type in subtypes
690+
// of the heaptype.
691+
enum Variance { Invariant, Covariant, Contravariant };
692+
693+
// The input types.
694+
const std::vector<HeapType>& types;
695+
696+
// The fields we will make nullable.
697+
std::unordered_set<FieldPos> nullables;
698+
699+
SubTypes subtypes;
700+
701+
Inhabitator(const std::vector<HeapType>& types)
702+
: types(types), subtypes(types) {}
703+
704+
Variance getVariance(FieldPos field);
705+
void markNullable(FieldPos field);
706+
void markBottomRefsNullable();
707+
void markExternRefsNullable();
708+
void breakNonNullableCycles();
709+
710+
std::vector<HeapType> build();
711+
};
712+
713+
Inhabitator::Variance Inhabitator::getVariance(FieldPos field) {
714+
auto [type, idx] = field;
715+
assert(!type.isBasic());
716+
if (type.isStruct()) {
717+
if (type.getStruct().fields[idx].mutable_ == Mutable) {
718+
return Invariant;
719+
} else {
720+
return Covariant;
721+
}
722+
}
723+
if (type.isArray()) {
724+
if (type.getArray().element.mutable_ == Mutable) {
725+
return Invariant;
726+
} else {
727+
return Covariant;
728+
}
729+
}
730+
if (type.isSignature()) {
731+
if (idx < type.getSignature().params.size()) {
732+
return Contravariant;
733+
} else {
734+
return Covariant;
735+
}
736+
}
737+
WASM_UNREACHABLE("unexpected kind");
738+
}
739+
740+
void Inhabitator::markNullable(FieldPos field) {
741+
// Mark the given field nullable in the original type and in other types
742+
// necessary to keep subtyping valid.
743+
nullables.insert(field);
744+
auto [curr, idx] = field;
745+
switch (getVariance(field)) {
746+
case Covariant:
747+
// Mark the field null in all supertypes. If the supertype field is
748+
// already nullable or does not exist, that's ok and this will have no
749+
// effect.
750+
while (auto super = curr.getSuperType()) {
751+
nullables.insert({*super, idx});
752+
curr = *super;
753+
}
754+
break;
755+
case Invariant:
756+
// Find the top type for which this field exists and mark the field
757+
// nullable in all of its subtypes.
758+
if (curr.isArray()) {
759+
while (auto super = curr.getSuperType()) {
760+
curr = *super;
761+
}
762+
} else {
763+
assert(curr.isStruct());
764+
while (auto super = curr.getSuperType()) {
765+
if (super->getStruct().fields.size() <= idx) {
766+
break;
767+
}
768+
curr = *super;
769+
}
770+
}
771+
[[fallthrough]];
772+
case Contravariant:
773+
// Mark the field nullable in all subtypes. If the subtype field is
774+
// already nullable, that's ok and this will have no effect. TODO: Remove
775+
// this extra `index` variable once we have C++20. It's a workaround for
776+
// lambdas being unable to capture structured bindings.
777+
const size_t index = idx;
778+
subtypes.iterSubTypes(curr, [&](HeapType type, Index) {
779+
nullables.insert({type, index});
780+
});
781+
break;
782+
}
783+
}
784+
785+
void Inhabitator::markBottomRefsNullable() {
786+
for (auto type : types) {
787+
auto children = type.getTypeChildren();
788+
for (size_t i = 0; i < children.size(); ++i) {
789+
auto child = children[i];
790+
if (child.isRef() && child.getHeapType().isBottom() &&
791+
child.isNonNullable()) {
792+
markNullable({type, i});
793+
}
794+
}
795+
}
796+
}
797+
798+
void Inhabitator::markExternRefsNullable() {
799+
// The fuzzer cannot instantiate non-nullable externrefs, so make sure they
800+
// are all nullable.
801+
// TODO: Remove this once the fuzzer imports externref globals or gets some
802+
// other way to instantiate externrefs.
803+
for (auto type : types) {
804+
auto children = type.getTypeChildren();
805+
for (size_t i = 0; i < children.size(); ++i) {
806+
auto child = children[i];
807+
if (child.isRef() && child.getHeapType() == HeapType::ext &&
808+
child.isNonNullable()) {
809+
markNullable({type, i});
810+
}
811+
}
812+
}
813+
}
814+
815+
// Use a depth-first search to find cycles, marking the last found reference in
816+
// the cycle to be made non-nullable.
817+
void Inhabitator::breakNonNullableCycles() {
818+
// Types we've finished visiting. We don't need to visit them again.
819+
std::unordered_set<HeapType> visited;
820+
821+
// The path of types we are currently visiting. If one of them comes back up,
822+
// we've found a cycle. Map the types to the other types they reference and
823+
// our current index into that list so we can track where we are in each level
824+
// of the search.
825+
InsertOrderedMap<HeapType, std::pair<std::vector<Type>, Index>> visiting;
826+
827+
for (auto root : types) {
828+
if (visited.count(root)) {
829+
continue;
830+
}
831+
assert(visiting.size() == 0);
832+
visiting.insert({root, {root.getTypeChildren(), 0}});
833+
834+
while (visiting.size()) {
835+
auto& [curr, state] = *std::prev(visiting.end());
836+
auto& [children, idx] = state;
837+
838+
while (idx < children.size()) {
839+
// Skip non-reference children because they cannot refer to other types.
840+
if (!children[idx].isRef()) {
841+
++idx;
842+
continue;
843+
}
844+
// Skip nullable references because they don't cause uninhabitable
845+
// cycles.
846+
if (children[idx].isNullable()) {
847+
++idx;
848+
continue;
849+
}
850+
// Skip references that we have already marked nullable to satisfy
851+
// subtyping constraints. TODO: We could take such nullable references
852+
// into account when detecting cycles by tracking where in the current
853+
// search path we have made references nullable.
854+
if (nullables.count({curr, idx})) {
855+
++idx;
856+
continue;
857+
}
858+
// Skip references to types that we have finished visiting. We have
859+
// visited the full graph reachable from such references, so we know
860+
// they cannot cycle back to anything we are currently visiting.
861+
auto heapType = children[idx].getHeapType();
862+
if (visited.count(heapType)) {
863+
++idx;
864+
continue;
865+
}
866+
// If this ref forms a cycle, break the cycle by marking it nullable and
867+
// continue.
868+
if (auto it = visiting.find(heapType); it != visiting.end()) {
869+
markNullable({curr, idx});
870+
++idx;
871+
continue;
872+
}
873+
break;
874+
}
875+
876+
// If we've finished the DFS on the current type, pop it off the search
877+
// path and continue searching the previous type.
878+
if (idx == children.size()) {
879+
visited.insert(curr);
880+
visiting.erase(std::prev(visiting.end()));
881+
continue;
882+
}
883+
884+
// Otherwise we have a non-nullable reference we need to search.
885+
assert(children[idx].isRef() && children[idx].isNonNullable());
886+
auto next = children[idx++].getHeapType();
887+
visiting.insert({next, {next.getTypeChildren(), 0}});
888+
}
889+
}
890+
}
891+
892+
std::vector<HeapType> Inhabitator::build() {
893+
std::unordered_map<HeapType, size_t> typeIndices;
894+
for (size_t i = 0; i < types.size(); ++i) {
895+
typeIndices.insert({types[i], i});
896+
}
897+
898+
TypeBuilder builder(types.size());
899+
900+
// Copy types. Update references to point to the corresponding new type and
901+
// make them nullable where necessary.
902+
auto updateType = [&](FieldPos pos, Type& type) {
903+
if (!type.isRef()) {
904+
return;
905+
}
906+
auto heapType = type.getHeapType();
907+
auto nullability = type.getNullability();
908+
if (auto it = typeIndices.find(heapType); it != typeIndices.end()) {
909+
heapType = builder[it->second];
910+
}
911+
if (nullables.count(pos)) {
912+
nullability = Nullable;
913+
}
914+
type = builder.getTempRefType(heapType, nullability);
915+
};
916+
917+
for (size_t i = 0; i < types.size(); ++i) {
918+
auto type = types[i];
919+
if (type.isStruct()) {
920+
Struct copy = type.getStruct();
921+
for (size_t j = 0; j < copy.fields.size(); ++j) {
922+
updateType({type, j}, copy.fields[j].type);
923+
}
924+
builder[i] = copy;
925+
continue;
926+
}
927+
if (type.isArray()) {
928+
Array copy = type.getArray();
929+
updateType({type, 0}, copy.element.type);
930+
builder[i] = copy;
931+
continue;
932+
}
933+
if (type.isSignature()) {
934+
auto sig = type.getSignature();
935+
size_t j = 0;
936+
std::vector<Type> params;
937+
for (auto param : sig.params) {
938+
params.push_back(param);
939+
updateType({type, j++}, params.back());
940+
}
941+
std::vector<Type> results;
942+
for (auto result : sig.results) {
943+
results.push_back(result);
944+
updateType({type, j++}, results.back());
945+
}
946+
builder[i] = Signature(builder.getTempTupleType(params),
947+
builder.getTempTupleType(results));
948+
continue;
949+
}
950+
WASM_UNREACHABLE("unexpected type kind");
951+
}
952+
953+
// Establish rec groups.
954+
for (size_t start = 0; start < types.size();) {
955+
size_t size = types[start].getRecGroup().size();
956+
builder.createRecGroup(start, size);
957+
start += size;
958+
}
959+
960+
// Establish supertypes.
961+
for (size_t i = 0; i < types.size(); ++i) {
962+
if (auto super = types[i].getSuperType()) {
963+
if (auto it = typeIndices.find(*super); it != typeIndices.end()) {
964+
builder[i].subTypeOf(builder[it->second]);
965+
} else {
966+
builder[i].subTypeOf(*super);
967+
}
968+
}
969+
}
970+
971+
auto built = builder.build();
972+
assert(!built.getError() && "unexpected build error");
973+
return *built;
974+
}
975+
976+
} // anonymous namespace
977+
978+
std::vector<HeapType>
979+
HeapTypeGenerator::makeInhabitable(const std::vector<HeapType>& types) {
980+
if (types.empty()) {
981+
return {};
982+
}
983+
984+
// Remove duplicate and basic types. We will insert them back at the end.
985+
std::unordered_map<HeapType, size_t> typeIndices;
986+
std::vector<size_t> deduplicatedIndices;
987+
std::vector<HeapType> deduplicated;
988+
for (auto type : types) {
989+
if (type.isBasic()) {
990+
deduplicatedIndices.push_back(-1);
991+
continue;
992+
}
993+
auto [it, inserted] = typeIndices.insert({type, deduplicated.size()});
994+
if (inserted) {
995+
deduplicated.push_back(type);
996+
}
997+
deduplicatedIndices.push_back(it->second);
998+
}
999+
assert(deduplicatedIndices.size() == types.size());
1000+
1001+
// Construct the new types.
1002+
Inhabitator inhabitator(deduplicated);
1003+
inhabitator.markBottomRefsNullable();
1004+
inhabitator.markExternRefsNullable();
1005+
inhabitator.breakNonNullableCycles();
1006+
deduplicated = inhabitator.build();
1007+
1008+
// Re-duplicate and re-insert basic types as necessary.
1009+
std::vector<HeapType> result;
1010+
for (size_t i = 0; i < types.size(); ++i) {
1011+
if (deduplicatedIndices[i] == (size_t)-1) {
1012+
assert(types[i].isBasic());
1013+
result.push_back(types[i]);
1014+
} else {
1015+
result.push_back(deduplicated[deduplicatedIndices[i]]);
1016+
}
1017+
}
1018+
return result;
1019+
}
1020+
6571021
} // namespace wasm

src/tools/fuzzing/heap-types.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ struct HeapTypeGenerator {
3838
// Create a populated `HeapTypeGenerator` with `n` random HeapTypes with
3939
// interesting subtyping.
4040
static HeapTypeGenerator create(Random& rand, FeatureSet features, size_t n);
41+
42+
// Given a sequence of newly-built heap types, produce a sequence of similar
43+
// or identical types that are all inhabitable, i.e. that are possible to
44+
// create values for.
45+
static std::vector<HeapType>
46+
makeInhabitable(const std::vector<HeapType>& types);
4147
};
4248

4349
} // namespace wasm

0 commit comments

Comments
 (0)