open-standards/wai/SECURITY.md
Transaction Science 59ab0d9489
All checks were successful
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 1m59s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 3s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m8s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m0s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m30s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m9s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m12s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m12s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m18s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m9s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m49s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 28s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 7m57s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 20s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 13m52s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 29s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 54s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 1m58s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m10s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 1m6s
WAI neural decoders end-to-end / synthetic fixtures → Rust decode → reference (±4) (pull_request) Successful in 1m11s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m12s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 4s
WAI section references / cited sections exist (pull_request) Successful in 10s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 33m46s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 4m40s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 2m53s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
standards workspaces build + test / aac-rs (pull_request) Successful in 9m8s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 3m2s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 4m3s
standards workspaces build + test / map-rs (pull_request) Successful in 7m9s
standards workspaces build + test / mesh-rs (pull_request) Successful in 4m14s
standards workspaces build + test / openpay (pull_request) Successful in 31m58s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Successful in 1m11s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 10m31s
WAI HTTP delivery / HTTP corpus · falsification and differential (pull_request) Successful in 1h31m39s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 2m1s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 3m36s
wai-pack / wai-pack · linux (pull_request) Successful in 1m7s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 24s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 1m47s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m21s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 3m18s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m10s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m16s
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 30s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 12m1s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 31s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 3m31s
wai-pack / wai-pack · macos (pull_request) Successful in 2m11s
wai: SECURITY.md, SPEC §2 and §6.5 say what holds
SECURITY.md:
- "Allocation follows the input" was false for the manifest. The reference
  readers parse it into serde_json::Value; measured on the release build,
  a 94 MB prev of 2^20 runs peaks at about 7 times its length and a
  134,217,000-byte array of zeros at about 38 times (5.1 GB RSS, 7.6 s in
  from_bytes, which returned Ok). The text now says so, and the argument
  that a sink short of that memory could not decode such layers either is
  gone.
- CI does link ONNX Runtime and libzstd: wai-neural-gates.yml and
  wai-neural-e2e.yml build `neural`. No workflow builds `codecs`.
- No Kani harness is in int_kernels.rs; the harnesses are in rans.rs,
  int_entropy.rs, int_transform.rs, wai_video_policy.rs, staged/predict.rs
  and staged/wir1_arith.rs. Its SIMD kernels, the only unsafe outside the
  C ABI, are held to the scalar kernels by equality tests, not by a model
  checker.
- The bare "(§5.1)" names http-delivery; the writer test is described as
  it now runs; the Python verifiers' checks are described as they now run.

SPEC §2: the 2^20-unit cap is staged-delivery Appendix A.3's (section 3
bounds a count only by 32 bits); the limit bounds what a reader reads, not
the memory its parse takes; the paragraph is reflowed.

SPEC §6.5: Lane A's records are "among them", and include wai.video.ssai
and wai.video.channel. Lane B no longer says every record applies:
staged-measurement counts no encrypted object, and a verifier without the
key checks no payload-dependent rule of a reconstruct-session claim
(jwp-receipts §2.4.12 rule 3); nothing in the reference states a Lane B
tier. The confidentiality header and the CHANGELOG say the same.

SPEC §5 and determinism-tiers §5: only `photo` comes from a trained model;
gradient, bloom, facade and the cross-architecture golden use seeded
integer weights, as the CHANGELOG said both texts did.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 17:16:04 -04:00

