open-standards/wai/SECURITY.md
Transaction Science 6c8eb07afe
All checks were successful
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 9s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 10s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 9s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 9s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 6s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 7s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 8s
WAI energy receipts / does this change touch it (pull_request) Successful in 6s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 6s
wai-fleet service / does this change touch it (pull_request) Successful in 6s
WAI operation catalogue / does this change touch it (pull_request) Successful in 13s
WAI conformance / does this change touch it (pull_request) Successful in 13s
wai-key-proxy / does this change touch it (pull_request) Successful in 6s
wai-ledger service / does this change touch it (pull_request) Successful in 6s
WAI neural sink gates / does this change touch it (pull_request) Successful in 5s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 7s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 6s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 9s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 7s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 14s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 15s
WAI Studio / does this change touch it (pull_request) Successful in 6s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 7s
wai-web wasm32 / does this change touch it (pull_request) Successful in 13s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m27s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 33s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m8s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 2m56s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 33s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 3m29s
wai-fleet service / service · linux (pull_request) Successful in 2m40s
wai-key-proxy / proxy · linux (pull_request) Successful in 1m5s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m17s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m50s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 9m14s
wai-ledger service / service · linux (pull_request) Successful in 2m4s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 5m10s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m51s
WAI neural sink gates / the exact JPEG-AI decoder reports its path (pinned inputs, required) (pull_request) Successful in 2m38s
wai-pack / wai-pack · linux (pull_request) Successful in 1m47s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 3m57s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 2m57s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 2m31s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 8m25s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 3m32s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m36s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 5m39s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m29s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m56s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 11m58s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 30m34s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 38m39s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 1m5s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 1m47s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 10m35s
wai-fleet service / service · macos (pull_request) Successful in 42s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 10m37s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 55s
wai-ledger service / service · macos (pull_request) Successful in 1m10s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 2m1s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 16m26s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 12m18s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 6m34s
wai-pack / wai-pack · macos (pull_request) Successful in 2m57s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 7m29s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 14m38s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 11m1s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 15m43s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 15m44s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 12m18s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m24s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 1m11s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 3m35s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 6m14s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 5m41s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 44s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 12m22s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 16s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 32s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m3s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m38s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m53s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 17m20s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 25s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 15s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 6m59s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 18m2s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h12m0s
wai/prior-carriage-spec, converted to the layout of the fix: no shared append point
The change of wai/prior-carriage-spec (98192fe63..26834bb4c, 6 commits), on the fix branch: its
changelog entries as files of wai/changes/, its wai-conformance.yml edits as step files,
corpus files and test tags, and every other file as the branch has it.

- e5db9b313 wai: where a parameter set is fetched — set roots, location hints, and <base> defined
- 29fe43ea2 wai: a parameter set as an RFC 9842 compression dictionary, with a vector a stock client checks
- faaeb6c73 wai: when a parameter set is worth holding — amortisation charged per sink, bootstrap and generations
- 9a820db4a wai: a parameter set's usage statement, sealed sets released by key, leakage, and the privacy of held sets
- 89a9f9924 wai: the chunk-tree prior form, wai.prior.tree — a set verified piece by piece from any peers
- 26834bb4c wai: one playlist line of set roots, inner forms as the layouts list them, and a listing's usage statement read or the set refused

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 01:45:33 -05:00

