diff --git a/design/mvp/Binary.md b/design/mvp/Binary.md index d7503097..50ca37e7 100644 --- a/design/mvp/Binary.md +++ b/design/mvp/Binary.md @@ -287,6 +287,12 @@ Notes: `none` case of an optional immediate.) * 🔧 for fixed-sized lists the length of the list must be larger than 0 to pass validation. +* Validation of each `defvaltype` definition rejects types whose resolved + structural AST contains a node whose static in-memory byte size exceeds + `MAX_VALUE_BYTE_LENGTH` for either `i32` or `i64` pointer types. See + [Element Size validation](CanonicalABI.md#element-size) for `elem_size`, + `MAX_VALUE_BYTE_LENGTH`, and the overflow-safe validation requirement. + This is a static validation error, not a runtime trap. ## Canonical Definitions diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index 621296b5..b7c9ace8 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -2361,6 +2361,130 @@ def elem_size_flags(labels): return 4 ``` +`MAX_VALUE_BYTE_LENGTH` is `(1 << 28) - 1`, the same value as +`MAX_STRING_BYTE_LENGTH` and `MAX_LIST_BYTE_LENGTH`. Component validation +requires that every node in the resolved structural type AST (after +despecialization) satisfies `elem_size(t, ptr_type) <= MAX_VALUE_BYTE_LENGTH` +for each pointer type `ptr_type ∈ {i32, i64}`. This is a static validation +error, not a runtime trap. Implementations may validate incrementally as long +as the invariant holds for every resolved node. Validation arithmetic must be +overflow-safe: use non-wrapping integer arithmetic and, for fixed-length lists, +prefer rejecting when +`length > MAX_VALUE_BYTE_LENGTH // elem_size(element, ptr_type)` before +multiplying. + +```python +MAX_VALUE_BYTE_LENGTH = (1 << 28) - 1 + +def checked_exceeds_max(n): + return n > MAX_VALUE_BYTE_LENGTH + +def checked_add(a, b): + s = a + b + if checked_exceeds_max(s): + return MAX_VALUE_BYTE_LENGTH + 1 + return s + +def checked_mul(a, b): + if b != 0 and a > MAX_VALUE_BYTE_LENGTH // b: + return MAX_VALUE_BYTE_LENGTH + 1 + return a * b + +def checked_align_to(ptr, alignment): + r = align_to(ptr, alignment) + if checked_exceeds_max(r): + return MAX_VALUE_BYTE_LENGTH + 1 + return r + +def checked_elem_size_list(elem_type, maybe_length, ptr_type): + if maybe_length is not None: + es = checked_elem_size(elem_type, ptr_type) + if es == 0: + return MAX_VALUE_BYTE_LENGTH + 1 + return checked_mul(maybe_length, es) + r = 2 * ptr_size(ptr_type) + return r if not checked_exceeds_max(r) else MAX_VALUE_BYTE_LENGTH + 1 + +def checked_elem_size_record(fields, ptr_type): + s = 0 + for f in fields: + s = checked_align_to(s, alignment(f.t, ptr_type)) + if checked_exceeds_max(s): + return MAX_VALUE_BYTE_LENGTH + 1 + s = checked_add(s, checked_elem_size(f.t, ptr_type)) + if checked_exceeds_max(s): + return MAX_VALUE_BYTE_LENGTH + 1 + if s == 0: + return MAX_VALUE_BYTE_LENGTH + 1 + return checked_align_to(s, alignment_record(fields, ptr_type)) + +def checked_elem_size_variant(cases, ptr_type): + s = checked_elem_size(discriminant_type(cases), ptr_type) + s = checked_align_to(s, max_case_alignment(cases, ptr_type)) + cs = 0 + for c in cases: + if c.t is not None: + cs = max(cs, checked_elem_size(c.t, ptr_type)) + s = checked_add(s, cs) + if checked_exceeds_max(s): + return MAX_VALUE_BYTE_LENGTH + 1 + return checked_align_to(s, alignment_variant(cases, ptr_type)) + +def checked_elem_size(t, ptr_type): + match despecialize(t): + case BoolType() : return 1 + case S8Type() | U8Type() : return 1 + case S16Type() | U16Type() : return 2 + case S32Type() | U32Type() : return 4 + case S64Type() | U64Type() : return 8 + case F32Type() : return 4 + case F64Type() : return 8 + case CharType() : return 4 + case StringType() : return checked_mul(2, ptr_size(ptr_type)) + case ErrorContextType() : return 4 + case ListType(t, l) : return checked_elem_size_list(t, l, ptr_type) + case RecordType(fields) : return checked_elem_size_record(fields, ptr_type) + case VariantType(cases) : return checked_elem_size_variant(cases, ptr_type) + case FlagsType(labels) : return elem_size_flags(labels) + case OwnType() | BorrowType() : return 4 + case StreamType() | FutureType() : return 4 + +def valid_valtype_size(t, ptr_type) -> bool: + return checked_elem_size(t, ptr_type) <= MAX_VALUE_BYTE_LENGTH + +def check_resolved_type_size(t) -> bool: + t = despecialize(t) + match t: + case ListType(elem, maybe_len): + if elem is not None and not check_resolved_type_size(elem): + return False + if maybe_len is not None: + for ptr_type in ['i32', 'i64']: + elem_sz = checked_elem_size(elem, ptr_type) + if elem_sz == 0 or maybe_len > MAX_VALUE_BYTE_LENGTH // elem_sz: + return False + case StreamType(elem) | FutureType(elem): + if elem is not None and not check_resolved_type_size(elem): + return False + case RecordType(fields): + for f in fields: + if not check_resolved_type_size(f.t): + return False + case VariantType(cases): + for c in cases: + if c.t is not None and not check_resolved_type_size(c.t): + return False + case _: + pass + for ptr_type in ['i32', 'i64']: + if not valid_valtype_size(t, ptr_type): + return False + return True + +def check_defvaltype_size(t) -> bool: + return check_resolved_type_size(t) +``` + ## Loading The `load` function defines how to read a value of a given value type `t` diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index 5ae72e9f..149b9ea0 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -732,6 +732,9 @@ and sequencing contained values. 🔧 When the optional `` immediate of the `list` type constructor is present, the list has a fixed length and the representation of the list in memory is specialized to this length. Note that the fixed length must be larger than 0. +The static in-memory byte size of every node in the resolved structural type +AST must not exceed `MAX_VALUE_BYTE_LENGTH` (`2^28 - 1` bytes); see +[Element Size validation](CanonicalABI.md#element-size). ##### Handle types @@ -1034,6 +1037,36 @@ is a central part of validation and, e.g., occurs when validating that the `with` arguments of an [`instantiate`](#instance-definitions) expression are type-compatible with the `import`s of the component being instantiated. +##### Maximum static value type size + +Each `defvaltype` definition must satisfy a size invariant over the resolved +structural type AST (after despecialization, including implicit nodes such as +the pair record introduced by `map`). For every node `t` in that AST and each +pointer type `ptr_type ∈ {i32, i64}`, the static in-memory byte size +`elem_size(t, ptr_type)` must not exceed `MAX_VALUE_BYTE_LENGTH` +(`2^28 - 1`, the same limit as `MAX_STRING_BYTE_LENGTH` and +`MAX_LIST_BYTE_LENGTH`). Implementations enforce this at component validation +time as a static error. Implementations may validate incrementally (e.g., when +checking each type-index definition, with memoization) as long as every +resolved node satisfies the invariant. Reference validation applies +`despecialize` at each recursively visited node (the same pattern as +`contains()`), so nested specialized types such as `map` inside `option` or +`record` are checked after expansion. + +Validation must use overflow-safe size computation: arithmetic is over +mathematical (non-wrapping) integers, and implementations must not compute +`length * elem_size(element)` in fixed-width arithmetic before comparing to +`MAX_VALUE_BYTE_LENGTH`. A preferred pattern for fixed-length lists is to +validate element types first, then reject when +`length > MAX_VALUE_BYTE_LENGTH // elem_size(element, ptr_type)`. + +Dynamic `string` and variable `list` payload sizes remain limited at runtime +by `MAX_STRING_BYTE_LENGTH` and `MAX_LIST_BYTE_LENGTH`; their static +`elem_size` is only the pointer pair (`2 * ptr_size`). + +See [Element Size validation](CanonicalABI.md#element-size) for reference +algorithms. + To incrementally describe how type-checking works, we'll start by asking how *type equality* works for non-resource, non-handle, local type definitions and build up from there. diff --git a/design/mvp/WIT.md b/design/mvp/WIT.md index 5fe1f403..8eb1e55b 100644 --- a/design/mvp/WIT.md +++ b/design/mvp/WIT.md @@ -1888,7 +1888,9 @@ first-class type. 🔧 A `list` with a fixed length provides the low-level memory representation of a homogeneous `tuple` of the same length, but with the dynamic indexing of a -list. E.g., the following two functions have the same low-level (Core +list. The static in-memory byte size of the resolved type must not exceed +`2^28 - 1` bytes (see [Element Size validation](CanonicalABI.md#element-size)). +E.g., the following two functions have the same low-level (Core WebAssembly) representation, but will naturally produce different source-level bindings: diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index 0567b758..cfe66c6d 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -1334,6 +1334,116 @@ def elem_size_flags(labels): if n <= 16: return 2 return 4 +MAX_VALUE_BYTE_LENGTH = (1 << 28) - 1 + +def checked_exceeds_max(n): + return n > MAX_VALUE_BYTE_LENGTH + +def checked_add(a, b): + s = a + b + if checked_exceeds_max(s): + return MAX_VALUE_BYTE_LENGTH + 1 + return s + +def checked_mul(a, b): + if b != 0 and a > MAX_VALUE_BYTE_LENGTH // b: + return MAX_VALUE_BYTE_LENGTH + 1 + return a * b + +def checked_align_to(ptr, alignment): + r = align_to(ptr, alignment) + if checked_exceeds_max(r): + return MAX_VALUE_BYTE_LENGTH + 1 + return r + +def checked_elem_size_list(elem_type, maybe_length, ptr_type): + if maybe_length is not None: + es = checked_elem_size(elem_type, ptr_type) + if es == 0: + return MAX_VALUE_BYTE_LENGTH + 1 + return checked_mul(maybe_length, es) + r = 2 * ptr_size(ptr_type) + return r if not checked_exceeds_max(r) else MAX_VALUE_BYTE_LENGTH + 1 + +def checked_elem_size_record(fields, ptr_type): + s = 0 + for f in fields: + s = checked_align_to(s, alignment(f.t, ptr_type)) + if checked_exceeds_max(s): + return MAX_VALUE_BYTE_LENGTH + 1 + s = checked_add(s, checked_elem_size(f.t, ptr_type)) + if checked_exceeds_max(s): + return MAX_VALUE_BYTE_LENGTH + 1 + if s == 0: + return MAX_VALUE_BYTE_LENGTH + 1 + return checked_align_to(s, alignment_record(fields, ptr_type)) + +def checked_elem_size_variant(cases, ptr_type): + s = checked_elem_size(discriminant_type(cases), ptr_type) + s = checked_align_to(s, max_case_alignment(cases, ptr_type)) + cs = 0 + for c in cases: + if c.t is not None: + cs = max(cs, checked_elem_size(c.t, ptr_type)) + s = checked_add(s, cs) + if checked_exceeds_max(s): + return MAX_VALUE_BYTE_LENGTH + 1 + return checked_align_to(s, alignment_variant(cases, ptr_type)) + +def checked_elem_size(t, ptr_type): + match despecialize(t): + case BoolType() : return 1 + case S8Type() | U8Type() : return 1 + case S16Type() | U16Type() : return 2 + case S32Type() | U32Type() : return 4 + case S64Type() | U64Type() : return 8 + case F32Type() : return 4 + case F64Type() : return 8 + case CharType() : return 4 + case StringType() : return checked_mul(2, ptr_size(ptr_type)) + case ErrorContextType() : return 4 + case ListType(t, l) : return checked_elem_size_list(t, l, ptr_type) + case RecordType(fields) : return checked_elem_size_record(fields, ptr_type) + case VariantType(cases) : return checked_elem_size_variant(cases, ptr_type) + case FlagsType(labels) : return elem_size_flags(labels) + case OwnType() | BorrowType() : return 4 + case StreamType() | FutureType() : return 4 + +def valid_valtype_size(t, ptr_type) -> bool: + return checked_elem_size(t, ptr_type) <= MAX_VALUE_BYTE_LENGTH + +def check_resolved_type_size(t) -> bool: + t = despecialize(t) + match t: + case ListType(elem, maybe_len): + if elem is not None and not check_resolved_type_size(elem): + return False + if maybe_len is not None: + for ptr_type in ['i32', 'i64']: + elem_sz = checked_elem_size(elem, ptr_type) + if elem_sz == 0 or maybe_len > MAX_VALUE_BYTE_LENGTH // elem_sz: + return False + case StreamType(elem) | FutureType(elem): + if elem is not None and not check_resolved_type_size(elem): + return False + case RecordType(fields): + for f in fields: + if not check_resolved_type_size(f.t): + return False + case VariantType(cases): + for c in cases: + if c.t is not None and not check_resolved_type_size(c.t): + return False + case _: + pass + for ptr_type in ['i32', 'i64']: + if not valid_valtype_size(t, ptr_type): + return False + return True + +def check_defvaltype_size(t) -> bool: + return check_resolved_type_size(t) + ## Loading def load(cx, ptr, t): diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index 619e5f42..e00bb598 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -3048,4 +3048,51 @@ def core_consumer(args): test_sync_threads() test_thread_cancel_callback() +def test_max_value_byte_length(): + MAX = MAX_VALUE_BYTE_LENGTH + + assert check_defvaltype_size(ListType(U8Type(), MAX)) + assert check_defvaltype_size(ListType(U64Type(), 33554431)) + assert check_defvaltype_size(ListType(StringType(), 16777215)) + + assert not check_defvaltype_size(ListType(U8Type(), MAX + 1)) + assert not check_defvaltype_size(ListType(U64Type(), 33554432)) + assert not check_defvaltype_size(ListType(U64Type(), 1 << 29)) + + assert not check_defvaltype_size(TupleType([ + ListType(U8Type(), MAX), + ListType(U8Type(), 1), + ])) + + assert not check_defvaltype_size(RecordType([ + FieldType("a", ListType(U8Type(), 134217728)), + FieldType("b", ListType(U8Type(), 134217728)), + ])) + + assert not check_defvaltype_size(MapType(U8Type(), ListType(U8Type(), MAX))) + + assert check_defvaltype_size(OptionType(MapType(U8Type(), U8Type()))) + + assert not check_defvaltype_size( + OptionType(MapType(U8Type(), ListType(U8Type(), MAX))) + ) + assert not check_defvaltype_size( + RecordType([FieldType("m", MapType(U8Type(), ListType(U8Type(), MAX)))]) + ) + + assert not check_defvaltype_size(ListType(ListType(U8Type(), MAX), 2)) + + assert not check_defvaltype_size(ListType(StringType(), 16777216)) + + length = 1 << 29 + assert (length * 8) % (1 << 32) == 0 + assert not check_defvaltype_size(ListType(U64Type(), length)) + + nested = ListType(U8Type(), MAX) + for _ in range(64): + nested = ListType(nested, 2) + assert not check_defvaltype_size(nested) + +test_max_value_byte_length() + print("All tests passed") diff --git a/test/nyi.txt b/test/nyi.txt index 52163801..1b785695 100644 --- a/test/nyi.txt +++ b/test/nyi.txt @@ -1 +1,2 @@ # See README.md +./validation/max-value-size.wast diff --git a/test/validation/max-value-size.wast b/test/validation/max-value-size.wast new file mode 100644 index 00000000..bb9279e3 --- /dev/null +++ b/test/validation/max-value-size.wast @@ -0,0 +1,76 @@ +;; Validation rejects defvaltypes whose resolved structural AST exceeds +;; MAX_VALUE_BYTE_LENGTH (2^28 - 1 bytes). See CanonicalABI.md#element-size. + +;; valid boundaries + +(component + (type (list u8 268435455)) +) + +(component + (type (list u64 33554431)) +) + +(component + (type (list string 16777215)) +) + +;; single fixed list just over the limit + +(assert_invalid + (component (type (list u8 268435456))) + "exceeds maximum byte size") + +;; fixed list whose product exceeds MAX + +(assert_invalid + (component (type (list u64 33554432))) + "exceeds maximum byte size") + +;; u32 wrap class: real byte size is 2^32 but naive u32 multiply wraps to 0 + +(assert_invalid + (component (type (list u64 536870912))) + "exceeds maximum byte size") + +;; compound sum exceeds MAX + +(assert_invalid + (component + (type (tuple (list u8 268435455) (list u8 1)))) + "exceeds maximum byte size") + +(assert_invalid + (component + (type (record + (field "a" (list u8 134217728)) + (field "b" (list u8 134217728))))) + "exceeds maximum byte size") + +;; map despecialization: inner list valid alone, pair record is MAX + 1 + +(assert_invalid + (component (type (map u8 (list u8 268435455)))) + "exceeds maximum byte size") + +;; nested fixed list + +(assert_invalid + (component (type (list (list u8 268435455) 2))) + "exceeds maximum byte size") + +;; pointer-width-sensitive rejection (passes i32, fails i64) + +(assert_invalid + (component (type (list string 16777216))) + "exceeds maximum byte size") + +;; nested map despecialization inside option and record + +(assert_invalid + (component (type (option (map u8 (list u8 268435455))))) + "exceeds maximum byte size") + +(assert_invalid + (component (type (record (field "m" (map u8 (list u8 268435455)))))) + "exceeds maximum byte size")