From 6ad1f39479d3912ba1d7c222e3509f4436e70772 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Sun, 9 Aug 2026 14:14:16 +0530 Subject: [PATCH 1/3] Specify maximum static value type size for validation. Require resolved type AST nodes to satisfy elem_size <= 2^28 - 1 for both i32 and i64 pointer types using overflow-safe validation, closing fixed-length-list sizing hazards (#682). Signed-off-by: Gaurav Chaudhary --- design/mvp/Binary.md | 6 ++ design/mvp/CanonicalABI.md | 123 ++++++++++++++++++++++++ design/mvp/Explainer.md | 30 ++++++ design/mvp/WIT.md | 4 +- design/mvp/canonical-abi/definitions.py | 109 +++++++++++++++++++++ design/mvp/canonical-abi/run_tests.py | 38 ++++++++ test/nyi.txt | 1 + test/validation/max-value-size.wast | 66 +++++++++++++ 8 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 test/validation/max-value-size.wast 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..4e754730 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -2361,6 +2361,129 @@ 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: + 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(despecialize(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..69c664e5 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,33 @@ 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. + +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..0b6ce6d9 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -1334,6 +1334,115 @@ 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: + 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(despecialize(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..10f9714c 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -3048,4 +3048,42 @@ 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 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..23e21579 --- /dev/null +++ b/test/validation/max-value-size.wast @@ -0,0 +1,66 @@ +;; 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") From 4a86ca440d9bd67521055522ec7625ebf19f37b4 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Sun, 9 Aug 2026 14:25:31 +0530 Subject: [PATCH 2/3] Fix nested despecialization in value type size validation. Apply despecialize at each recursive check_resolved_type_size call so nested map/option/record nodes are validated, not only top-level types. Add regression tests for option> and record fields (#682). Signed-off-by: Gaurav Chaudhary --- design/mvp/CanonicalABI.md | 3 ++- design/mvp/Explainer.md | 5 ++++- design/mvp/canonical-abi/definitions.py | 3 ++- design/mvp/canonical-abi/run_tests.py | 9 +++++++++ test/validation/max-value-size.wast | 10 ++++++++++ 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index 4e754730..b7c9ace8 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -2453,6 +2453,7 @@ 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): @@ -2481,7 +2482,7 @@ def check_resolved_type_size(t) -> bool: return True def check_defvaltype_size(t) -> bool: - return check_resolved_type_size(despecialize(t)) + return check_resolved_type_size(t) ``` ## Loading diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index 69c664e5..149b9ea0 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -1048,7 +1048,10 @@ pointer type `ptr_type ∈ {i32, i64}`, the static in-memory byte size `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. +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 diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index 0b6ce6d9..cfe66c6d 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -1413,6 +1413,7 @@ 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): @@ -1441,7 +1442,7 @@ def check_resolved_type_size(t) -> bool: return True def check_defvaltype_size(t) -> bool: - return check_resolved_type_size(despecialize(t)) + return check_resolved_type_size(t) ## Loading diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index 10f9714c..e00bb598 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -3071,6 +3071,15 @@ def test_max_value_byte_length(): 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)) diff --git a/test/validation/max-value-size.wast b/test/validation/max-value-size.wast index 23e21579..bb9279e3 100644 --- a/test/validation/max-value-size.wast +++ b/test/validation/max-value-size.wast @@ -64,3 +64,13 @@ (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") From 5fe4782c7da0c7a7999631a2fa19d4db298bc5bd Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Sun, 16 Aug 2026 17:27:24 +0530 Subject: [PATCH 3/3] Simplify max value size validation to reuse elem_size. Replace the duplicate checked_* Python with one Element Size invariant (every despecialized AST node, both pointer widths) and short pointers in Binary, Explainer, and canonopt validation. Keep WAST coverage and drop the run_tests.py validator tests. Signed-off-by: Gaurav Chaudhary --- design/mvp/Binary.md | 10 +- design/mvp/CanonicalABI.md | 136 +++--------------------- design/mvp/Explainer.md | 36 ++----- design/mvp/WIT.md | 4 +- design/mvp/canonical-abi/definitions.py | 110 ------------------- design/mvp/canonical-abi/run_tests.py | 47 -------- test/validation/max-value-size.wast | 18 +++- 7 files changed, 41 insertions(+), 320 deletions(-) diff --git a/design/mvp/Binary.md b/design/mvp/Binary.md index 5894df1d..6e5a602a 100644 --- a/design/mvp/Binary.md +++ b/design/mvp/Binary.md @@ -287,12 +287,10 @@ 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. +* Validation of each `defvaltype` requires every resolved AST node (after + despecialization) to satisfy the `elem_size` bound in + [Element Size](CanonicalABI.md#element-size). 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 b7c9ace8..3201f5a4 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -2361,129 +2361,16 @@ 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) -``` +Validation of each `defvaltype` requires that every node `u` in the resolved +structural AST — after `despecialize`, including implicit nodes such as `map`'s +key/value record — satisfies `elem_size(u, ptr_type) ≤ 2^28−1` for both +`ptr_type ∈ {i32, i64}`. This is a static validation error, not a runtime trap. +The bound is the same number as `MAX_LIST_BYTE_LENGTH` / `MAX_STRING_BYTE_LENGTH`. +`elem_size` is unbounded-integer math; fixed-width implementations must reject +overflow rather than wrap. Checking only the root type is not enough: +`map>` has a small list header but an oversized pair record. +Incremental or memoized checking is allowed only if every resolved node of that +definition is still covered. ## Loading @@ -3718,6 +3605,9 @@ performed for a component. These are defined as: * `lift(T)` * requires `realloc` if `T` contains a `list` or `string` +Value types used by `lift`/`lower` are already rejected at `defvaltype` +definition if they exceed the [Element Size](#element-size) bound, so lift and +lower may assume static layouts fit. ### `canon lift` diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index dbc38808..49149368 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -738,9 +738,6 @@ 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 @@ -1045,33 +1042,12 @@ 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. +Each `defvaltype` is rejected unless every node of its resolved structural AST +(after despecialization) has `elem_size` at most `2^28 − 1` for both `i32` and +`i64` pointer types. Checking only the root is not enough: wrappers such as +`map`, `option`, `stream`, and `future` can have a small header while a nested +node exceeds the bound. This is a static validation error. See +[Element Size](CanonicalABI.md#element-size). 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 diff --git a/design/mvp/WIT.md b/design/mvp/WIT.md index 8eb1e55b..5fe1f403 100644 --- a/design/mvp/WIT.md +++ b/design/mvp/WIT.md @@ -1888,9 +1888,7 @@ 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. 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 +list. 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 58f5bf87..8f04f093 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -1334,116 +1334,6 @@ 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 e00bb598..619e5f42 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -3048,51 +3048,4 @@ 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/validation/max-value-size.wast b/test/validation/max-value-size.wast index bb9279e3..4b307d6e 100644 --- a/test/validation/max-value-size.wast +++ b/test/validation/max-value-size.wast @@ -1,5 +1,5 @@ ;; Validation rejects defvaltypes whose resolved structural AST exceeds -;; MAX_VALUE_BYTE_LENGTH (2^28 - 1 bytes). See CanonicalABI.md#element-size. +;; 2^28 - 1 bytes. See CanonicalABI.md#element-size. ;; valid boundaries @@ -15,6 +15,12 @@ (type (list string 16777215)) ) +;; valid map despecialization (pair record is well under the bound) + +(component + (type (map u8 (list u8 4))) +) + ;; single fixed list just over the limit (assert_invalid @@ -74,3 +80,13 @@ (assert_invalid (component (type (record (field "m" (map u8 (list u8 268435455)))))) "exceeds maximum byte size") + +;; stream/future handle size is 4; payload must still be checked + +(assert_invalid + (component (type (stream (list u8 268435455)))) + "exceeds maximum byte size") + +(assert_invalid + (component (type (future (list u8 268435455)))) + "exceeds maximum byte size")