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
1 file changed,
+21
−0
docs/superpowers/specs/2026-08-30-docs-design.md+21 −0
No CI script — run details
Intent
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 |