From 7541a51a42d4130da486e59680a53b70bf106479 Mon Sep 17 00:00:00 2001 From: Brandon Cook Date: Sun, 5 Jul 2026 21:17:06 +1000 Subject: [PATCH] =?UTF-8?q?fix:=20per-room=20CSPRNG=20seed=20for=20unseede?= =?UTF-8?q?d=20rooms=20=E2=80=94=20the=20Ctx=20carried=20a=20shared=20zero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .changeset/fix-unseeded-room-rng.md | 5 ++ ABI.md | 7 +++ host/gameabi/host.go | 19 ++++++- host/gameabi/seed_test.go | 55 +++++++++++++++++++++ host/gameabi/testdata/fixture/fixture.wasm | Bin 56525 -> 58617 bytes host/gameabi/testdata/fixture/main.go | 5 ++ 6 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-unseeded-room-rng.md create mode 100644 host/gameabi/seed_test.go diff --git a/.changeset/fix-unseeded-room-rng.md b/.changeset/fix-unseeded-room-rng.md new file mode 100644 index 0000000..d9f7efe --- /dev/null +++ b/.changeset/fix-unseeded-room-rng.md @@ -0,0 +1,5 @@ +--- +"kit": patch +--- + +Fix every unseeded wasm room sharing one deterministic RNG stream. The guest runtime seeds the SDK room PRNG (`r.Rand()` in Go, `rng_u64` in Rust) from the CallContext seed verbatim, but for a room without an operator-set seed the host encoded the raw config zero — so every room of every game drew the identical "random" sequence (identical blackjack shoes across rooms; casino outcomes learnable by replaying a throwaway room and then betting on the known stream). The host now derives a per-room seed from the host CSPRNG when `SeedSet` is false and carries it in the room's config, so the Ctx (and the WASI entropy source, which previously used the guessable room-start timestamp) are seeded per room. Host-side only: existing game binaries pick up the fix without a rebuild. Explicitly seeded runs (`--seed` / `-seed`, conformance, hibernation restore) are unchanged and stay deterministic. diff --git a/ABI.md b/ABI.md index d7a069e..ed869a4 100644 --- a/ABI.md +++ b/ABI.md @@ -157,6 +157,13 @@ u16 memberCount on `leave`, includes the departed entry u8 settled 0/1 ``` +**Seed.** The guest runtime seeds the SDK room PRNG from `seed` verbatim, so +the host MUST populate it with a per-room unpredictable value when no seed was +requested — never a shared constant (every room would deal the identical +"random" stream). `seedSet=1` means an operator explicitly requested this seed +(deterministic dev/test runs, hibernation restore); the value is equally +binding either way. + **Roster-epoch member-section forms (minor addition).** For a guest whose meta declares `CtxFeatRosterEpoch` (§4.2), the host MAY replace the member section with one of two sentinel forms keyed on `memberCount` (real rosters diff --git a/host/gameabi/host.go b/host/gameabi/host.go index dbbc089..f5d03c7 100644 --- a/host/gameabi/host.go +++ b/host/gameabi/host.go @@ -2,8 +2,10 @@ package gameabi import ( "context" - "errors" + crand "crypto/rand" "crypto/sha256" + "encoding/binary" + "errors" "fmt" "io" "log/slog" @@ -529,7 +531,20 @@ func (h *wasmHandler) OnStart(r sdk.Room) { h.nowNanos = r.Now().UnixNano() h.seed = h.cfg.Seed if !h.cfg.SeedSet { - h.seed = h.nowNanos + // The guest runtime seeds its SDK PRNG (r.Rand()) from the CallContext + // seed verbatim, so an unseeded room MUST carry a per-room seed in its + // cfg — left at the zero value, every room of every game would share + // one deterministic stream (identical blackjack shoes; casino outcomes + // learnable from a throwaway room). Derived from the host CSPRNG, not + // the room clock: a start timestamp is guessable to within a small + // search window, which a wagering game cannot afford. + var b [8]byte + if _, err := crand.Read(b[:]); err == nil { + h.seed = int64(binary.LittleEndian.Uint64(b[:])) + } else { + h.seed = h.nowNanos // CSPRNG unreachable; degraded, not fatal + } + h.cfg.Seed = h.seed } inst, err := h.game.compiled.Instance(context.Background(), diff --git a/host/gameabi/seed_test.go b/host/gameabi/seed_test.go new file mode 100644 index 0000000..6032657 --- /dev/null +++ b/host/gameabi/seed_test.go @@ -0,0 +1,55 @@ +package gameabi + +import ( + "log/slog" + "strings" + "testing" + + "github.com/shellcade/kit/v2/host/sdk" +) + +// readGameRand starts a fixture room with cfg, presses 'g', and returns the +// guest's logged r.Rand() value — the SDK room PRNG the guest runtime seeds +// from the CallContext seed (NOT the 'r' WASI entropy source). +func readGameRand(t *testing.T, cfg sdk.RoomConfig) string { + t.Helper() + g := loadFixture(t, Options{}) + cap := &logCapture{} + tr := sdk.NewTestRoom(g, cfg, sdk.Services{Log: slog.New(cap)}) + tr.Start() + tr.Join(p1) + tr.Input(p1, runeIn('g')) + line, ok := cap.findLine("fixture: game_rand=") + if !ok { + t.Fatalf("no game_rand log; got %v", cap.lines()) + } + return strings.TrimPrefix(line, "fixture: game_rand=") +} + +// TestUnseededRoomsGetDistinctGameRand: two production-shaped rooms (no +// explicit seed: Seed=0, SeedSet=false) must NOT share an r.Rand() stream. +// The guest runtime seeds its PRNG from the CallContext seed verbatim, so the +// host must place a per-room derived seed there when the operator did not set +// one — otherwise every room of every game deals the identical "random" +// sequence (and casino outcomes are learnable by replaying a fresh room). +func TestUnseededRoomsGetDistinctGameRand(t *testing.T) { + cfg := sdk.RoomConfig{Mode: sdk.ModeSolo, Capacity: 1, MinPlayers: 1} + a, b := readGameRand(t, cfg), readGameRand(t, cfg) + if a == b { + t.Fatalf("two unseeded rooms produced the identical r.Rand() stream: %s", a) + } +} + +// TestExplicitSeedKeepsGameRandDeterministic: the dev/--seed contract is +// untouched by the unseeded-room derivation — the same explicit seed still +// reproduces the same r.Rand() stream. +func TestExplicitSeedKeepsGameRandDeterministic(t *testing.T) { + cfg := sdk.RoomConfig{Mode: sdk.ModeSolo, Capacity: 1, MinPlayers: 1, Seed: 7, SeedSet: true} + if s1, s2 := readGameRand(t, cfg), readGameRand(t, cfg); s1 != s2 { + t.Fatalf("same explicit seed produced different r.Rand() streams: %s vs %s", s1, s2) + } + other := sdk.RoomConfig{Mode: sdk.ModeSolo, Capacity: 1, MinPlayers: 1, Seed: 8, SeedSet: true} + if s1, s3 := readGameRand(t, cfg), readGameRand(t, other); s1 == s3 { + t.Fatalf("different explicit seeds produced the identical r.Rand() stream: %s", s1) + } +} diff --git a/host/gameabi/testdata/fixture/fixture.wasm b/host/gameabi/testdata/fixture/fixture.wasm index c440906f543a4b8f68aebf1f62ea32f10d7f5b8d..d7402b7d1ddf8106ea49f575e8d744b4b1a4592c 100644 GIT binary patch delta 10903 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 S+_^Pz^SMXy)0N*m$p0T?$c@1O diff --git a/host/gameabi/testdata/fixture/main.go b/host/gameabi/testdata/fixture/main.go index d99c4c6..d592e31 100644 --- a/host/gameabi/testdata/fixture/main.go +++ b/host/gameabi/testdata/fixture/main.go @@ -13,6 +13,7 @@ // 'c' config_get("greeting"), logged // 't' log the guest's own time.Now().UnixNano() (== the room clock) // 'r' log 8 bytes from crypto/rand (the WASI-virtualized entropy source) +// 'g' log the next value from r.Rand() (the SDK room PRNG, Ctx-seeded) // 'd' arm a 250ms countdown deadline; each wake renders remaining ms, then // when CallContext time passes the deadline it renders BOOM and ends // @@ -149,6 +150,10 @@ func (rm *room) OnInput(r kit.Room, p kit.Player, in kit.Input) { // rooms with the same seed log identical entropy. _, _ = crand.Read(rm.entropy[:]) r.Log("fixture: rand=" + hex8(rm.entropy)) + case 'g': + // The SDK room PRNG — seeded by the guest runtime from the CallContext + // seed, distinct from the 'r' WASI entropy source. + r.Log("fixture: game_rand=" + strconv.FormatInt(r.Rand().Int63(), 10)) case 'd': // Arm a countdown anchored to the room clock; the host drives it forward // with wakes (CallContext time), no host-side timer involved.