fix: per-room CSPRNG seed for unseeded rooms — the Ctx carried a shared zero#77
Merged
Conversation
…ed zero The guest runtime (Go and Rust SDKs alike) seeds the room PRNG from the CallContext seed verbatim, but the host encoded the raw RoomConfig into the Ctx: without --seed, every room of every game received Seed=0 and drew the one identical "random" stream. Identical blackjack shoes in every room, and casino outcomes learnable by replaying a throwaway room. The derived seed the host already computed for the WASI entropy source never reached the Ctx — and was itself the room-start timestamp, guessable to within a small search window. Both now come from one per-room CSPRNG draw, patched into h.cfg at OnStart so every encoded Ctx carries it. Host-side only: deployed game binaries pick up the fix without a rebuild. Explicit seeds (--seed/-seed, conformance, hibernation restore) are unchanged. The fixture gains a 'g' command logging r.Rand() so the regression test proves the property through the real wasm path — two unseeded rooms must diverge, same explicit seed must not. The doubles (kittest, TestRoom's own rng, native devrun) all derive real seeds, which is why every existing test missed this; ABI.md now states the host's obligation normatively. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Every production room of every wasm game plays the same "random" stream. The guest runtime (Go and Rust SDKs alike) seeds the SDK room PRNG from the CallContext seed verbatim (
internal/game/run.go,rust/src/rt.rs) — but the host encoded the rawRoomConfiginto the Ctx, and without--seedthat isSeed=0, SeedSet=false. Result:rand.NewSource(0)/SplitMix64::new(0)in every room.User-visible: the identical blackjack shoe (and blackjack-challenge shoe) in every room. Exploitable: pokies/scratchies/roulette outcomes are learnable by replaying a throwaway room, then betting the known stream in a fresh one.
The host already derived a per-room seed when
SeedSetwas false — but only fed it to the WASI entropy source, never the Ctx. And that derivation was the room-start timestamp, guessable to within a small search window.The fix (host-side only)
wasmHandler.OnStartnow draws one per-room seed from the host CSPRNG when!SeedSetand patches it intoh.cfg.Seed, so every encoded Ctx and the WASI entropy source carry it. Deployed game binaries pick up the fix with no rebuild — both guest SDKs already consume the Ctx seed correctly; they were just handed a constant.Explicitly seeded runs are untouched:
--seed/-seed, conformance determinism checks, and hibernation restore (which already snapshotsh.seedand restores it withSeedSet: true) all behave exactly as before.Why nothing caught it
Every double derives a real seed: the native dev runner uses
time.Now().UnixNano(), kittest and the conformance harness use explicit seeds, and the host-sideroomHandle.Rand()derivesFNV(roomID)^nano. Wasm-ABI-path-only — same blind spot as the casino zero-balance bug (#73).Tests
The fixture gains a
'g'command loggingr.Rand()(fixture.wasm rebuilt, keeping its v2.0.2 kit pin so it models deployed game binaries).host/gameabi/seed_test.goproves through the real wasm path:TestUnseededRoomsGetDistinctGameRand— two production-shaped rooms must diverge (failed before the fix: both logged the seed-0 stream's first draw)TestExplicitSeedKeepsGameRandDeterministic— same explicit seed reproduces the stream; different seeds divergeABI.md §4.1 now states the host obligation normatively:
seedMUST be per-room unpredictable when the operator didn't request one.Full suite +
go vet+-raceon gameabi: green. Changeset: kit patch.🤖 Generated with Claude Code