wai/wai-int-module is one WebAssembly module with the interface of waism §5 and nothing else: no import, one memory with a declared maximum, and every allocation trapping where memory cannot grow (R13). It decodes the integer payloads (WIV1, WIH1, WIA1, WIS1), int_synth, int_mlicpp, opus.dred.int and avatar.face through wai-rs's integer decoders, whose networks run on wai-ops/1; networks and weights arrive as each capability's set. - record/: the scalar and SIMD128 builds (7,099 vector instructions), named by SHA-256 in manifest.json, each held at the integer level by the Rust checker and check-module.mjs. build.sh builds both with the machine's paths remapped and checks both with both checkers. A build on another host has other bytes (the compiler's crate hashes take in each path dependency's absolute path and the host) and gives every case the same outcome. - corpus.json (corpus.py): every committed vector and refusal fixture of those capabilities, 120 cases, from the committed corpora. - host/ (wasmtime 50.0.1 Cranelift and Winch, Wasmi 2.0.0 with floats off), run-node.mjs and browser/run-browser.mjs run every case as a sink does and print one run digest; every run, on Linux x86_64, Linux arm64 and macOS arm64, gives the same. - plant.py plants a float or a relaxed vector instruction in a module, for the checkers. - waism §5, The set: one file as its bytes, several as their file sequence. avatars §6 names generator.wfg and identity.wfi; avatars §7.8, opus-dred §4.8 and determinism-tiers §5 state the module as a fabric. Co-Authored-By: Claude Opus 5.5 <[email protected]>
525 lines
40 KiB
Markdown
525 lines
40 KiB
Markdown
# WAI Extension: WA²ISM™, the Deterministic Module Profile
|
||
|
||
> Status: Draft. WA²ISM™, named WAISM™ in running text, is WAI's deterministic module
|
||
> profile: a checked subset of standard WebAssembly,
|
||
> run by unmodified engines. It is not a fork of WebAssembly and not a new bytecode: it adds no
|
||
> instruction, type, section or semantics, and any engine that implements WebAssembly 3.0 runs a
|
||
> module that meets it. What the profile decides is what a sink may state about such a module's
|
||
> output. It has two levels (§2): an **integer level**, with no floating point, whose output is the
|
||
> same on every engine with no engine setting; and a **float level**, which admits floating point
|
||
> and holds a tier only on an engine set to the NaN rule of the deterministic profile of
|
||
> WebAssembly 3.0.
|
||
> A sink checks a decoder module against numbered rules (§4) before it instantiates it, holds a
|
||
> signed conformance result over the module's digest (§6, check 9c), runs it through a fixed
|
||
> interface (§5) under the settings of §7, refuses with the codes of §8, and names what it checked
|
||
> in its receipt (§10). Checks 9 to 13 follow [jwp-receipts](jwp-receipts.md) §7.9's checks 1 to 8,
|
||
> which authorise the module and which this text leaves as they are. The corpora a checker, a
|
||
> module and an engine are held to are in §9. This text defines the profile; it names no
|
||
> implementation.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision: WebAssembly 3.0, the specification build dated 2026-11-04: the
|
||
> appendix *Profiles* (*Defined Profiles*: the full profile, FUL, and the deterministic profile,
|
||
> DET); the appendix *Implementation Limitations* (*Execution*); *Execution*, *Numerics*
|
||
> (*Floating-Point Operations*: *Rounding*, *NaN Propagation*; *Relaxed Operations*) and
|
||
> *Instructions* (`memory.grow`); *Binary Format*, *Types* (limits) and *Modules* (the preamble,
|
||
> the section ids, custom sections, the start section). A section number in this text is this
|
||
> extension's unless it names another document.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the two levels (§2); the rules a module meets, the caps and a checker's verdict (§4);
|
||
the interface a module exports (§5); the steps a sink takes to check and run a module (§6); the
|
||
engine settings it uses (§7); the refusal codes (§8); the corpora (§9); what a receipt states
|
||
(§10); and conformance (§11).
|
||
|
||
Out of scope: how a sink obtains a module and who may grant it ([jwp-receipts](jwp-receipts.md)
|
||
§7.9, [http-delivery](http-delivery.md) §8.4); reconstruct modules, and decoder modules a sink
|
||
runs on no WebAssembly engine, which this text does not cover; the arithmetic a decoder
|
||
computes, which its capability's text fixes ([integer-operators](integer-operators.md) for the
|
||
integer graphs); and a pinned parameter set, which is data and is held by jwp-receipts §7.9's
|
||
checks 1 to 8 alone.
|
||
|
||
Relationship to WebAssembly and to the other texts:
|
||
|
||
- **WebAssembly 3.0.** Core WebAssembly leaves open the sign and payload of a NaN, the results of
|
||
the relaxed vector instructions, whether `memory.grow` and `table.grow` succeed, what an engine
|
||
does past its resource limits, and every value a host supplies through an import. Every other
|
||
result is fixed: integer instructions are exact, and floating-point ones are rounded to nearest,
|
||
ties to even (*Numerics*, *Rounding*). The **deterministic profile** (DET) of WebAssembly 3.0
|
||
(appendix *Profiles*, *Defined Profiles*) fixes the first two: every NaN a floating-point
|
||
instruction generates is canonical and positive, and every relaxed vector instruction has one
|
||
behaviour. Under it, `memory.grow` and `table.grow` remain non-deterministic. The integer level
|
||
refuses floating point and relaxed vector instructions, so nothing the deterministic profile
|
||
changes is reachable from it, and it needs no engine setting. The float level needs the
|
||
deterministic profile's NaN rule, and nothing else of it is reachable, since both levels refuse
|
||
relaxed vector instructions (R4). Both levels refuse imports, fix the memory and the table, and
|
||
turn a failed `memory.grow` into a refusal.
|
||
- **[jwp-receipts](jwp-receipts.md) §7.9.** A module's identity is its digest, the SHA-256 of
|
||
its bytes as instantiated, and its grant, key log, revocation and fallback are §7.9's. Its
|
||
checks 9 and 10 are this text's (§6).
|
||
- **[jwp-receipts](jwp-receipts.md) §2.4.12.** The decoder entry of a `reconstruct-session`
|
||
claim names the level, the engine, its settings, the probe and the conformance result (§10).
|
||
- **[conformance-results](conformance-results.md).** The record a sink holds for check 9c.
|
||
- **[browser-sink](browser-sink.md) §4.3 step 6.** The tier a page's wasm decoder holds.
|
||
- **[determinism-tiers](determinism-tiers.md) §5 and SPEC §7.** WebAssembly as a fabric of
|
||
`decode-equivalence` is a module held at the integer level.
|
||
- **[integer-operators](integer-operators.md) §6.** On WebAssembly, two of that section's
|
||
"MUST NOT"s are properties of the module a sink checks.
|
||
|
||
## 2. Levels
|
||
|
||
The profile's identifiers are defined here, and only here. Every other text, this one included,
|
||
names a level as the **integer level** or the **float level**.
|
||
|
||
| level | identifier | floating point | engine | what its output holds |
|
||
|---|---|---|---|---|
|
||
| integer level | `waism/1-int` | refused (R3) | any engine that implements WebAssembly 3.0, a browser's included, with no setting | the class tier of its capability ([SPEC](../SPEC.md) §3.1, *Tier*) and, for a `GenerativeDeterministic` capability, its generation-equivalence (SPEC §7) |
|
||
| float level | `waism/1-det` | admitted | only an engine set to the deterministic profile's NaN rule, shown by the probe (§6, check 10); no page (§7) | `entropy-consistency` where that is the class tier of its capability, on such an engine; otherwise none. Never `decode-equivalence` or generation-equivalence: SPEC §7, *Replicate-soundness*, admits a neural decode at `decode-equivalence` only where no floating-point operation is on its path, and this revision gives the float level neither |
|
||
|
||
The levels differ only in R3. Each identifier names this text's version 1; a module meets a
|
||
level when a checker gives it `held` at that level (§4.3), and R13.
|
||
|
||
## 3. Terms
|
||
|
||
- **Module**: a decoder module (jwp-receipts §7.9) that a sink would run on a WebAssembly
|
||
engine, whatever its bytes. Bytes that are not a WebAssembly 3.0 module, such as a component
|
||
or bytes that do not decode, are such a module where a sink would run them on one, and R2
|
||
refuses them (§4.3). A decoder module a sink runs on no WebAssembly engine is not one.
|
||
- **Item**: a part of a module's bytes a rule is about: an instruction, in a function body or a
|
||
constant expression; a value type, in a function type, a local declaration (one that declares
|
||
no locals among them), a global type, a block type or a typed `select`; an entry of the
|
||
import, table, memory, global or export section; a type entry of the type section; or a section.
|
||
- **Caps**: the three limits of §4.2's first table. Its validation limits are part of R2, and are
|
||
no caps.
|
||
- **Verdict**: a checker's answer for a module at a level under caps (§4.3).
|
||
- **Need**: the tier a sink holds an object's decoded output to, as [browser-sink](browser-sink.md)
|
||
§2 defines it, for any sink: the object's pin `tier`, else its track's declared determinism,
|
||
else `none`.
|
||
- **Held at a level**: a sink holds a module at a level, for an object, when checks 1 to 10 of §6
|
||
pass for it at that level.
|
||
|
||
## 4. Rules
|
||
|
||
### 4.1 The rules
|
||
|
||
Each rule names what it rests on: a clause of WebAssembly 3.0, or a clause of WAI. §9.1 names,
|
||
for each rule, at least one planted module that breaks it and no other.
|
||
|
||
| rule | level | the module | rests on |
|
||
|---|---|---|---|
|
||
| R1 | both | is at most `module_bytes` long (§4.2). | WebAssembly 3.0, *Implementation Limitations* (an implementation may limit the size of a module); §4.2 |
|
||
| R2 | both | is a WebAssembly 3.0 module: its bytes decode under the binary format, from the preamble `\0asm` and version 1, and it is valid, within the validation limits of §4.2. Shared memory, atomic instructions, a component, and every other construct WebAssembly 3.0 does not define, are outside it. | WebAssembly 3.0, *Binary Format* (*Modules*: the preamble and the section ids; *Types*: limits, whose four forms have no shared one), *Validation* and *Implementation Limitations* (an implementation may limit the number and size of a module's parts); §4.2 |
|
||
| R3 | integer | contains no floating point: no `f32` or `f64` value type, and no instruction that takes or gives a floating-point value, an `f32` or an `f64` or a `v128` read or written as `f32x4` or `f64x2` lanes. That covers constants, arithmetic, comparisons, conversions to and from integers (the saturating truncations among them), reinterpretations, lane splats and extractions, and loads and stores of floating-point values. | WebAssembly 3.0, *Numerics*, *NaN Propagation* (the sign of a NaN result is non-deterministic, and its payload is chosen among arithmetic NaNs); SPEC §7, *Replicate-soundness* (no float operation anywhere on a decode path that backs `replicate`); integer-operators §6 |
|
||
| R4 | both | contains no relaxed vector instruction: the integer ones (`i8x16.relaxed_swizzle`, the four `relaxed_laneselect`s, `i16x8.relaxed_q15mulr_s`, the two relaxed dot products) as well as the floating-point ones. | WebAssembly 3.0, *Numerics*, *Relaxed Operations* (each result is one of a set, and the implementation chooses which); integer-operators §6 (no instruction whose result the instruction set leaves to the implementation) |
|
||
| R5 | both | imports nothing: no function, table, memory, global or tag. | WebAssembly 3.0, *Binary Format*, *Modules* (the import section, whose values the host supplies at instantiation); §5 (the output is a function of the capability, the payload and the set alone) |
|
||
| R6 | both | defines exactly one memory, with address type `i32` and a declared maximum of at most `memory_pages` (§4.2). | WebAssembly 3.0, *Execution*, `memory.grow` (growth is non-deterministic, and fails past a declared maximum); *Binary Format*, *Types* (limits); §4.2 |
|
||
| R7 | both | defines at most one table, whose element type is `funcref` (`ref null func`, in either of its encodings), whose address type is `i32` and whose minimum equals its maximum, and contains no `table.grow`, `table.fill` or `table.set`. | WebAssembly 3.0, *Profiles*, *Defined Profiles* (`table.grow` remains non-deterministic under the deterministic profile); §5 |
|
||
| R8 | both | has no start section. | WebAssembly 3.0, *Binary Format*, *Modules* (the start section names a function that runs at instantiation); §6 check 12 (the guard covers `wai_run`, not instantiation) |
|
||
| R9 | both | gives no function type, local or global a reference type. | §5 (every value across the interface is an `i32`) |
|
||
| R10 | both | exports exactly the six exports of §5, each of the kind and type given there, and nothing else. | §5 |
|
||
| R11 | both | defines at most `functions` functions (§4.2). | WebAssembly 3.0, *Implementation Limitations*; §4.2 |
|
||
| R12 | both | contains none of these constructs of WebAssembly 3.0: exception handling (a tag section, `throw`, `throw_ref`, `try_table`); tail calls (`return_call`, `return_call_indirect`); typed function references (`call_ref`, `return_call_ref`, `ref.as_non_null`, `br_on_null`, `br_on_non_null`); and garbage-collected types and instructions (a type entry that is not a function type, among them a recursive or subtype declaration; `struct.*`, `array.*`, `ref.i31`, `i31.get_s`, `i31.get_u`, `ref.test`, `ref.cast`, `br_on_cast`, `br_on_cast_fail`, `any.convert_extern`, `extern.convert_any`, `ref.eq`). | §1 (the profile admits what a decoder module uses: a construct a decoder has no need of is one more that a sink's engine would run and that a conformance result would have to answer for, §11) |
|
||
| R13 | both | after a `memory.grow` that fails, gives no output: within `wai_run` the call traps, and within `wai_alloc` it traps or returns 0. A module whose memory's minimum equals its maximum and which contains no `memory.grow` meets this rule. | WebAssembly 3.0, *Execution*, `memory.grow` (it may fail, pushing −1) and *Profiles* (growth remains non-deterministic under the deterministic profile); integer-operators §6 ("A refused decode outputs nothing") |
|
||
|
||
An instruction breaks R3 by what its opcode alone takes or gives. One whose operand or result
|
||
types are fixed elsewhere (a block, a typed `select`, a call, `local.get`, `drop`) breaks R3 only
|
||
through the value type that fixes them: an earlier item, or, for a block type or a typed
|
||
`select`, the value type that follows its opcode.
|
||
|
||
Rules R1 to R12 are about the module's bytes, and a checker decides them (§4.3). R13 is about
|
||
what the module does, and a conformance result attests it (§6, check 9c; §9.2). Custom sections
|
||
are part of the bytes, so the digest covers them; WebAssembly's semantics ignore them, and no
|
||
rule, check or sink of this text reads them.
|
||
|
||
### 4.2 Caps and validation limits
|
||
|
||
| cap | the profile's value | what it bounds |
|
||
|---|---|---|
|
||
| `module_bytes` | 16,777,216 | the module's length, in bytes |
|
||
| `functions` | 50,000 | the functions the module defines |
|
||
| `memory_pages` | 65,536 | the memory's declared maximum, in 64 KiB pages |
|
||
|
||
A sink **MAY** apply lower caps of its own. A module over a sink's caps is not held at that sink:
|
||
the caps change where a module is held, never what it outputs.
|
||
|
||
The **validation limits** are part of R2, and no sink changes them. WebAssembly 3.0 lets an
|
||
implementation limit the number and the size of a module's parts, so without fixed limits two
|
||
validators could differ on a module within the caps; a module beyond one of these is not valid
|
||
under this profile, and is `refused R2 -`.
|
||
|
||
| limit | value | what it bounds |
|
||
|---|---|---|
|
||
| types | 1,000,000 | the entries of the type section, and the types they define |
|
||
| functions, globals, tags | 1,000,000 each | those the module imports and defines, together |
|
||
| tables | 100,000 | the tables the module imports and defines, together |
|
||
| memories | 100 | the memories the module imports and defines, together |
|
||
| element segments, data segments | 100,000 each | the entries of the element section, and of the data section; and the data count section's count |
|
||
| segment items | 10,000,000 | the items of one element segment |
|
||
| name | 100,000 | the bytes of one name: an import's module or field, an export's, a custom section's |
|
||
| body | 7,654,321 | the bytes of one function body, after its size |
|
||
| locals | 50,000 | a function's parameters and the locals its body declares, together |
|
||
| parameters, results | 1,000 each | those of one function type |
|
||
| struct fields | 10,000 | those of one struct type |
|
||
| catches | 10,000 | the catch clauses of one `try_table` |
|
||
| subtype depth | 63 | a type's declared supertypes, followed to a type that declares none |
|
||
| interface | 999,998 | the sum, over every import and export, of 2 and the parameters and results of its type for a function or a tag, and of 1 for a table, a memory or a global; so a module has at most 999,998 imports and exports |
|
||
|
||
### 4.3 The verdict
|
||
|
||
A checker's verdict for a module at a level under caps is one line of ASCII: `held`, or
|
||
`refused R<n> <offset>`, with `<n>` the rule and `<offset>` a decimal byte offset into the
|
||
module, or `-`. It is a function of the module's bytes, the level and the caps, and it is
|
||
decided in this order:
|
||
|
||
1. R1: a module longer than `module_bytes` is `refused R1 0`, and is not read further.
|
||
2. R2: a module that does not decode, or is not valid, is `refused R2 -`. Decoders and
|
||
validators report such a failure at different offsets, so this verdict carries none.
|
||
3. R3 to R12, over the items of the module in binary order: the first item that breaks one of
|
||
them is refused at its offset, by the lowest-numbered rule it breaks. The offset of an
|
||
instruction is that of its first byte (for a prefixed opcode, the prefix); of a value type,
|
||
its first byte; of an entry or a type entry, its first byte; of a section, its id byte. R11
|
||
is broken by the function section; a table instruction that R7 refuses, by that instruction.
|
||
4. Then the rules an absence breaks, in rule order, at the offset equal to the module's length:
|
||
R6 where the module defines no memory, and R10 where an export of §5 is missing.
|
||
5. Otherwise `held`.
|
||
|
||
A module that breaks rules at several items is refused at the first; a sink reports that rule.
|
||
Since the levels differ only in R3, a module refused R3 at the integer level is checked at the
|
||
float level, and every other verdict is the same at both.
|
||
|
||
## 5. The module ABI
|
||
|
||
A module exports exactly these:
|
||
|
||
| export | kind and type | what it does |
|
||
|---|---|---|
|
||
| `memory` | the module's one memory | holds every buffer the sink passes in and reads out |
|
||
| `wai_abi` | function `() → i32` | returns `1`, the version of this interface |
|
||
| `wai_alloc` | function `(len: i32) → i32` | returns the offset of a region of `len` bytes the module will not otherwise use, or `0` where it cannot |
|
||
| `wai_run` | function `(cap_ptr: i32, cap_len: i32, in_ptr: i32, in_len: i32, set_ptr: i32, set_len: i32) → i32` | decodes one object; returns `0` when the output is the decoded medium, or `1` when it is a refusal code |
|
||
| `wai_out_ptr` | function `() → i32` | the offset of the output |
|
||
| `wai_out_len` | function `() → i32` | the output's length, in bytes |
|
||
|
||
A sink decodes an object through a module on an instance of its own: it instantiates the module
|
||
for that object, makes these calls once each, in this order, and discards the instance.
|
||
|
||
1. `wai_abi()`. A result other than `1` is `module_result_invalid`.
|
||
2. For each of three inputs, in this order: the capability, as the UTF-8 bytes of its term; the
|
||
payload, the bytes the envelope carries for that capability; and the set, the files the decode
|
||
reads beside the payload, as *The set* below gives them. The sink calls `wai_alloc(len)` with
|
||
the input's length and writes the input at the offset returned. A result of `0` is
|
||
`module_resources`. A capability that decodes against no set passes `0, 0` for it, with no
|
||
call.
|
||
3. `wai_run` with the three offsets and lengths, under the guard of §6 check 12.
|
||
4. `wai_out_ptr()` and `wai_out_len()`. A region that does not lie within the memory is
|
||
`module_result_invalid`.
|
||
5. Where `wai_run` returned `0`, the region is the decoded medium in its capability's canonical
|
||
buffer ([determinism-tiers](determinism-tiers.md) §2: canonical RGB8 for an image, int16 PCM,
|
||
the attribute tensor, a per-frame hash for video). Where it returned `1`, the region is a
|
||
refusal code in ASCII, one of the codes the text that defines the capability's payload lists
|
||
for its decode, and the decode is refused with that code, as that text routes it. Any other
|
||
result of `wai_run`, or a refusal text that is not such a code, is `module_result_invalid`.
|
||
|
||
**The set.** The files a decode reads beside its payload are its capability's parameter set,
|
||
which SPEC §3.1's checks 6–8 have held to its pin's digest, and any file the capability's text
|
||
has the payload name by its digest, which the decode refuses where the file's digest is another
|
||
([avatars](avatars.md) §6's identity prior). Where those are one file, the set is that file's
|
||
bytes. Where they are more, the set is their **file sequence**: for each file, in ascending
|
||
byte order of its name, the name's length as one byte, from 1 to 255; the name, in UTF-8; the
|
||
file's length as four bytes, little-endian; and the file's bytes; with nothing between or
|
||
after. A file's name is its path in the set's listing (SPEC §3.1, the form `wai.prior.set`),
|
||
or the name the capability's text gives it. So the set of `wai.neural.int_hyper` is the file
|
||
sequence of `entropy.json`, `g_s.whm` and `h_s.whm`, and the set of `wai.avatar.face` the
|
||
file sequence of `generator.wfg` and `identity.wfi`. A file sequence names no file twice. An
|
||
input that is not the form its capability's set takes is not one this interface carries: the
|
||
module traps (§6, check 12), and nothing is output.
|
||
|
||
Nothing else reaches the module: it imports nothing (R5), runs nothing at instantiation (R8), and
|
||
starts from its own bytes on every instance. So the output is a function of the capability, the
|
||
payload and the set. A decode that reads anything else, such as the decoded state of another
|
||
object, is not one this interface carries.
|
||
|
||
## 6. How a sink checks and runs a module
|
||
|
||
A sink that decodes an object through a module takes these steps in order and reports the first
|
||
that fails. A failure at steps 1 to 10 means the module is not held for the object: the capability
|
||
is unsupported for that envelope at that sink, and dispatch continues to the declared fallback or
|
||
a sibling rendition (jwp-receipts §7.9, *Fail closed*, *Fallback, never inert*). A failure at
|
||
steps 11 to 13 refuses the object's decode through the module, and dispatch continues the same
|
||
way; nothing is output.
|
||
|
||
**0. Identity.** The SHA-256 of the exact bytes the sink will instantiate (jwp-receipts §7.9,
|
||
*Identity*).
|
||
|
||
**1–8. Authorisation.** jwp-receipts §7.9's checks 1 to 8, unchanged.
|
||
|
||
**9. Profile.**
|
||
- **9a. The module:** rules R1 and R2.
|
||
- **9b. Its contents:** rules R3 to R12. The sink checks the module at the integer level, and,
|
||
where its verdict there is `refused R3`, at the float level. The module's level is the first
|
||
whose verdict is `held`. Otherwise the sink reports `module_profile_violation` with the
|
||
verdict of the last level it checked, its rule and its offset. A sink **MAY** keep the verdict
|
||
of 9a and 9b by the module's digest, its level and its caps, since nothing else decides it.
|
||
- **9c. Attestation.** The sink holds a `conform-result/1` record
|
||
([conformance-results](conformance-results.md) §2) that attests this module for the capability
|
||
being decoded: the record is admitted under conformance-results §4.1 against the module corpus
|
||
of §9.2 for that capability, as the sink holds it; its `kind` is `decoder` and its `suite` is
|
||
`module/` followed by the capability's term; an entry of its `implementation.files` has the
|
||
module's digest as its `sha256`; its `key` is `supplied`; and its `signer_pubkey` is a key the
|
||
grant's origin lists with `module/decoder`: the head jwp-receipts §7.4 step 3 reached for the
|
||
grant at jwp-receipts §7.9's check 3 lists an entry whose `key` is that `signer_pubkey`, which
|
||
is not `revoked`, carries no `pq_key` ([pq-signatures](pq-signatures.md) §6.1: a hybrid entry
|
||
signs nothing with Ed25519 alone, and a record has no other signature), holds `module/decoder` in its
|
||
`may_sign`, and, where it is `retired`, has an `until` no earlier than the record's
|
||
`started_at`, the two compared as jwp-receipts §7.4 compares times. Otherwise
|
||
`module_unattested`. The record carries no `signer_id`, so it is no item jwp-receipts §7.4
|
||
confirms: this check reads the head the grant was confirmed under, and nothing else of the
|
||
key log.
|
||
|
||
**10. Engine.**
|
||
- At the float level, where the object's need is above `none`: the sink's engine is set to the
|
||
deterministic profile's NaN rule (§7, `nan-canonical`), and the probe of §9.3, run in this
|
||
process on that engine and setting before the module's first run, gave the probe's bytes.
|
||
Otherwise `module_engine_not_deterministic` where the engine cannot be so set, and
|
||
`module_probe_mismatch` where the probe gave other bytes. Where the need is `none` this check
|
||
passes, and the output holds no tier.
|
||
- At the integer level: a sink **SHOULD** decode one vector of the capability's module corpus
|
||
(§9.2) once per engine and setting in each process, before the module's first run there, and
|
||
compare the output with the vector's digest. A difference is `module_probe_mismatch`. This
|
||
catches an engine that does not implement WebAssembly 3.0 as written; the level's semantics do
|
||
not need it.
|
||
|
||
**11. Instantiate** with an empty import object. A sink **SHOULD** reserve the memory's declared
|
||
maximum where its engine allows it. A failure to instantiate or to reserve is `module_resources`.
|
||
Then the calls of §5, steps 1 and 2.
|
||
|
||
**12. Run** `wai_run` under a guard: an instruction budget the engine counts, or a wall-clock
|
||
limit the sink enforces (§7). A trap, a stack exhausted among them, is `module_trap`; an
|
||
exhausted budget or limit is `module_timeout`. A sink whose engine reports that a `memory.grow`
|
||
failed refuses the run (`module_resources`) whatever the module then returns. Every sink also
|
||
relies on R13, which check 9c attests: a page cannot observe a failed grow. Availability can
|
||
differ between sinks; the output never does.
|
||
|
||
**13. Output.** §5's steps 4 and 5. The sink hashes the decoded medium (SHA-256), and its
|
||
receipt names the module as §10 states.
|
||
|
||
A module held at the integer level and run through steps 11 to 13 holds, for its output, what §2
|
||
gives that level; one held at the float level holds what §2 gives the float level, only after
|
||
check 10 passed with the object's need above `none`.
|
||
|
||
## 7. Engine settings
|
||
|
||
| term | the setting |
|
||
|---|---|
|
||
| `threads-off` | the engine refuses shared memory and atomic instructions |
|
||
| `relaxed-off` | the engine refuses relaxed vector instructions |
|
||
| `nan-canonical` | the engine gives every NaN a floating-point instruction generates as the positive canonical NaN, the deterministic profile's rule |
|
||
| `fuel` | the run is bounded by a budget of instructions the engine counts |
|
||
| `wall-clock` | the run is bounded by a wall-clock limit the sink enforces |
|
||
|
||
A sink, at either level:
|
||
- instantiates with an empty import object, and links no host interface;
|
||
- limits the memory's growth to the module's declared maximum, and its stack to a fixed size, the
|
||
same for every run;
|
||
- bounds every run of `wai_run` with `fuel` or `wall-clock`;
|
||
- **SHOULD** set `threads-off` and `relaxed-off` where its engine can be so set, as a second guard
|
||
behind R2 and R4.
|
||
|
||
At the float level a sink also sets `nan-canonical`, and check 10's probe shows it. A sink
|
||
**MUST NOT** take an engine setting that fixes the behaviour of relaxed vector instructions for
|
||
the deterministic profile: both levels refuse those instructions (R4), and only the probe shows
|
||
an engine's NaN behaviour. A page takes check 10 as failed: the output of a float-level module
|
||
in a page holds no tier, and a page decodes through one only an object whose need is `none`.
|
||
|
||
The **engine** a receipt names (§10) is written `<engine>@<version>`, optionally followed by
|
||
`/<compiler>`: `<engine>` and `<compiler>` are 1 to 64 of `a`–`z`, `0`–`9`, `_` and `-`, and
|
||
`<version>` is 1 to 64 of ASCII letters, digits, `.`, `+` and `-`, beginning with a digit. A
|
||
budget's instruction count is not energy, and no receipt states it.
|
||
|
||
## 8. Refusal codes
|
||
|
||
| code | raised at | then |
|
||
|---|---|---|
|
||
| `module_profile_violation` | §6 check 9a, 9b: with the rule and the offset of the verdict (§4.3) | the module is not held for the object |
|
||
| `module_unattested` | §6 check 9c | the module is not held for the object |
|
||
| `module_engine_not_deterministic` | §6 check 10 | the module is not held for an object whose need is above `none` |
|
||
| `module_probe_mismatch` | §6 check 10 | the module is not held for the object |
|
||
| `module_resources` | §6 check 11 (instantiation, reservation, `wai_alloc` returning `0`) and check 12 (a failed `memory.grow` the engine reports) | the object's decode through the module is refused |
|
||
| `module_trap` | §6 check 12 | the object's decode through the module is refused |
|
||
| `module_timeout` | §6 check 12 | the object's decode through the module is refused |
|
||
| `module_result_invalid` | §5 steps 1, 4 and 5 (§6 checks 11 and 13) | the object's decode through the module is refused |
|
||
|
||
In each case nothing is output, and dispatch continues as §6 states. A refusal the module returns
|
||
through `wai_run` is its capability's own, and routes as that capability's text routes it.
|
||
|
||
## 9. Corpora
|
||
|
||
### 9.1 The checker corpus
|
||
|
||
A conforming checker gives each module below the verdict stated, at the level and under the caps
|
||
stated. Each module is given by its construction: any bytes built as its row describes are that
|
||
module, and the offset in its verdict is that of the item the row names, in those bytes (§4.3).
|
||
Caps not stated are the profile's (§4.2).
|
||
|
||
**Planted modules.**
|
||
|
||
| case | level, caps | the module | verdict |
|
||
|---|---|---|---|
|
||
| `oversize` | both; `module_bytes` one below the module's length | `minimal-int`, unchanged | `refused R1 0` |
|
||
| `invalid` | both | a function whose body leaves an `i64` where its type returns `i32` | `refused R2 -` |
|
||
| `shared-memory` | both | a shared memory and an `i32.atomic.load` | `refused R2 -` |
|
||
| `component` | both | a component's preamble | `refused R2 -` |
|
||
| `scalar-float` | integer | `f32.add` of two `f32.const`s in `wai_run` | `refused R3`, at the first `f32.const` |
|
||
| `float-unreachable` | integer | `f32.add` and `drop` in `wai_run` after a `return`, where the operand stack is polymorphic and no earlier item gives a floating-point value | `refused R3`, at the `f32.add` |
|
||
| `float-block-type` | integer | a block in `wai_run` whose type is the result `f32`, holding only `unreachable`, its result dropped | `refused R3`, at the block type's value type |
|
||
| `float-select-type` | integer | a typed `select` whose type is `f32` in `wai_run` after a `return`, where the operand stack is polymorphic, its result dropped | `refused R3`, at its value type |
|
||
| `float-constant` | integer | a global initialised by `f64.const` | `refused R3`, at the global's value type |
|
||
| `float-signature` | integer | a function type with an `f64` parameter | `refused R3`, at that value type |
|
||
| `float-vector` | integer | `f32x4.add` | `refused R3`, at the instruction |
|
||
| `float-conversion` | integer | `f32x4.convert_i32x4_s` | `refused R3`, at the instruction |
|
||
| `float-truncation` | integer | `i32.trunc_sat_f32_s` on an `f32` built by `f32.reinterpret_i32` | `refused R3`, at `f32.reinterpret_i32` |
|
||
| `float-load` | integer | `f32.load` | `refused R3`, at the instruction |
|
||
| `relaxed-swizzle` | both | `i8x16.relaxed_swizzle` | `refused R4`, at the instruction |
|
||
| `relaxed-laneselect` | both | `i16x8.relaxed_laneselect` | `refused R4`, at the instruction |
|
||
| `relaxed-dot` | both | `i16x8.relaxed_dot_i8x16_i7x16_s` | `refused R4`, at the instruction |
|
||
| `relaxed-madd` | float | `f32x4.relaxed_madd` | `refused R4`, at the instruction |
|
||
| `import-function` | both | an imported function returning an `i32` (a clock) | `refused R5`, at the import |
|
||
| `import-memory` | both | its memory imported, not defined | `refused R5`, at the import; it breaks R6 too, which §4.3 reports only after every item |
|
||
| `import-global` | both | an imported immutable `i32` global | `refused R5`, at the import |
|
||
| `no-memory` | both | no memory: its memory, its export `memory` and the store in `wai_run` removed | `refused R6`, at the module's length; it breaks R10 too, which §4.3 reports after R6 |
|
||
| `two-memories` | both | a second memory | `refused R6`, at the second memory's entry |
|
||
| `memory64` | both | its memory of address type `i64`, and the store in `wai_run` addressed by an `i64` | `refused R6`, at the memory's entry |
|
||
| `no-maximum` | both | its memory with no maximum | `refused R6`, at the memory's entry |
|
||
| `over-pages` | both; `memory_pages` 16 | its memory's maximum 17 | `refused R6`, at the memory's entry |
|
||
| `two-tables` | both | two `funcref` tables, each with its minimum its maximum | `refused R7`, at the second table's entry |
|
||
| `externref-table` | both | a table of `externref`, its minimum its maximum | `refused R7`, at the table's entry |
|
||
| `table-unfixed` | both | a `funcref` table whose minimum is below its maximum | `refused R7`, at the table's entry |
|
||
| `table-grow` | both | a `funcref` table, its minimum its maximum, and `table.grow` | `refused R7`, at the instruction |
|
||
| `table-set` | both | the same table, and `table.set` | `refused R7`, at the instruction |
|
||
| `table-fill` | both | the same table, and `table.fill` | `refused R7`, at the instruction |
|
||
| `start` | both | a start section | `refused R8`, at the section's id byte |
|
||
| `funcref-parameter` | both | a function of the module, not exported, with a `funcref` parameter | `refused R9`, at that value type |
|
||
| `externref-global` | both | a global of type `externref` | `refused R9`, at its value type |
|
||
| `funcref-local` | both | a local of type `funcref` | `refused R9`, at its value type |
|
||
| `i64-export` | both | `wai_run` with its last parameter an `i64` | `refused R10`, at the export's entry |
|
||
| `extra-export` | both | a seventh export, a function | `refused R10`, at that export's entry |
|
||
| `exported-global` | both | an exported global | `refused R10`, at that export's entry |
|
||
| `missing-run` | both | no export `wai_run` | `refused R10`, at the module's length |
|
||
| `over-functions` | both; `functions` 8 | nine functions | `refused R11`, at the function section's id byte |
|
||
| `tail-call` | both | `return_call` | `refused R12`, at the instruction |
|
||
| `exception` | both | a tag section and `throw` | `refused R12`, at the tag section's id byte |
|
||
| `struct-type` | both | a struct type in the type section | `refused R12`, at that type entry |
|
||
| `call-ref` | both | `call_ref` of a `ref.func` naming `wai_abi`, which a declarative element segment declares | `refused R12`, at the instruction |
|
||
|
||
Every planted module but `component` is the module `minimal-int` (below) with the change
|
||
named, which breaks the rule named. Two of them, `import-memory` and `no-memory`, cannot break
|
||
it alone, and their rows say which other rule they break and why the verdict is the one stated.
|
||
Where a row adds an instruction, unless it says otherwise, the instruction is added to `wai_run`
|
||
before its store, its operands are `i32.const`s, `v128.const`s or `ref.null func`, as its type
|
||
takes, and a result it leaves is dropped. None of those is a floating-point item, so the
|
||
instruction is the first item of the module that breaks a rule. An instruction that takes a
|
||
floating-point operand has an earlier item that gives one, which §4.3 refuses first, except in
|
||
code the operand stack reaches as polymorphic, as `float-unreachable` places it.
|
||
|
||
**Conforming modules.**
|
||
|
||
| case | the module | verdict |
|
||
|---|---|---|
|
||
| `minimal-int` | the interface of §5 and nothing else: one memory of minimum 1 page and maximum 2; no table, global, data segment, element segment or custom section; `wai_abi` returns 1, `wai_alloc` returns 0, `wai_run` stores one fixed byte at offset 0 with `i32.store8` and returns 0, `wai_out_ptr` returns 0 and `wai_out_len` returns 1; no function reads a parameter | `held` at both levels |
|
||
| `vector-int` | `minimal-int` with `i32x4.dot_i16x8_s`, `i8x16.swizzle`, `i8x16.shuffle` and `i16x8.q15mulr_sat_s` | `held` at both levels |
|
||
| `bulk-and-sign` | `minimal-int` with `memory.copy`, `memory.fill`, `i32.extend8_s` and a block of two results | `held` at both levels |
|
||
| `fixed-table` | `minimal-int` with one `funcref` table, its minimum its maximum, an element segment and `call_indirect` | `held` at both levels |
|
||
| `fixed-memory` | `minimal-int` whose memory's minimum is its maximum, with no `memory.grow` | `held` at both levels |
|
||
| `custom-section` | `minimal-int` with a custom section named `profile` whose contents are the float level's identifier | `held` at both levels |
|
||
| `minimal-float` | `minimal-int` with `f64.add` of two `f64.const`s in `wai_run` | `held` at the float level; `refused R3` at the integer level, at the first `f64.const` |
|
||
|
||
R13 is not a checker's rule; §9.2's planted module breaks it.
|
||
|
||
### 9.2 The module corpus
|
||
|
||
The module corpus of a capability holds the cases a module that decodes it is attested on (§6,
|
||
check 9c):
|
||
|
||
- each committed vector and refusal fixture of the capability's conformance corpus, as the text
|
||
that defines its payload names that corpus: the module is given the vector's capability, payload
|
||
and set (§5), and the case expects the vector's output digest or its refusal code;
|
||
- for a module that contains `memory.grow`, the **grow-refusal cases**: for each vector `v`, and
|
||
each `k` from 1 to the number of `memory.grow` instructions the module executes decoding `v`,
|
||
the case `(v, k)`, in which the `k`-th of them fails and returns −1 and every other succeeds.
|
||
The case expects a refusal and no output: a trap, or `module_resources` where the failure was in
|
||
`wai_alloc`. A module whose memory's minimum equals its maximum and which contains no
|
||
`memory.grow` has none of these cases.
|
||
|
||
The planted module **`grow-swallowed`** meets R1 to R12 at the integer level, and, when a
|
||
`memory.grow` within `wai_run` fails, continues and returns `0` with an output. Its record fails
|
||
the grow-refusal case `(v, 1)` of any vector whose decode grows, so check 9c refuses it.
|
||
|
||
### 9.3 The probe
|
||
|
||
The probe is a module that meets R1 to R12 at the float level, and whose `wai_run`, for any
|
||
input, outputs the results of the cases below, in order: each result's bits in little-endian
|
||
order, four bytes for an `f32`, eight for an `f64`, and sixteen for a `v128`, lane 0 first. Each
|
||
input is written as its bits. A case's expected result is the one the deterministic profile
|
||
prescribes: the positive canonical NaN, `0x7fc00000` for `f32` and `0x7ff8000000000000` for
|
||
`f64`, in every lane.
|
||
|
||
| case | operation | operands |
|
||
|---|---|---|
|
||
| 1 | `f32.add` | `0xffa00001`, `0x3f800000` |
|
||
| 2 | `f32.mul` | `0xffa00001`, `0x40000000` |
|
||
| 3 | `f32.div` | `0x00000000`, `0x00000000` |
|
||
| 4 | `f32.sub` | `0x7f800000`, `0x7f800000` |
|
||
| 5 | `f32.sqrt` | `0xbf800000` |
|
||
| 6 | `f32.min` | `0xffa00001`, `0x3f800000` |
|
||
| 7 | `f32.nearest` | `0xffa00001` |
|
||
| 8 | `f32.demote_f64` | `0xfff4000000000001` |
|
||
| 9 | `f64.add` | `0xfff4000000000001`, `0x3ff0000000000000` |
|
||
| 10 | `f64.promote_f32` | `0xffa00001` |
|
||
| 11 | `f32x4.add` | lanes `0xffa00001`, `0x7fa00001`, `0xffc00000`, `0x7f800000`; lanes `0x3f800000`, `0x3f800000`, `0x3f800000`, `0xff800000` |
|
||
| 12 | `f64x2.mul` | lanes `0xfff4000000000001`, `0x7ff8000000000001`; lanes `0x4000000000000000`, `0x4000000000000000` |
|
||
|
||
The probe is about NaNs only. The deterministic profile's other rule, on relaxed vector
|
||
instructions, is unreachable from a module of either level (R4).
|
||
|
||
## 10. Receipts
|
||
|
||
A sink that records a `reconstruct-session` claim (jwp-receipts §2.4.12) of a decode through a
|
||
module writes, in that decoder entry, the module members of that section:
|
||
|
||
- `module_profile`: the identifier of the level the module was held at (§2);
|
||
- `engine`: the engine that ran it, written as §7 states;
|
||
- `engine_settings`: the terms of §7 the run was under, in ascending byte order, none twice;
|
||
- `module_probe`: `match` where check 10's probe, or at the integer level its vector, ran and
|
||
matched in this process, and `none` where none ran;
|
||
- `conform_result`: the SHA-256 of the RFC 8785 form, `sig` included, of the record check 9c
|
||
admitted.
|
||
|
||
A claim verifier that implements this profile checks them under jwp-receipts §2.4.12's rules 15
|
||
to 17. One that does not refuses an entry that carries them (`unknown_member`), as that section
|
||
states.
|
||
|
||
## 11. Conformance
|
||
|
||
- **This text.** Every rule of §4.1 names the clause of WebAssembly 3.0 or of WAI it rests on, and
|
||
§9.1, or for R13 §9.2, names at least one planted module that breaks that rule and no other,
|
||
with the verdict or the refusal it gets.
|
||
- **A conforming checker** gives every module of §9.1 the verdict stated there, and every module
|
||
the verdict §4.3 decides. A verdict is a function of the module's bytes, the level and the caps,
|
||
so two conforming checkers give every module the same verdict, byte for byte; a module on which
|
||
two checkers differ shows that one of them does not conform.
|
||
- **A conforming module** at a level gets `held` at that level from a conforming checker, and
|
||
meets R13.
|
||
- **A conforming sink** takes the steps of §6 in order for every object it decodes through a
|
||
module, with the settings of §7; reports the codes of §8; and names the module in its receipt as
|
||
§10 states. It states a tier for a module's output only as §2 and §6 give it.
|
||
|
||
## 12. What holding a module shows
|
||
|
||
That a sink holds a module at the integer level shows that the module's output is the same on
|
||
every engine that implements WebAssembly 3.0, or that there is none: a run that meets a limit is
|
||
refused, never output differently. At the float level it shows the same on every engine that
|
||
executes the deterministic profile's NaN rule. The record of check 9c shows that the module, on
|
||
its capability's corpus and its grow-refusal cases, gave what the corpus states. That the module
|
||
decodes every other input as its capability's text fixes is not something the profile shows: the
|
||
tier a sink states rests on the capability's registration, the grant and the record together.
|