oficina / docs Public

core: fail closed on unexpected entropy requests, not fake-succeed

__getrandom_v03_custom now returns Err(getrandom::Error::UNSUPPORTED)
instead of filling the buffer with zeros. This crate assigns every ID it
hands to yrs deliberately, so an entropy request is a bug we want to hear
about, not a value worth fabricating — yrs::Observer::subscribe() and
uuid_v4() both draw entropy on paths this crate doesn't exercise yet but
later stages (live sync) will.

Verified (and documented in the plan's task-3-report.md) that this does
not by itself make those two call sites panic: fastrand::Rng::new()
swallows a getrandom failure internally and substitutes a fixed fallback
seed rather than propagating it, confirmed by reading the vendored
fastrand source and by a live repro. Recorded as an open finding rather
than worked around.

Also rewrote vendor/README.md per review: states exactly what changed in
fastrand's Cargo.toml, cites the reproduced upstream getrandom E0599 plus
a related open issue, and gives a concrete removal trigger/command.
Confirmed by removing the patch and rebuilding that it is still required
(same E0599) — not deletable yet.
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 0cb352eb5951bd3d73a243868455bd60dcbb4288 parent 6529d43
2 files changed, +122 −36
  • core/src/lib.rs +20 −9
  • vendor/README.md +102 −27
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 05d3aeb..7afb0d1 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -26,19 +26,30 @@
//! for the `wasm32-unknown-unknown` target only (native builds, including
//! `cargo test`, are unaffected and keep the real OS entropy source). That
//! backend calls out to the `__getrandom_v03_custom` symbol defined below
-//! instead of any JS API. This module never actually depends on document
-//! GUIDs being unpredictable, so a fixed, non-cryptographic fill is fine.
+//! instead of any JS API.
+//!
+//! This crate assigns every ID it hands to `yrs` deliberately —
+//! `Doc::with_client_id` takes an explicit client ID — so nothing here
+//! legitimately needs entropy. But `yrs` itself draws entropy in a couple
+//! of places that are unreachable from what this crate does *today* and
+//! therefore untested by it: `Observer::subscribe()` (`yrs` src/observer.rs,
+//! both the `sync` and non-`sync` impls) draws a subscription ID from
+//! `fastrand::Rng::new()` and uses it as an `Origin` — with a fabricated
+//! fixed value, every subscription would collide and cancelling one would
+//! cancel another; `uuid_v4()` (`yrs` src/lib.rs) draws a document GUID the
+//! same way — with a fabricated fixed value, every document would share a
+//! GUID. Both become reachable once live document sync (this project's
+//! later stages) actually subscribes to change events or constructs fresh
+//! documents. An entropy request from this crate is therefore a bug we want
+//! to hear about the moment it happens, not a value to fabricate — so this
+//! hook fails instead of returning fake bytes. Fail closed, not fake-succeed.
#[cfg(target_arch = "wasm32")]
#[no_mangle]
extern "Rust" fn __getrandom_v03_custom(
- dest: *mut u8,
- len: usize,
+ _dest: *mut u8,
+ _len: usize,
) -> Result<(), getrandom::Error> {
- // SAFETY: `getrandom`'s custom-backend contract requires `dest` to be
- // valid for `len` bytes and fully initialized by this function.
- let buf = unsafe { std::slice::from_raw_parts_mut(dest, len) };
- buf.fill(0);
- Ok(())
+ Err(getrandom::Error::UNSUPPORTED)
}
/// Returns the ABI version this module exports. The Go host checks this on
diff --git a/vendor/README.md b/vendor/README.md
index 1f47c06..cce4acf 100644
--- a/vendor/README.md
+++ b/vendor/README.md
@@ -2,9 +2,11 @@
## fastrand-2.5.0
+### The problem
+
`yrs` (pinned exact in `core/Cargo.toml`) depends on `fastrand` with its
`js` feature enabled unconditionally, and fastrand's own manifest ties that
-feature, on `wasm32-unknown-unknown`, to `getrandom` with the `wasm_js`
+feature, on `wasm32`/`wasm64`-`unknown`, to `getrandom` with the `wasm_js`
feature forced on. That feature makes `getrandom` default to a
`wasm-bindgen`-based backend which imports host JS functions
(`crypto.getRandomValues`, etc.) — an import the Go/wazero host cannot
@@ -12,34 +14,107 @@ satisfy, and the thing `core/tests/no_host_imports.rs` gates against.
`yrs::Doc` calls into this path unconditionally on construction (even via
`Doc::with_client_id`, which still generates a random document GUID via
-`fastrand`). Merely referencing `yrs::Doc` anywhere in `docs-core`'s public
-API — required by `apply_update`/`text_content`, since the test suite calls
-them from a separate compilation unit against the `rlib` — is enough for
-those `wasm-bindgen` exports to end up linked into the final `.wasm` module,
-even though the code path is never reached at runtime.
+`fastrand::Rng::new()` -> `getrandom`). Worse: merely *referencing*
+`yrs::Doc` anywhere in `docs-core`'s public API — required for
+`apply_update`/`text_content`, since the test suite calls them from a
+separate compilation unit against the `rlib` — is enough for those
+`wasm-bindgen` exports to end up linked into the final `.wasm` module, even
+along code paths that are never reached at runtime. Reproduced in
+isolation (a scratch crate depending on nothing but `yrs = "=0.23.5"`, with
+a single `pub fn unused(_doc: &yrs::Doc) {}` and no method calls at all);
+see `task-3-report.md` in the plan directory for the full trace.
+
+### The fix, and why it needs vendoring
`getrandom` supports exactly this scenario: selecting its `custom` backend
via `--cfg getrandom_backend="custom"` (set in `docs-wt-core/.cargo/
config.toml`, scoped to the `wasm32-unknown-unknown` target only) routes
entropy requests to a symbol we provide ourselves
-(`__getrandom_v03_custom` in `core/src/lib.rs`), with no JS dependency.
-
-That override alone isn't enough, though: `getrandom-0.4.3`'s `error.rs`
-has a `Display` match arm for `Error::WEB_CRYPTO` gated only on
-`feature = "wasm_js"` (not on which backend is actually selected), while
-`Error::WEB_CRYPTO` itself is defined inside the `wasm_js` backend module,
-which a `custom`-backend override skips compiling entirely. As long as
-fastrand keeps requesting the `wasm_js` feature, this is a hard compile
-error (`E0599`) — Cargo features are additive across the whole dependency
-graph, so `docs-core` cannot un-request a feature another crate requires.
-
-This vendored copy of `fastrand-2.5.0` (unmodified Rust source; see the
-comment in its `Cargo.toml`) drops the forced `features = ["wasm_js"]` on
-its `wasm32`/`wasm64`-`unknown` `getrandom` dependency, so nothing in the
-graph requests that feature and the conflicting match arm is never
-compiled. `fastrand`'s actual logic (the `js` feature still exists, and
-`fastrand::Rng::new()` still calls into `getrandom`) is untouched — only
-which `getrandom` backend gets linked changes, and that is exactly what the
-`--cfg getrandom_backend="custom"` override is choosing on purpose.
-
-Wired in via `[patch.crates-io]` in `core/Cargo.toml`.
+(`__getrandom_v03_custom` in `core/src/lib.rs`, which — deliberately —
+fails rather than fabricating bytes; see the doc comment above it), with
+no JS dependency.
+
+That override alone doesn't compile, though. `getrandom-0.4.3`'s
+`src/error.rs:181-185` has a `Display`/`Debug` match arm:
+
+```rust
+#[cfg(all(
+ feature = "wasm_js",
+ target_arch = "wasm32",
+ any(target_os = "unknown", target_os = "none")
+))]
+Error::WEB_CRYPTO => "Web Crypto API is unavailable",
+```
+
+gated only on the `wasm_js` Cargo *feature* — not on which backend is
+actually *selected* via `--cfg getrandom_backend`. `Error::WEB_CRYPTO`
+itself is defined inside `src/backends/wasm_js.rs:71`, inside the `mod
+wasm_js;` declaration that a `getrandom_backend="custom"` override skips
+compiling entirely (`src/backends.rs`'s `cfg_if!` chain is mutually
+exclusive; `custom` is checked first and short-circuits the rest). Result,
+reproduced directly against the pinned version with nothing changed except
+adding the cfg override:
+
+```
+error[E0599]: no associated function or constant named `WEB_CRYPTO` found for struct `error::Error` in the current scope
+ --> .../getrandom-0.4.3/src/error.rs:185:20
+```
+
+As long as *anything* in the dependency graph requests fastrand's `js`
+feature on a wasm32/wasm64-unknown target — which `yrs` does,
+unconditionally, and which we cannot override, since Cargo features are
+additive across the whole graph and `docs-core` has no way to un-request a
+feature another crate requires — this is a hard compile error, regardless
+of which non-`wasm_js` backend `--cfg getrandom_backend` selects (`custom`,
+`unsupported`, etc. all hit the same match arm). This looks like a real
+gap in `getrandom` 0.4.3's own cfg-gating (the match arm's cfg should also
+require `not(getrandom_backend = "custom")` or equivalent, matching what
+the backend-selection `cfg_if!` already does) rather than anything specific
+to this project; `getrandom`'s own issue tracker has at least one adjacent,
+still-open report of the same problem area — a `custom`-backend override
+not fully insulating a build from a `wasm_js` feature forced on by another
+dependency in the graph — closed "not planned":
+<https://github.com/rust-random/getrandom/issues/633>. We did not find an
+issue number for this exact `E0599`/`WEB_CRYPTO` symptom; if one turns up,
+add it here.
+
+### What was changed
+
+This vendored copy of `fastrand-2.5.0` is the unmodified upstream Rust
+source (`src/`, `benches/`, `tests/`, licenses — byte-identical to the
+crates.io tarball); only `Cargo.toml` differs, by exactly one edit: the
+`wasm32`/`wasm64`-`unknown`-scoped `getrandom` dependency drops
+`features = ["wasm_js"]` (kept `optional = true`). Nothing in fastrand's
+actual logic changes — the `js` feature still exists, `fastrand::Rng::new()`
+still calls into `getrandom` the same way — only *which* `getrandom`
+backend ends up linked changes, and that's exactly what the
+`--cfg getrandom_backend="custom"` override in `.cargo/config.toml` is
+choosing on purpose. Wired in via `[patch.crates-io]` in `core/Cargo.toml`.
+
+### When this can be deleted
+
+Try removing the `[patch.crates-io]` block from `core/Cargo.toml` (and
+`vendor/fastrand-2.5.0/`) and running:
+
+```
+cargo build --release --manifest-path core/Cargo.toml --target wasm32-unknown-unknown
+```
+
+If that compiles cleanly (no `E0599`), delete the patch and this
+directory — that's the goal state. Concretely, that becomes true once any
+one of:
+
+1. A `getrandom` release fixes `error.rs`'s `WEB_CRYPTO` match arm to also
+ respect `getrandom_backend` (e.g. adds `not(getrandom_backend =
+ "custom")`), and `core/Cargo.toml`'s `getrandom = "=0.4.3"` pin is
+ bumped to that version.
+2. A `fastrand` release stops forcing `wasm_js` onto its optional
+ `getrandom` dependency (making it opt-in instead, which is what
+ getrandom issue #633 above is effectively asking for), and `yrs`'s pin
+ on it moves to that version.
+3. `yrs` stops requiring `fastrand`'s `js` feature at all for the
+ wasm32-unknown-unknown target — unlikely while it still generates
+ random document GUIDs internally, but would remove the whole problem.
+
+Verified as of this writing (2026-08-30) that the patch is still required:
+removing it and rebuilding reproduces the exact `E0599` above.