From 6ad1f39479d3912ba1d7c222e3509f4436e70772 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Sun, 9 Aug 2026 14:14:16 +0530 Subject: [PATCH 1/2] 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/2] 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")