189 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Security
This file says where WAI's reference implementation reads untrusted input, what it
bounds, and what it does not yet do. The rules a sink must follow are in
[`SPEC.md`](SPEC.md) and [`extensions/`](extensions/); this file points at them and
adds nothing normative.
## Reporting a vulnerability
Email **[email protected]**. Do not open a public issue or pull request
for a vulnerability that is not yet fixed.
Say which component is affected (a crate and file, a verifier, or a section of the
specification), the commit you tested, the input or steps that trigger it, what
happened and what you expected. An input file is the most useful thing to send.
In scope:
- the specification and its extensions, where a rule lets a conforming sink or
verifier be made to misbehave;
- the reference implementation: `wai-rs`, `wai-quantum`, `wai-web`, `wai-pack`,
`wai-relay`, `wai-verify` and the other crates under this directory;
- the conformance verifiers (`*-conformance/*.py`) and their corpora.
Out of scope: services anyone deploys with this code, which their operators answer for.
WAI is pre-1.0. Fixes go to `main` of the canonical repository,
`https://git.transaction.science/Transaction-Science/open-standards`; no earlier
release line is maintained.
## Manifest parsing
Every envelope a sink receives is untrusted. Its container and manifest are read by
`wai-rs/src/container.rs` (`Wai::from_bytes`, `WaiMulti::from_bytes`,
`manifest_bytes`) and, over HTTP, by `wai-rs/src/http_binding/head.rs`
(`parse_head`).
What the readers bound:
| what | bound | where it is specified |
|---|---|---|
| every length field (`man_len`, `payload_len`, each rendition's offset and length) | checked against the bytes in hand before anything is sliced; sums formed so they cannot wrap (`saturating_add`; the `WAI2` payload total in `u64`) | SPEC §2, §7.1 |
| manifest length | at most 134 217 728 bytes (2²⁷), sized to the largest manifest a conforming staged layer has. A longer one is refused from `man_len` alone, before it is read or, over HTTP, requested (`manifest_too_large`; `head_malformed` from `parse_head`) | SPEC §2, *Reader limits* |
| manifest nesting | at most 127 nested arrays and objects, the manifest object counting as one; 128 are refused | SPEC §2, *Reader limits* |
| member names | a member named twice in one object, at any depth, is refused | SPEC §2 |
| `WAI2` rendition table | at most 65 535 rows (`u16`), as many as the manifest's `renditions`, packed contiguously from offset 0 | SPEC §7.1 |
| an HTTP object | exactly one envelope; any other length is refused (`object_length_mismatch`) | http-delivery §5.1 |
| a head proof | at most 64 chaining values; `covered_len` is the object's length, or shorter and ending on a chunk boundary of the hashed input | http-delivery §6 |
A reader copies a payload only after its bounds are checked, so a payload never makes
it allocate more than it was given. A `WAI1` reader ignores bytes after the payload; an
HTTP object that carries any is refused ([http-delivery](extensions/http-delivery.md)
§5.1).
**Parsing a manifest takes more memory than its length.** The reference readers parse
the manifest into a JSON tree (`serde_json::Value`, for `target`, `refines`,
`derived_from` and every member this revision does not define), and a parsed value
takes more memory than its text: about seven times the length of a 94 MB `prev` that
names a producer for each of 2²⁰ units, the largest manifest a conforming layer has,
and nearly 40 times the length of a manifest of many small values, such as one long
array of zeros. A manifest at the length limit can take some 5 GB and several seconds
to parse, and a reader may not refuse it for its length (SPEC §2). The length limit
bounds what a reader must read, not what parsing it costs; the reference sets no limit
of its own on the parse.
The library tests in `container.rs` and `http_binding/tests.rs` check the manifest
limits at their edges: from its first eight bytes, every reader refuses a manifest one
byte past the length limit as too large and one at the limit only as truncated; a
2 MiB manifest is read and written back byte for byte; and 127 levels are read where
128 are refused. The writers (`Wai::to_bytes`, `WaiMulti::to_bytes`, and `pack` and
`packMulti` in `wai-web`) refuse a manifest past the length limit, and a payload whose
length does not fit its `u32` field. The Rust writers stop serialising as soon as the
manifest passes the limit; the test runs both writers at limits equal to an envelope's
manifest and payload lengths and one byte below them, since serialising 128 MiB takes a
debug build many seconds. `verify_pack.mjs` checks the browser writers at the real
limit.
The stdlib Python verifiers read envelopes too. `staged_verify.py`,
`claim_verify.py`, `pack_verify.py` and `http_verify.py` refuse a manifest past the
length limit as the reference does. `http_verify.py` and `wai_http_vectors` agree on
`parse_head` at the limit in the HTTP corpus; the other three each build an envelope in
memory with a manifest one byte past it and require it refused. Those that read
manifests through `staged_verify.strict_json` refuse nesting past 127; `pack_verify.py`
reads up to the interpreter's recursion limit and refuses past it as unparseable.
Payload formats that WAI defines carry their own caps, checked before the first symbol
is decoded: a tensor of at most 2²⁶ elements, a `WIV1` clip of at most 2³⁰ decoded
bytes and 2²⁰ frames, and a sink cost limit (`int_payload::check_cost`,
`cost_over_limit`) ([integer-payloads](extensions/integer-payloads.md) §7, §8).
`wai.neural.int_mlicpp` refuses a header whose shapes, or whose buffers, pass its cap
before it allocates them (`tests/int_mlicpp_preflight.rs`).
## Native codec libraries and sandboxed decode
The default `codecs` feature binds C and C++ libraries for the floor and the
recommended set: libzstd and liblzma for `wai.text.zstd` and `wai.text.xz`, libjxl
for JPEG XL, libavif for AVIF decode, libopus, dav1d for AV1 decode, and libFLAC for
FLAC encode. PNG, JPEG and FLAC decode run in Rust (the `image` and `claxon` crates).
The `neural` feature binds ONNX Runtime for the float neural decoders. These libraries
parse payload bytes directly, and they are the usual place a memory-safety defect
in a media stack lives.
They run in the caller's process, with its privileges: the reference implementation
has no process or system-call sandbox. A sink that decodes payloads from origins it
does not trust through these libraries should run the decode in a separate process
with no file-system or network access, or use a decoder compiled to WebAssembly and
run it in a WebAssembly runtime.
What does not touch them:
- CI builds, except the neural workflows: no workflow enables `codecs` or `semantic`,
so no CI build links the codec libraries. `wai-neural-gates.yml` and
`wai-neural-e2e.yml` build the `neural` feature, which links ONNX Runtime and
libzstd, and run the float neural decoders through them; no other workflow's build
links any of these libraries.
- WAI's own decoders are Rust: the integer codecs, worlds, scores, films and the
generator. Outside the C ABI (`ffi.rs`, `ffi_codecs.rs`, `ffi_neural.rs`), the only
`unsafe` in `wai-rs`'s library code is in the AArch64 SIMD kernels of
`int_kernels.rs`, each block with the bounds argument beside it.
- `wai-web`, the browser crate, links none of the C libraries and runs inside the
browser's WebAssembly sandbox.
Model checking covers part of the integer core: Kani harnesses beside the code in
`rans.rs`, `int_entropy.rs`, `int_transform.rs`, `wai_video_policy.rs`,
`staged/predict.rs` and `staged/wir1_arith.rs`, hosted by `wai-verify` and run by
`.github/workflows/wai-formal-verification.yml`. `wai-verify` also compiles
`int_kernels.rs`, on its scalar path, because `int_transform.rs` calls it, but no
harness reaches it: its AArch64 SIMD kernels, the only `unsafe` named above, are not
model-checked. Equality tests hold them to the scalar reference kernels, element by
element beside each kernel and over every committed integer-decoder vector
(`tests/int_kernels_vectors.rs`), on every platform leg of `wai-conformance.yml`.
Known gaps:
- **Decompression output is not bounded.** `wai.text.zstd`, in the floor, and
`wai.text.xz` decode the whole stream into memory, so a small payload can expand to
any size the format allows. The image, audio and video decoders allocate what the
bitstream declares, within the libraries' own limits. The reference sets no output
limit of its own.
- **Fuzzing is shallow.** `tests/fuzz_smoke.rs` feeds seeded random bytes to each
codec wrapper and to `Wai::from_bytes` and requires that nothing panics. It needs
the `codecs` feature, which no CI workflow builds, so it runs only by hand. There is
no coverage-guided fuzzing of the manifest parser, the integer payload parsers or the
codec wrappers.
## Module authorisation
A module is code a sink obtains apart from an envelope and runs to decode a
capability or to rebuild a reconstruction ([SPEC](SPEC.md) §4, *Module
authorisation*). A sink runs one only if it holds it under
[jwp-receipts](extensions/jwp-receipts.md) §7.9: a grant, signed under a module
origin's key log, naming the SHA-256 of the module's bytes exactly as the sink
instantiates them, its form, the capabilities it may decode and a validity window,
with revocation through the same key log. A module the sink does not hold leaves its
capability unadvertised for that envelope, and the envelope's fallback still runs.
Parameter sets pinned by digest are checked before use and revoked the same way
(SPEC §3.1, checks 5–8).
The reference dispatcher runs no module obtained apart from an envelope. With a key
log (`key_log::SinkKeyLog`, feature `key_discovery`) it refuses a revoked parameter
set (`prior_revoked`).
Authorisation decides which code runs, not what that code can reach. The
environment a module runs in (its imports, a memory limit, a time limit, and the
determinism rules an exact replay needs) is not specified yet. Until it is, a sink
that runs modules confines them itself, for example in a WebAssembly runtime that
gives the module no file-system, network, clock or random-number imports.
## Fingerprinting: advertised parameter sets
The Sink Capability Descriptor ([http-delivery](extensions/http-delivery.md) §12.1)
can list `held_sets`: for each capability, the digests of the parameter sets the
sink holds. Which sets a sink holds depends on what it has fetched before, so the
list can identify a sink across the sites it is sent to, much as a cookie does. A
shared compression dictionary named by its hash raises the same concern (RFC 9842).
Privacy rules for the descriptor and for parameter-set fetches are not written yet.
They are to be added to http-delivery: the list kept per top-level site, clearable by
the user, disclosed only as far as a choice of rendition needs it, never sent across
sites, and off by default. Until they land, send `held_sets` only to the origin that
needs it to choose a rendition, and never across sites. No code in this tree builds
or sends the descriptor.
## Protected content
End-to-end encryption at the application layer (`mls-sframe`, SPEC §6) keeps content
from relays and from anyone outside the group, not from a member's device: the
decrypted payload and the decoded output are in the application's memory. It is not a
protected-path robustness level ([SPEC](SPEC.md) §6.5).