ladybird/Libraries/LibWasm/TypeSystem.cpp
Ali Mohammad Pur 62cb073ada LibWasm: Validate the wasm-gc and function-references instructions
Actual implementations are still trap-on-exec.
2026-06-19 17:01:47 +02:00

548 lines
24 KiB
C++

/*
* Copyright (c) 2026, Ali Mohammad Pur <ali@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/HashMap.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/StringBuilder.h>
#include <LibSync/Mutex.h>
#include <LibWasm/AbstractMachine/Validator.h>
#include <LibWasm/TypeSystem.h>
namespace Wasm {
// https://webassembly.github.io/spec/core/valid/conventions.html#rolling-and-unrolling
class TypeRegistry {
AK_MAKE_NONCOPYABLE(TypeRegistry);
AK_MAKE_NONMOVABLE(TypeRegistry);
public:
static TypeRegistry& the()
{
// Defined types are deliberately immortal: they are the process-wide identities that
// runtime values and compiled code point at.
static TypeRegistry* registry = new TypeRegistry;
return *registry;
}
DefinedType const* type_at(u32 registry_index)
{
Sync::MutexLocker locker(m_mutex);
if (registry_index >= m_types.size())
return nullptr;
return m_types[registry_index].ptr();
}
ErrorOr<Vector<DefinedType const*>, ValidationError> intern_group(ReadonlySpan<TypeSection::Type> types, TypeSection::Type::RecGroupSpan group, ReadonlySpan<DefinedType const*> resolved_so_far);
private:
TypeRegistry() = default;
Sync::Mutex m_mutex;
Vector<NonnullOwnPtr<DefinedType>> m_types;
HashMap<ByteString, u32> m_interned_groups; // group key -> index of first member
};
DefinedType const* TypeContext::resolve(TypeIndex index) const
{
if (types.is_empty())
return TypeRegistry::the().type_at(index.value());
if (index.value() >= types.size())
return nullptr;
return types[index.value()];
}
namespace {
struct RolledTypeUse {
enum class Form : u8 {
Closed,
Recursive,
};
Form form;
u32 index; // registry index (Closed) or group-internal index (Recursive)
};
class GroupRoller {
public:
GroupRoller(TypeSection::Type::RecGroupSpan group, ReadonlySpan<DefinedType const*> resolved_so_far)
: m_group(group)
, m_resolved_so_far(resolved_so_far)
{
}
// https://webassembly.github.io/spec/core/valid/conventions.html#aux-roll-rectype
// roll_x(rectype) substitutes the type indices [x, x+n) with rec.i; anything below x must already be defined.
ErrorOr<RolledTypeUse, ValidationError> roll(TypeIndex index) const
{
if (index.value() < m_group.first_type_index)
return RolledTypeUse { RolledTypeUse::Form::Closed, m_resolved_so_far[index.value()]->registry_index() };
if (index.value() < m_group.first_type_index + m_group.size)
return RolledTypeUse { RolledTypeUse::Form::Recursive, index.value() - m_group.first_type_index };
return ValidationError { ByteString::formatted("unknown type {} referenced from recursive type group", index.value()) };
}
ErrorOr<void, ValidationError> serialize(StringBuilder& builder, ValueType type) const
{
builder.append(static_cast<char>(type.kind()));
builder.append(type.is_nullable() ? '\1' : '\0');
if (type.is_typeuse()) {
auto use = TRY(roll(type.unsafe_typeindex()));
builder.append(use.form == RolledTypeUse::Form::Closed ? 'C' : 'R');
builder.append(ReadonlyBytes { &use.index, sizeof(use.index) });
}
return {};
}
ErrorOr<void, ValidationError> serialize(StringBuilder& builder, FieldType const& field) const
{
TRY(serialize(builder, field.type()));
builder.append(field.is_mutable() ? '\1' : '\0');
return {};
}
ErrorOr<void, ValidationError> serialize(StringBuilder& builder, TypeSection::Type const& type) const
{
builder.append(type.is_final() ? '\1' : '\0');
u32 supertype_count = type.supertypes().size();
builder.append(ReadonlyBytes { &supertype_count, sizeof(supertype_count) });
for (auto supertype : type.supertypes()) {
auto use = TRY(roll(supertype));
builder.append(use.form == RolledTypeUse::Form::Closed ? 'C' : 'R');
builder.append(ReadonlyBytes { &use.index, sizeof(use.index) });
}
TRY(type.description().visit(
[&](FunctionType const& function) -> ErrorOr<void, ValidationError> {
builder.append('F');
u32 count = function.parameters().size();
builder.append(ReadonlyBytes { &count, sizeof(count) });
for (auto& parameter : function.parameters())
TRY(serialize(builder, parameter));
count = function.results().size();
builder.append(ReadonlyBytes { &count, sizeof(count) });
for (auto& result : function.results())
TRY(serialize(builder, result));
return {};
},
[&](StructType const& struct_) -> ErrorOr<void, ValidationError> {
builder.append('S');
u32 count = struct_.fields().size();
builder.append(ReadonlyBytes { &count, sizeof(count) });
for (auto& field : struct_.fields())
TRY(serialize(builder, field));
return {};
},
[&](ArrayType const& array) -> ErrorOr<void, ValidationError> {
builder.append('A');
return serialize(builder, array.type());
}));
return {};
}
// https://webassembly.github.io/spec/core/valid/conventions.html#aux-unroll-rectype
ErrorOr<ValueType, ValidationError> substituted(ValueType type, u32 group_base_registry_index) const
{
if (!type.is_typeuse())
return type;
auto use = TRY(roll(type.unsafe_typeindex()));
auto registry_index = use.form == RolledTypeUse::Form::Closed ? use.index : group_base_registry_index + use.index;
return ValueType(ValueType::TypeUseReference, TypeIndex(registry_index), type.is_nullable());
}
ErrorOr<TypeSection::Type, ValidationError> substituted(TypeSection::Type const& type, u32 group_base_registry_index) const
{
Vector<TypeIndex> supertypes;
supertypes.ensure_capacity(type.supertypes().size());
for (auto supertype : type.supertypes()) {
auto use = TRY(roll(supertype));
supertypes.append(TypeIndex(use.form == RolledTypeUse::Form::Closed ? use.index : group_base_registry_index + use.index));
}
auto composite = TRY(type.description().visit(
[&](FunctionType const& function) -> ErrorOr<TypeSection::Type::CompositeType, ValidationError> {
Vector<ValueType> parameters;
parameters.ensure_capacity(function.parameters().size());
for (auto& parameter : function.parameters())
parameters.append(TRY(substituted(parameter, group_base_registry_index)));
Vector<ValueType> results;
results.ensure_capacity(function.results().size());
for (auto& result : function.results())
results.append(TRY(substituted(result, group_base_registry_index)));
return TypeSection::Type::CompositeType { FunctionType { move(parameters), move(results) } };
},
[&](StructType const& struct_) -> ErrorOr<TypeSection::Type::CompositeType, ValidationError> {
Vector<FieldType> fields;
fields.ensure_capacity(struct_.fields().size());
for (auto& field : struct_.fields())
fields.append(FieldType { field.is_mutable(), TRY(substituted(field.type(), group_base_registry_index)) });
return TypeSection::Type::CompositeType { StructType { move(fields) } };
},
[&](ArrayType const& array) -> ErrorOr<TypeSection::Type::CompositeType, ValidationError> {
return TypeSection::Type::CompositeType { ArrayType { FieldType { array.type().is_mutable(), TRY(substituted(array.type().type(), group_base_registry_index)) } } };
}));
return TypeSection::Type { move(composite), move(supertypes), type.is_final() };
}
private:
TypeSection::Type::RecGroupSpan m_group;
ReadonlySpan<DefinedType const*> m_resolved_so_far;
};
}
ErrorOr<Vector<DefinedType const*>, ValidationError> TypeRegistry::intern_group(ReadonlySpan<TypeSection::Type> types, TypeSection::Type::RecGroupSpan group, ReadonlySpan<DefinedType const*> resolved_so_far)
{
GroupRoller roller { group, resolved_so_far };
StringBuilder key_builder;
u32 group_size = group.size;
key_builder.append(ReadonlyBytes { &group_size, sizeof(group_size) });
for (size_t i = 0; i < group.size; ++i)
TRY(roller.serialize(key_builder, types[group.first_type_index + i]));
auto key = key_builder.to_byte_string();
Sync::MutexLocker locker(m_mutex);
Optional<u32> base_registry_index = m_interned_groups.get(key);
if (!base_registry_index.has_value()) {
auto base = static_cast<u32>(m_types.size());
for (size_t i = 0; i < group.size; ++i) {
auto sub_type = TRY(roller.substituted(types[group.first_type_index + i], base));
sub_type.set_rec_group({ base, group.size });
m_types.append(adopt_own(*new DefinedType(move(sub_type), base + i)));
}
for (size_t i = 0; i < group.size; ++i) {
auto& defined_type = *m_types[base + i];
if (!defined_type.m_sub_type.supertypes().is_empty()) {
auto supertype_registry_index = defined_type.m_sub_type.supertypes().first().value();
if (supertype_registry_index >= base + i)
return ValidationError { ByteString::formatted("Invalid supertype {}, supertypes must precede their subtypes", supertype_registry_index) };
defined_type.m_supertype = m_types[supertype_registry_index].ptr();
defined_type.m_subtyping_depth = defined_type.m_supertype->m_subtyping_depth + 1;
defined_type.m_ancestors.ensure_capacity(defined_type.m_subtyping_depth + 1);
defined_type.m_ancestors.extend(defined_type.m_supertype->m_ancestors);
}
defined_type.m_ancestors.append(&defined_type);
}
m_interned_groups.set(move(key), base);
base_registry_index = base;
}
Vector<DefinedType const*> result;
result.ensure_capacity(group.size);
for (size_t i = 0; i < group.size; ++i)
result.append(m_types[*base_registry_index + i].ptr());
return result;
}
// https://webassembly.github.io/spec/core/valid/modules.html#types
ErrorOr<Vector<DefinedType const*>, ValidationError> canonicalize_module_types(TypeSection const& section)
{
Vector<DefinedType const*> resolved;
resolved.ensure_capacity(section.types().size());
for (size_t type_index = 0; type_index < section.types().size();) {
auto group = section.types()[type_index].rec_group();
if (group.first_type_index != type_index || group.first_type_index + group.size > section.types().size())
return ValidationError { ByteString("malformed recursive type group spans"sv) };
auto group_types = TRY(TypeRegistry::the().intern_group(section.types(), group, resolved));
resolved.extend(move(group_types));
type_index += group.size;
}
return resolved;
}
DefinedType const* canonicalize_type(TypeSection::Type const& type, TypeContext const& context)
{
Vector<TypeSection::Type> closed_types;
Vector<TypeIndex> supertypes;
for (auto supertype : type.supertypes()) {
auto const* resolved = context.resolve(supertype);
VERIFY(resolved);
supertypes.append(TypeIndex(resolved->registry_index()));
}
auto close = [&](ValueType value_type) {
return canonicalized(value_type, context);
};
auto composite = type.description().visit(
[&](FunctionType const& function) -> TypeSection::Type::CompositeType {
Vector<ValueType> parameters;
for (auto& parameter : function.parameters())
parameters.append(close(parameter));
Vector<ValueType> results;
for (auto& result : function.results())
results.append(close(result));
return FunctionType { move(parameters), move(results) };
},
[&](StructType const& struct_) -> TypeSection::Type::CompositeType {
Vector<FieldType> fields;
for (auto& field : struct_.fields())
fields.append(FieldType { field.is_mutable(), close(field.type()) });
return StructType { move(fields) };
},
[&](ArrayType const& array) -> TypeSection::Type::CompositeType {
return ArrayType { FieldType { array.type().is_mutable(), close(array.type().type()) } };
});
closed_types.append(TypeSection::Type { move(composite), move(supertypes), type.is_final() });
closed_types.first().set_rec_group({ 0, 1 });
auto result = TypeRegistry::the().intern_group(closed_types.span(), { 0, 1 }, {});
VERIFY(!result.is_error());
return result.release_value().first();
}
ValueType canonicalized(ValueType type, TypeContext const& context)
{
if (!type.is_typeuse() || context.types.is_empty())
return type;
auto const* resolved = context.resolve(type.unsafe_typeindex());
VERIFY(resolved);
return ValueType(ValueType::TypeUseReference, TypeIndex(resolved->registry_index()), type.is_nullable());
}
// https://webassembly.github.io/spec/core/valid/matching.html#defined-types
bool matches_defined_type(DefinedType const& defined_type1, DefinedType const& defined_type2)
{
if (&defined_type1 == &defined_type2)
return true;
if (defined_type2.subtyping_depth() >= defined_type1.subtyping_depth())
return false;
return defined_type1.ancestors()[defined_type2.subtyping_depth()] == &defined_type2;
}
// https://webassembly.github.io/spec/core/syntax/types.html#heap-types
ValueType::Kind top_of_heap_type(ValueType const& type, TypeContext const& context)
{
switch (type.kind()) {
case ValueType::AnyReference:
case ValueType::EqReference:
case ValueType::I31Reference:
case ValueType::StructReference:
case ValueType::ArrayReference:
case ValueType::NoneReference:
return ValueType::AnyReference;
case ValueType::FunctionReference:
case ValueType::NoFunctionReference:
return ValueType::FunctionReference;
case ValueType::ExternReference:
case ValueType::NoExternReference:
return ValueType::ExternReference;
case ValueType::ExceptionReference:
case ValueType::NoExceptionReference:
return ValueType::ExceptionReference;
case ValueType::TypeUseReference: {
auto const* defined_type = context.resolve(type.unsafe_typeindex());
VERIFY(defined_type);
return defined_type->expansion().visit(
[](FunctionType const&) { return ValueType::FunctionReference; },
[](StructType const&) { return ValueType::AnyReference; },
[](ArrayType const&) { return ValueType::AnyReference; });
}
default:
VERIFY_NOT_REACHED();
}
}
// https://webassembly.github.io/spec/core/valid/matching.html#heap-types
bool matches_heap_type(ValueType const& heap_type1, ValueType const& heap_type2, TypeContext const& context)
{
VERIFY(heap_type1.is_reference() && heap_type2.is_reference());
// The heap type heaptype_1 matches the heap type heaptype_2 if:
// Either: The heap type heaptype_2 is of the form heaptype_1.
if (heap_type1.kind() == heap_type2.kind()) {
if (!heap_type1.is_typeuse())
return true;
auto const* defined_type1 = context.resolve(heap_type1.unsafe_typeindex());
auto const* defined_type2 = context.resolve(heap_type2.unsafe_typeindex());
VERIFY(defined_type1 && defined_type2);
// Or: deftype_1 matches deftype_2.
return matches_defined_type(*defined_type1, *defined_type2);
}
switch (heap_type1.kind()) {
// Or: heaptype_1 is eq and heaptype_2 is any.
case ValueType::EqReference:
return heap_type2.kind() == ValueType::AnyReference;
// Or: heaptype_1 is i31/struct/array and heaptype_2 is eq (or any, transitively).
case ValueType::I31Reference:
case ValueType::StructReference:
case ValueType::ArrayReference:
return heap_type2.kind() == ValueType::EqReference || heap_type2.kind() == ValueType::AnyReference;
// Or: heaptype_1 is a defined type.
case ValueType::TypeUseReference: {
auto const* defined_type = context.resolve(heap_type1.unsafe_typeindex());
VERIFY(defined_type);
switch (heap_type2.kind()) {
// ... and heaptype_2 is func and the expansion of deftype is (func t1* -> t2*).
case ValueType::FunctionReference:
return defined_type->expansion().has<FunctionType>();
// ... and heaptype_2 is struct and the expansion of deftype is (struct fieldtype*).
case ValueType::StructReference:
return defined_type->expansion().has<StructType>();
// ... and heaptype_2 is array and the expansion of deftype is (array fieldtype).
case ValueType::ArrayReference:
return defined_type->expansion().has<ArrayType>();
// Transitively: deftype <= struct/array <= eq <= any.
case ValueType::EqReference:
case ValueType::AnyReference:
return defined_type->expansion().has<StructType>() || defined_type->expansion().has<ArrayType>();
default:
return false;
}
}
// Or: heaptype_1 is none and heaptype_2 matches any.
case ValueType::NoneReference:
return top_of_heap_type(heap_type2, context) == ValueType::AnyReference;
// Or: heaptype_1 is nofunc and heaptype_2 matches func.
case ValueType::NoFunctionReference:
return top_of_heap_type(heap_type2, context) == ValueType::FunctionReference;
// Or: heaptype_1 is noexn and heaptype_2 matches exn.
case ValueType::NoExceptionReference:
return top_of_heap_type(heap_type2, context) == ValueType::ExceptionReference;
// Or: heaptype_1 is noextern and heaptype_2 matches extern.
case ValueType::NoExternReference:
return top_of_heap_type(heap_type2, context) == ValueType::ExternReference;
default:
return false;
}
}
// https://webassembly.github.io/spec/core/valid/matching.html#reference-types
bool matches_reference_type(ValueType const& reference_type1, ValueType const& reference_type2, TypeContext const& context)
{
if (reference_type1.is_nullable() && !reference_type2.is_nullable())
return false;
return matches_heap_type(reference_type1, reference_type2, context);
}
// https://webassembly.github.io/spec/core/valid/matching.html#value-types
bool matches_value_type(ValueType const& value_type1, ValueType const& value_type2, TypeContext const& context)
{
if (value_type1.is_reference() && value_type2.is_reference())
return matches_reference_type(value_type1, value_type2, context);
return !value_type1.is_reference() && !value_type2.is_reference() && value_type1.kind() == value_type2.kind();
}
// https://webassembly.github.io/spec/core/valid/matching.html#result-types
bool matches_result_types(ReadonlySpan<ValueType> result_types1, ReadonlySpan<ValueType> result_types2, TypeContext const& context)
{
if (result_types1.size() != result_types2.size())
return false;
for (size_t i = 0; i < result_types1.size(); ++i) {
if (!matches_value_type(result_types1[i], result_types2[i], context))
return false;
}
return true;
}
// https://webassembly.github.io/spec/core/valid/matching.html#field-types
static bool matches_storage_type(ValueType const& storage_type1, ValueType const& storage_type2, TypeContext const& context)
{
if (storage_type1.is_packed() || storage_type2.is_packed())
return storage_type1.kind() == storage_type2.kind();
return matches_value_type(storage_type1, storage_type2, context);
}
// https://webassembly.github.io/spec/core/valid/matching.html#field-types
bool matches_field_type(FieldType const& field_type1, FieldType const& field_type2, TypeContext const& context)
{
if (field_type1.is_mutable() != field_type2.is_mutable())
return false;
if (!matches_storage_type(field_type1.type(), field_type2.type(), context))
return false;
if (field_type1.is_mutable() && !matches_storage_type(field_type2.type(), field_type1.type(), context))
return false;
return true;
}
// https://webassembly.github.io/spec/core/valid/matching.html#composite-types
bool matches_composite_type(TypeSection::Type::CompositeType const& composite_type1, TypeSection::Type::CompositeType const& composite_type2, TypeContext const& context)
{
return composite_type2.visit(
[&](FunctionType const& function2) {
auto const* function1 = composite_type1.get_pointer<FunctionType>();
if (!function1)
return false;
return matches_result_types(function2.parameters(), function1->parameters(), context)
&& matches_result_types(function1->results(), function2.results(), context);
},
[&](StructType const& struct2) {
auto const* struct1 = composite_type1.get_pointer<StructType>();
if (!struct1 || struct1->fields().size() < struct2.fields().size())
return false;
for (size_t i = 0; i < struct2.fields().size(); ++i) {
if (!matches_field_type(struct1->fields()[i], struct2.fields()[i], context))
return false;
}
return true;
},
[&](ArrayType const& array2) {
auto const* array1 = composite_type1.get_pointer<ArrayType>();
if (!array1)
return false;
return matches_field_type(array1->type(), array2.type(), context);
});
}
// https://webassembly.github.io/spec/core/valid/matching.html#limits
bool matches_limits(Limits const& limits1, Limits const& limits2)
{
if (limits1.min() < limits2.min())
return false;
if (!limits2.max().has_value())
return true;
return limits1.max().has_value() && *limits1.max() <= *limits2.max();
}
// https://webassembly.github.io/spec/core/valid/matching.html#memory-types
bool matches_memory_type(MemoryType const& memory_type1, MemoryType const& memory_type2)
{
if (memory_type1.limits().address_type() != memory_type2.limits().address_type())
return false;
return matches_limits(memory_type1.limits(), memory_type2.limits());
}
// https://webassembly.github.io/spec/core/valid/matching.html#table-types
bool matches_table_type(TableType const& table_type1, TableType const& table_type2, TypeContext const& context1, TypeContext const& context2)
{
if (table_type1.limits().address_type() != table_type2.limits().address_type())
return false;
if (!matches_limits(table_type1.limits(), table_type2.limits()))
return false;
auto element_type1 = canonicalized(table_type1.element_type(), context1);
auto element_type2 = canonicalized(table_type2.element_type(), context2);
return matches_reference_type(element_type1, element_type2, TypeContext {})
&& matches_reference_type(element_type2, element_type1, TypeContext {});
}
// https://webassembly.github.io/spec/core/valid/matching.html#global-types
bool matches_global_type(GlobalType const& global_type1, GlobalType const& global_type2, TypeContext const& context1, TypeContext const& context2)
{
if (global_type1.is_mutable() != global_type2.is_mutable())
return false;
auto value_type1 = canonicalized(global_type1.type(), context1);
auto value_type2 = canonicalized(global_type2.type(), context2);
if (!matches_value_type(value_type1, value_type2, TypeContext {}))
return false;
if (global_type1.is_mutable() && !matches_value_type(value_type2, value_type1, TypeContext {}))
return false;
return true;
}
// https://webassembly.github.io/spec/core/valid/matching.html#tag-types
bool matches_tag_type(DefinedType const& defined_type1, DefinedType const& defined_type2)
{
return matches_defined_type(defined_type1, defined_type2) && matches_defined_type(defined_type2, defined_type1);
}
}