Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions design/mvp/Binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
124 changes: 124 additions & 0 deletions design/mvp/CanonicalABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
33 changes: 33 additions & 0 deletions design/mvp/Explainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,9 @@ and sequencing contained values.
🔧 When the optional `<u32>` 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

Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion design/mvp/WIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
110 changes: 110 additions & 0 deletions design/mvp/canonical-abi/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
47 changes: 47 additions & 0 deletions design/mvp/canonical-abi/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
1 change: 1 addition & 0 deletions test/nyi.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# See README.md
./validation/max-value-size.wast
Loading