oficina / docs Public

Revalidate Home native streams and make WASM builds reproducible

Paul Campbell pushed by codex agent, operated by paul@keymail.dev 9d7b84acf0e0559dbfcc087ae2bdf4258ad3d5a3 parent b673a14
6 files changed, +100 −66
  • core/tests/wasm_trap_containment.rs +1 −1
  • internal/core/core_internal_test.go +57 −59
  • internal/core/testdata/docs_core.wasm binary
  • internal/docs/app.go +12 −1
  • internal/docs/native_stream_test.go +27 −4
  • scripts/build-wasm.sh +3 −1

CI failed — run details

diff --git a/core/tests/wasm_trap_containment.rs b/core/tests/wasm_trap_containment.rs
index 87e70aa..b0ac0d3 100644
--- a/core/tests/wasm_trap_containment.rs
+++ b/core/tests/wasm_trap_containment.rs
@@ -278,7 +278,7 @@ fn build_wasm() -> Vec<u8> {
});
let registry_root = cargo_home.join("registry").join("src");
let rustflags = format!(
- "--cfg getrandom_backend=\"custom\" --remap-path-prefix={}=/cargo-registry-src --remap-path-prefix={}=/docs-core",
+ "-C strip=symbols --cfg getrandom_backend=\"custom\" --remap-path-prefix={}=/cargo-registry-src --remap-path-prefix={}=/docs-core",
registry_root.display(),
workspace_root.display(),
);
diff --git a/internal/core/core_internal_test.go b/internal/core/core_internal_test.go
index dfe7251..a765d70 100644
--- a/internal/core/core_internal_test.go
+++ b/internal/core/core_internal_test.go
@@ -54,6 +54,8 @@ import (
"path/filepath"
"testing"
"time"
+
+ "github.com/tetratelabs/wazero/api"
)
// rng and mutate below are a byte-for-byte port of
@@ -305,54 +307,60 @@ func TestSlowUpdateIsCutOffByGuestCallTimeout(t *testing.T) {
t.Fatalf("expected the default callTimeout (%v), got %v", defaultGuestCallTimeout, rt.callTimeout)
}
- mod, err := rt.instantiate(ctx)
- if err != nil {
- t.Fatalf("instantiate: %v", err)
- }
+ warm := func() (api.Module, uint64, []byte) {
+ t.Helper()
+ mod, err := rt.instantiate(ctx)
+ if err != nil {
+ t.Fatalf("instantiate: %v", err)
+ }
- fixtureNames := []string{"hello", "unicode", "formatted", "concurrent"}
- fixtures := make([][]byte, len(fixtureNames))
- for i, name := range fixtureNames {
- fixtures[i] = loadFixtureBytes(t, name)
- }
+ fixtureNames := []string{"hello", "unicode", "formatted", "concurrent"}
+ fixtures := make([][]byte, len(fixtureNames))
+ for i, name := range fixtureNames {
+ fixtures[i] = loadFixtureBytes(t, name)
+ }
- // Warm up: replay trials 0..1132 exactly as
- // wasm_trap_containment.rs's fuzz loop does (fixture pick, mutate,
- // doc_new, apply), ignoring the outcome of each — most are clean
- // oks or clean rejections, and this loop must not go through
- // Handle.Apply (which would auto-rebuild on any genuine trap here
- // and undo the accumulated memory growth this test depends on).
- r := &rng{x: 0xC0FFEE}
- for i := 0; i < 1133; i++ {
+ // Warm up: replay trials 0..1132 exactly as
+ // wasm_trap_containment.rs's fuzz loop does (fixture pick, mutate,
+ // doc_new, apply), ignoring the outcome of each — most are clean
+ // oks or clean rejections, and this loop must not go through
+ // Handle.Apply (which would auto-rebuild on any genuine trap here
+ // and undo the accumulated memory growth this test depends on).
+ r := &rng{x: 0xC0FFEE}
+ for i := 0; i < 1133; i++ {
+ fi := r.nextUsize(len(fixtures))
+ mutated := mutate(r, fixtures[fi])
+ docHandle, err := createDoc(ctx, rt.callTimeout, mod, fmt.Sprintf("warmup-%d", i), uint64(2_000_000+i))
+ if err != nil {
+ t.Fatalf("warm-up trial %d: doc_new failed unexpectedly: %v", i, err)
+ }
+ _ = applyOn(ctx, rt.callTimeout, mod, docHandle, mutated) // outcome irrelevant; only the memory growth matters
+ }
+
+ // Trial 1133 itself: the exact reproducer documented in
+ // core/tests/wasm_trap_containment.rs ("a 3-byte-flipped `concurrent`
+ // fixture ... ran for 163 seconds of real CPU time"). Confirm the RNG
+ // sequence still lands on the documented bytes before relying on them.
fi := r.nextUsize(len(fixtures))
mutated := mutate(r, fixtures[fi])
- docHandle, err := createDoc(ctx, rt.callTimeout, mod, fmt.Sprintf("warmup-%d", i), uint64(2_000_000+i))
- if err != nil {
- t.Fatalf("warm-up trial %d: doc_new failed unexpectedly: %v", i, err)
+ const wantHex = "02010200040107636f6e6f656e7403424293010100040107636f6e74656e74034141418a"
+ if got := hex.EncodeToString(mutated); got != wantHex {
+ t.Fatalf("RNG/mutate drifted from core/tests/wasm_trap_containment.rs: trial 1133 produced "+
+ "fixture=%s bytes=%s, want the documented bytes=%s — this test no longer exercises the "+
+ "known-slow reproducer", fixtureNames[fi], got, wantHex)
+ }
+ if fixtureNames[fi] != "concurrent" {
+ t.Fatalf("expected trial 1133 to mutate the concurrent fixture, got %s", fixtureNames[fi])
}
- _ = applyOn(ctx, rt.callTimeout, mod, docHandle, mutated) // outcome irrelevant; only the memory growth matters
- }
- // Trial 1133 itself: the exact reproducer documented in
- // core/tests/wasm_trap_containment.rs ("a 3-byte-flipped `concurrent`
- // fixture ... ran for 163 seconds of real CPU time"). Confirm the RNG
- // sequence still lands on the documented bytes before relying on them.
- fi := r.nextUsize(len(fixtures))
- mutated := mutate(r, fixtures[fi])
- const wantHex = "02010200040107636f6e6f656e7403424293010100040107636f6e74656e74034141418a"
- if got := hex.EncodeToString(mutated); got != wantHex {
- t.Fatalf("RNG/mutate drifted from core/tests/wasm_trap_containment.rs: trial 1133 produced "+
- "fixture=%s bytes=%s, want the documented bytes=%s — this test no longer exercises the "+
- "known-slow reproducer", fixtureNames[fi], got, wantHex)
- }
- if fixtureNames[fi] != "concurrent" {
- t.Fatalf("expected trial 1133 to mutate the concurrent fixture, got %s", fixtureNames[fi])
- }
+ docHandle, err := createDoc(ctx, rt.callTimeout, mod, "trial-1133", 2_000_000+1133)
+ if err != nil {
+ t.Fatalf("creating the document under test: %v", err)
+ }
- docHandle, err := createDoc(ctx, rt.callTimeout, mod, "trial-1133", 2_000_000+1133)
- if err != nil {
- t.Fatalf("creating the document under test: %v", err)
+ return mod, docHandle, mutated
}
+ mod, docHandle, mutated := warm()
// Sanity: confirm this input is genuinely slow (not just a fast trap)
// under wazero's compiler engine at this memory-growth state, with a
@@ -375,31 +383,21 @@ func TestSlowUpdateIsCutOffByGuestCallTimeout(t *testing.T) {
"it returned in %v instead — the reproducer may have stopped reproducing, which would make "+
"the capped assertion below meaningless", sanityElapsed)
}
- t.Logf("uncapped: pathological update ran %v before failing on its own (%v)", sanityElapsed, sanityErr)
-
- // The actual assertion: a fresh document on the same (already grown)
- // instance, wired through the exported Handle so this exercises the
- // real Apply()/rebuild() path, called with a plain context carrying no
- // deadline of its own — so what cuts this call off, if anything does,
- // is exactly rt.callTimeout (the default, 1s), not a contrived
- // caller-side budget. That is the point fix-round-1 finding 2 raised:
- // the previous 5s default was close enough to this input's ~4.34s
- // natural running time that the cap wasn't reliably doing anything for
- // its own motivating case. 1s should cut it off at roughly a quarter
- // of that.
- docHandle2, err := createDoc(ctx, rt.callTimeout, mod, "trial-1133b", 2_000_000+11330)
- if err != nil {
- t.Fatalf("creating the second document under test: %v", err)
- }
- handle := &Handle{rt: rt, guid: "trial-1133b", clientID: 2_000_000 + 11330, mod: mod, handle: docHandle2}
+ t.Logf("sanity probe: pathological update failed or reached its 20s bound after %v (%v)", sanityElapsed, sanityErr)
+ _ = mod.Close(ctx)
+
+ // Repeat the exact warm-up independently: a timed-out sanity probe closes
+ // its module, and a fresh but unwarmed module would lose the reproducer.
+ mod, docHandle, mutated = warm()
+ handle := &Handle{rt: rt, guid: "trial-1133", clientID: 2_000_000 + 1133, mod: mod, handle: docHandle}
oldMod := handle.mod
start := time.Now()
applyErr := handle.Apply(ctx, mutated)
elapsed := time.Since(start)
- if applyErr == nil {
- t.Fatalf("expected the capped Apply to fail (it should have been cut off before completing), got nil error after %v", elapsed)
+ if !errors.Is(applyErr, context.DeadlineExceeded) {
+ t.Fatalf("expected the capped Apply to reach its deadline, got %v after %v", applyErr, elapsed)
}
// Generous upper bound: the call must not have been allowed to run
// anywhere near its natural ~4.34s completion time.
diff --git a/internal/core/testdata/docs_core.wasm b/internal/core/testdata/docs_core.wasm
index c03208f..a950278 100755
Binary files a/internal/core/testdata/docs_core.wasm and b/internal/core/testdata/docs_core.wasm differ
diff --git a/internal/docs/app.go b/internal/docs/app.go
index 01e27e3..ccfb4e0 100644
--- a/internal/docs/app.go
+++ b/internal/docs/app.go
@@ -23,6 +23,7 @@ import (
"amadan.net/oficina/home/handoff"
nativeauth "amadan.net/rastrillo/native/server"
"github.com/carlosframework/docs/internal/docs/linkstore"
+ "github.com/carlosframework/rastrillo"
"github.com/carlosframework/rastrillo/db"
"github.com/carlosframework/rastrillo/mail"
"github.com/carlosframework/rastrillo/migrate"
@@ -492,7 +493,17 @@ func New(cfg Config) (*App, error) {
if home == nil {
return authPlugin.RequireSession(next)
}
- return home.bridge.sessions.Require(home.bridge.resolver.middleware("/signin/local")(next))
+ current := home.bridge.sessions.Require(home.bridge.resolver.middleware("/signin/local")(next))
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // A live stream retains its opening context. Require trusts that cached
+ // session, so re-read the cookie before admitting each stream write.
+ session, ok := home.bridge.sessions.From(r)
+ if !ok {
+ http.Error(w, rastrillo.T(r, "docs.error.generic"), http.StatusUnauthorized)
+ return
+ }
+ current.ServeHTTP(w, sessions.WithSession(r, session))
+ })
}
nativeSessions, err := sessions.New(sessions.Config{DB: d.Writer(), Origin: cfg.Origin, SigninPath: "/signin", Logger: logger})
if err != nil {
diff --git a/internal/docs/native_stream_test.go b/internal/docs/native_stream_test.go
index f760928..d65e355 100644
--- a/internal/docs/native_stream_test.go
+++ b/internal/docs/native_stream_test.go
@@ -14,7 +14,20 @@ func TestNativeStreamStopsAfterSessionRevocation(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- owner := app.TestPerson(t, "stream@example.com")
+ authenticated := app.TestRequest(t, "stream@example.com", "GET", "/", nil)
+ assertNativeStreamRevoked(t, app, "stream@example.com", authenticated)
+}
+
+func TestNativeHomeStreamStopsAfterSessionRevocation(t *testing.T) {
+ f := newRouterHomeFixture(t)
+ browser := f.browser()
+ browser.localSignin(t, "alice@example.com")
+ assertNativeStreamRevoked(t, f.app, "alice@example.com", browser.request("GET", "/", ""))
+}
+
+func assertNativeStreamRevoked(t *testing.T, app *App, address string, authenticated *http.Request) {
+ t.Helper()
+ owner := app.TestPerson(t, address)
doc, err := app.Store().Create(t.Context(), owner, "Live fixture")
if err != nil {
t.Fatal(err)
@@ -22,26 +35,36 @@ func TestNativeStreamStopsAfterSessionRevocation(t *testing.T) {
streamDurations(t, app, 20*time.Millisecond, time.Second)
server := httptest.NewServer(app.Handler())
defer server.Close()
- authenticated := app.TestRequest(t, "stream@example.com", "GET", "/", nil)
req, _ := http.NewRequest("GET", server.URL+"/d/"+doc.Sqid+"/stream", nil)
+ req.Host = authenticated.Host
for _, cookie := range authenticated.Cookies() {
req.AddCookie(cookie)
}
- response, err := server.Client().Do(req)
+ client := server.Client()
+ client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
+ response, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
+ if response.StatusCode != http.StatusOK || response.Header.Get("Content-Type") != "text/event-stream" {
+ t.Fatalf("stream did not open: status=%d location=%q", response.StatusCode, response.Header.Get("Location"))
+ }
reader := bufio.NewReader(response.Body)
if name, _ := nextEvent(t, reader, time.Second); name != "update" {
t.Fatalf("no initial event: %s", name)
}
signout := httptest.NewRequest("POST", "/signout", nil)
+ signout.Host = authenticated.Host
signout.Header.Set("Sec-Fetch-Site", "same-origin")
for _, cookie := range authenticated.Cookies() {
signout.AddCookie(cookie)
}
- app.Handler().ServeHTTP(httptest.NewRecorder(), signout)
+ signedOut := httptest.NewRecorder()
+ app.Handler().ServeHTTP(signedOut, signout)
+ if signedOut.Code != http.StatusSeeOther {
+ t.Fatalf("signout failed: %d", signedOut.Code)
+ }
closed := make(chan error, 1)
go func() { _, err := io.Copy(io.Discard, reader); closed <- err }()
select {
diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh
index 522f045..f3a6c56 100755
--- a/scripts/build-wasm.sh
+++ b/scripts/build-wasm.sh
@@ -36,7 +36,9 @@ cargo clippy --manifest-path core/Cargo.toml --all-targets -- -D warnings -D cli
# it has to be repeated here alongside the remap flags.
registry_root="${CARGO_HOME:-$HOME/.cargo}/registry/src"
workspace_root="$(pwd)"
-export RUSTFLAGS="--cfg getrandom_backend=\"custom\" --remap-path-prefix=${registry_root}=/cargo-registry-src --remap-path-prefix=${workspace_root}=/docs-core"
+# Cargo also puts local path dependency identities into mangled debug names.
+# Remove that optional symbol section so identical code reproduces across worktrees.
+export RUSTFLAGS="-C strip=symbols --cfg getrandom_backend=\"custom\" --remap-path-prefix=${registry_root}=/cargo-registry-src --remap-path-prefix=${workspace_root}=/docs-core"
cargo build --release --manifest-path core/Cargo.toml --target wasm32-unknown-unknown --locked --lib
out="core/target/wasm32-unknown-unknown/release/docs_core.wasm"
[ -f "$out" ] || { echo "build-wasm: no artifact at $out" >&2; exit 1; }