oficina / docs Public

Spec: contained UB and an execution cap, both found by executing

Two §5.2 additions from plan 1, neither anticipated when the design was
written.

yrs calls from_utf8_unchecked on wire data with no validation, so a
corrupted update reaches undefined behaviour -- hit on 0.4-5.1% of
single-byte corruptions of our own fixtures. Random bytes are harmless,
which is exactly why a naive garbage-input test passes and proves
nothing. Not fixable from our layer; contained by the per-document wasm
instance and filed upstream.

That makes the sandbox load-bearing for security rather than only
portability: inside wasm the fault is a contained trap where a native
release build corrupts silently. An argument for the wazero design that
nobody made when it was chosen.

And a 36-byte corrupted fixture ran apply for 163 real seconds before
returning the correct answer. Pathological slowness is invisible to a
correctness test and is denial of service on a server that accepts
updates from anyone with a share link. The host caps execution and treats
exhaustion as a trap. The original design covered an instance that dies
and said nothing about one that never returns.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011F5CivVm4CmL9ByfSSAaXA
Paul Campbell pushed by paul@keymail.dev f7d2e161e93b882e8489391d3da090b344147d86 parent c48cfe2
1 file changed, +21 −0
  • docs/superpowers/specs/2026-08-30-docs-design.md +21 −0

No CI script — run details

diff --git a/docs/superpowers/specs/2026-08-30-docs-design.md b/docs/superpowers/specs/2026-08-30-docs-design.md
index 4a42cde..8733e0d 100644
--- a/docs/superpowers/specs/2026-08-30-docs-design.md
+++ b/docs/superpowers/specs/2026-08-30-docs-design.md
@@ -248,6 +248,27 @@ cycle is bursty and coarse.
instance's linear memory must then be considered unusable. The host
discards that instance and rebuilds the document from its latest snapshot
plus subsequent updates. The request fails cleanly; the document does not.
+- **A malformed update can trap, and that is contained rather than fixed.**
+ *(Found by executing plan 1, 2026-08-30.)* `yrs-0.23.5` calls
+ `std::str::from_utf8_unchecked` on wire data with no validation
+ (`src/encoding/read.rs:137`, and two further sites), so a corrupted update
+ reaches undefined behaviour. A mutation fuzz hit it on **0.4–5.1% of
+ single-byte corruptions** of our own fixtures; purely random bytes are
+ harmless, which is why a naive garbage-input test passes and proves nothing.
+ We cannot validate our way out — locating string fields inside a Yjs update
+ means parsing it, which is what `yrs` does. Filed upstream.
+ **This makes the wasm sandbox load-bearing for security, not only
+ portability**: inside wasm the fault is a contained trap, where a native
+ release build would corrupt silently. That is an argument for the wazero
+ design that was not made when it was chosen.
+- **Execution must be capped, not merely trapped.** A 36-byte corrupted
+ fixture ran `apply` for **163 real seconds** before returning successfully —
+ pathological slowness, not a hang, and invisible to any correctness test
+ because the answer is right. On a server accepting updates from anyone
+ holding a share link that is a denial-of-service vector. The host therefore
+ sets a fuel or deadline bound per call and treats exhaustion like a trap.
+ The original design covered an instance that dies; it said nothing about an
+ instance that never returns, and both are denial of service.
- **Instances are not concurrency-safe.** One module instance per open
document, mutex-guarded, held in a pool.
- **Linear memory grows and never shrinks.** One large layout permanently