An encrypted personal cloud built on the AT Protocol.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

DPoP nonce race condition in WASM bridge #4

open opened by sans-self.org

Summary#

Concurrent WASM PDS calls receive the same session snapshot. Each call independently mutates the DPoP nonce. Only one returned session gets persisted — the other's nonce is silently lost. Subsequent PDS requests may be rejected with an invalid nonce error.

Reproduction#

loadCabinet in web/src/stores/documents/store.ts fires two PDS calls in parallel:

const [freshDirs, freshGrants] = await Promise.all([
  worker.listDirectoriesRaw(pdsUrl, session),
  worker.listGrantsRaw(pdsUrl, session),
]);
await persistSession(did, freshGrants.session);

Both calls start with the same session object (one loadSession call). Each WASM call creates an ephemeral XrpcClient, makes HTTP requests that update the DPoP nonce, and returns an independently mutated session. Only freshGrants.session is persisted — the nonce from listDirectoriesRaw is discarded.

Impact#

  • The PDS may reject the next authenticated request if it expects the nonce from the discarded call
  • Manifests as sporadic "Invalid DPoP Nonce" errors, especially under load or on slow connections
  • The race window exists in every Promise.all site that fires multiple PDS calls with the same session

Affected call sites#

  • stores/documents/store.ts:217-220loadCabinet (listDirectoriesRaw + listGrantsRaw)
  • routes/cabinet/shared.tsx:185-195fetchGrants (listOutgoingGrants + listIncomingGrants)
  • stores/search.ts:68-101loadInbox (multiple concurrent chains)

Root cause#

Every WASM PDS export (crates/opake-wasm/src/) follows the same pattern:

  1. make_client(pds_url, session) — deserializes JS session, creates ephemeral XrpcClient
  2. Makes XRPC calls — DPoP nonce mutated in-place on the client's session
  3. result_with_session(&client, &payload) — clones the mutated session into the return value

Since each call gets its own XrpcClient with a copy of the session, concurrent calls diverge independently. There is no serialization, no mutex, and no merge logic.

Proposed fix#

A session gate (FIFO queue) in the Web Worker that serializes all PDS calls through a single session owner. Each call loads the current session, executes, and persists the returned session before the next call starts. Concurrent calls from the main thread queue up automatically.

Regression test#

web/tests/bugs/dpop-race.test.ts — exercises the exact Promise.all pattern and proves nonce loss occurs. 3 test cases, all passing (proving the bug exists).

sign up or login to add to the discussion
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:wydyrngmxbcsqdvhmd7whmye/sh.tangled.repo.issue/3mhjnsunbj422