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
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]>
189 lines
11 KiB
Markdown
189 lines
11 KiB
Markdown
# 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).
|