272 lines
18 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 values | at most 4 194 304 JSON values (2²²), the manifest object and every value in it at any depth; a member's name is not one. One more is refused while the values are counted, before anything is built from the manifest (`manifest_too_many_values`; `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).
**What parsing a manifest takes.** A parsed value takes more memory than its text, and
many small values can take many times their length, so the length limit bounds what a
reader must read and not what a parse costs. The value limit bounds the rest. The
reference readers count a manifest's values before they build anything from it
(`check_manifest_text`) and refuse it as soon as the count passes the limit; they then
build each array and object at the size counted, and move the manifest's members out of
that one tree (`read_manifest`, `read_manifest_v2`). On a 64-bit target a read allocates
at most 128 bytes for each value and one for each byte of the manifest, and for a `WAI2`
one rendition entry of 768 bytes for each it lists: at most 640 MiB for a `WAI1`
manifest at both limits, whatever its shape, and 48 MiB more for a `WAI2`'s entries.
`tests/manifest_parse_memory.rs` measures this with an allocator that counts every byte
asked for: a manifest of small values at the length limit is refused for its values
before anything is built; one at the length limit within the value limit is read; a
manifest of each shape at the value limit is read and one value past it refused; and so
are the largest manifest a conforming staged layer has and a `WAI2` listing as many
renditions as its table holds. Each stays within the bound above.
`tests/manifest_reader_equivalence.rs` holds these readers to the result of the derived
reader they replace, for every envelope committed under `wai/` and for each with every
member removed or given a value of each JSON type.
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, or past the value limit, as the reference does. `http_verify.py` and `wai_http_vectors` agree on
`parse_head` at each limit in the HTTP corpus; the other three each build an envelope in
memory with a manifest one byte past the length limit, and one value past the value
limit, and require each 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`).
`wai.text.zstd` and `wai.text.xz` carry streams whose formats bound neither what a
payload decodes to nor the history its decoder must keep, so the specification sets
both (SPEC §2): at most 2²⁷ decoded bytes, and at most 2²⁷ bytes of window or
dictionary. The reference decoders (`codecs/text.rs`) write their output into a buffer
that is never sized past the length limit and refuse the payload when its output would
pass it (`text_decoded_too_long`); a Zstandard frame that declares a longer content
size is refused before anything is decoded. They give the decoding library the window
limit, so a stream that declares more is refused from its header, before the library
allocates it (`text_window_too_large`). The encoders refuse input past the length limit.
`tests/text_decode_bounds.rs` checks each limit at its edge, on streams made in the test
that are at it and one step past it; `tests/text_decode_memory.rs` measures, with a
counting allocator, that refusing a payload of a few kilobytes that would expand to
four times a limit takes no more output buffer than the limit. The C entry points
`wai_text_zstd_decode` and `wai_text_xz_decode` are these decoders.
The picture, audio and video payloads of the floor and the recommended set have reader
limits as well (SPEC §2): a picture at most 65 536 pixels wide and tall and 2²⁷ pixels
in all, audio of at most 2²⁷ samples counting every channel, and AV1 of at most 2²⁰
temporal units whose frames are pictures within those limits. The reference decoders
(`codecs/image.rs`, `codecs/audio.rs`, `codecs/video.rs`) refuse a picture from the
size its header declares, before they decode it (`image_too_large`): PNG and JPEG
through the `image` crate's header read, AVIF through libavif's own decoder limits,
which it checks against the `ispe` of every image item while it parses the container
and again against the AV1 sequence header, and JPEG XL through a pass of libjxl that
reads only the basic information. Each decodes only its own format. The FLAC decoder
refuses a `STREAMINFO` whose declared total passes the limit before it decodes, and
never sizes its output from the declaration past the limit; both audio decoders count
their output as it grows and refuse it past the limit (`audio_decoded_too_long`). The
AV1 decoder refuses a framing header past the limits (`video_too_large`), more temporal
units than the header declares, and a frame larger than it declares, and decodes every
layout and depth AV1 allows rather than assuming 8-bit 4:2:0. `av1_decode_each` hands
on one frame at a time, so its memory is one frame and dav1d's reference frames whatever
the payload's length; `av1_decode` and `wai_video_av1_decode`, which return every frame
at once, refuse a payload whose frames would take more than 4 GiB together, from its
header (`video_decoded_too_large`). The encoders refuse what a reader may refuse.
`tests/media_decode_bounds.rs` checks each limit at its edge and one past it, on real
payloads and on headers rewritten to declare more than their data holds.
## 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 (with dav1d inside it), 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:
- Most CI builds. One job builds `codecs`: `codec-wrappers` in `wai-conformance.yml`,
on the macOS runner, which runs the wrappers' fuzz smoke, the text and media decode
limits and the C entry points through the 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 workflow enables
`semantic`, and 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`.
The extension formats are held to the reader limits they share with SPEC §2
(`wai::limits`): a score or haptic signal renders at most 2²⁷ samples counting every
channel, an audio scene's stereo floor the same, and a splat4d clip, a film or a reel's
shots together at most 2²⁰ frames. Each reader refuses a contract past them when it
reads it (`duration_too_long`), before anything is rendered, and refuses a film or reel
schedule whose last tick a `u64` does not hold (the tick arithmetic used to wrap).
`tests/extension_duration_limits.rs` checks each at the limit and one past it.
The reader is fuzzed with coverage guidance. `wai-rs/fuzz/` holds four libFuzzer
targets, built by cargo-fuzz with AddressSanitizer:
- `envelope`: version detection, the `WAI1` and `WAI2` readers and the listing form;
- `int_payload`: `int_payload::decode_int` against each parameter set the integer
conformance corpus commits;
- `media`: the picture, audio, video and text decoders of the floor and the recommended
set;
- `containers`: the extension containers (world, causal op, film, reel, score, haptic,
splat4d, audio scene).
They are seeded from the repository's own envelopes, integer payloads and containers,
and from a valid payload per decoder. `wai-fuzz.yml` runs each for 15 minutes every
night on the macOS runner and keeps the corpus between nights. A find fails the job
and keeps its input; `fuzz/run.sh` says how to turn one into a regression test. On every
change, `tests/fuzz_smoke.rs` still runs seeded random bytes through each wrapper and
`Wai::from_bytes`.
Known gaps:
- **The C libraries are not instrumented.** AddressSanitizer covers the Rust code. The
codec libraries run as the system builds them, so a memory error inside one is seen
only when it crashes the process or exceeds a limit.
- **Replaying a world costs CPU in proportion to the tick it is asked for.** A film or
reel samples its embedded world at the ticks its schedule names, and the reference
replay steps every tick from genesis. The schedule's frames are bounded and its last
tick must fit a `u64`, but no limit yet bounds how far a schedule may reach, so a
small film can ask a renderer for a long replay. Memory is bounded; time is not.
- **The float neural decoders are not fuzzed.** They need the `neural` feature and its
runtime. The integer decoders are fuzzed.
## 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).
The privacy rules for the descriptor and for parameter-set fetches are
[http-delivery](extensions/http-delivery.md) §12.5: the sets a sink holds are kept per
top-level site, `held_sets` is off unless turned on for a site, it lists only the held
sets the content being chosen among pins, nothing is sent or named across sites,
clearing a site's data clears its sets, and a report of which sets sinks hold is an
aggregate, never one sink's list. `prior_carriage::privacy` builds the member under
them (feature `prior_carriage`); no code in this tree sends the descriptor.
A parameter set fitted to a catalog's frames can carry features of those frames, and its
digest names the title it was fitted to. [prior-carriage](extensions/prior-carriage.md)
§18 states the usage statement a set carries inside the bytes its pin covers, the sealed
form `wai.prior.sealed`, whose set no sink reads until its origin releases the key, and
what a set can disclose.
## Protected content
End-to-end encryption at the application layer (a room's sealed objects, SPEC §6 and [confidentiality](extensions/confidentiality.md)) 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).