Skip to content

fix(server): atomic session ownership claim - deny a lost claim instead of streaming in a foreign session (Eve2-F1) - #616

Merged
LinuxDevil merged 1 commit into
mainfrom
fix/eve2-f1-session-squat
Oct 12, 2026
Merged

LinuxDevil merged 1 commit into
mainfrom
fix/eve2-f1-session-squat

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

Finding (Eve2-F1, critical)

Session ownership was a check-then-act race. ownsSession() (src/server/fetchRoutes.ts) checked sessionOwner() + sessionStored() across awaits and then called claimSession() (src/server/requestGuard.ts), which silently no-opped when the id was already claimed; ownsSession then returned true unconditionally.

Two concurrent first-touch POST /chat calls for one unowned session both passed: the losing principal streamed a full turn inside a session owned by the other caller and received its transcript content (verified by audit/eve2/repro/f1-session-squat-toctou.ts on audit/eve2-findings: mallory's SSE carried alice's secret). claimOnCommit() had the same unguarded re-claim.

Fix

  • claimSession() now reports whether this caller holds the claim afterward: true when it just took (or already held) it, false when another principal owns it. The check-and-set is one synchronous step, so it is atomic.
  • ownsSession() returns the claim's answer, so a concurrent first touch that registered another owner first is refused with 403 LOUSHO_SESSION_FORBIDDEN — never streamed.
  • claimOnCommit() checks its re-claim: when the claim was evicted past the owner cap and taken by another principal while the turn ran, the turn's transcript is cleared (session.clear(), best-effort) and the stream ends on the same coded error instead of a clean done.
  • Covers every caller of the shared guard: POST /chat, the deprecated { message } body, GET /chat/:id, approvals, the POST <base>/ui useChat endpoint and serveFetch() (deployed node server / Worker). Channel routes (mountChannels/mountFetchChannels) do not use the session-owner guard — verified unaffected.

Test

Two new regression tests in src/server/requestGuard.test.ts (both failed before the fix):

  • concurrent first touch: two principals race on an unowned session (first transcript load delayed so both checks observe unowned/unstored) — asserts exactly one 200, a 403 LOUSHO_SESSION_FORBIDDEN for the loser, none of the loser's input in the winner's stream or the stored transcript, and a 403 on the loser's sequential retry.
  • claim lost mid-turn: with the owner cap at 1, an eviction + a foreign claim via the session-less { message } path while a gated turn is in flight — asserts the stream ends on the coded error and the stored transcript is cleared.

Existing guard tests (squatting, fail-closed stored sessions, bounded owner map, /ui) all still pass.

Gates

  • npx tsc --noEmit -p tsconfig.json ✅
  • npm run lint ✅
  • npx vitest run src/server ✅ 89 tests (7 files)
  • Repro re-run: malloryStatus: 403, mallorySeesAliceSecret: false, transcript holds only alice's turn → "no bug" (exit 1)
  • Public API unchanged (claimSession is internal, absent from api/*.api.md) — no api:update needed.

…nstead of streaming in a foreign session (Eve2-F1)

ownsSession() checked sessionOwner() and sessionStored() across awaits and
then called claimSession(), which silently no-opped when the id was already
claimed. Two concurrent first-touch POST /chat calls for one unowned session
both passed: the losing principal streamed a full turn inside the winner's
session and received its transcript.

claimSession() now reports whether the caller holds the claim afterward.
ownsSession() returns its answer, so a concurrent first touch that
registered another owner first is refused (403 LOUSHO_SESSION_FORBIDDEN).
claimOnCommit() checks its re-claim too: when the claim was forgotten past
the owner cap and taken by another principal while the turn ran, the turn's
transcript is cleared again and the stream ends on the same coded error.
@LinuxDevil
LinuxDevil merged commit d84eb2d into main Oct 12, 2026
7 checks passed
LinuxDevil added a commit that referenced this pull request Oct 12, 2026
…ata modes for the rest (#620)

- products-admin: new driver that points the coding-agent machinery (plan
  mode, approval-gated fs+shell, durable store, loop guard) at a real
  greenfield build — a products admin dashboard CRUD (JSON store, node:http
  REST, single-page dashboard, node --test suite). The harness boots the
  agent-built server and verifies the full CRUD over real HTTP.
- repo-maintainer: REPO_FIXTURE=<dir> reviews a real checkout + diff; ships
  real-fixture/ (the merged PR #616 diff + files at head). B5 adapts: a real
  diff has no planted bugs, so the check asserts a coherent typed review.
- research-analyst: CORPUS_DIR=<dir> researches real .md/.txt/.mdx docs
  (verified against ../docs). Cost check fixed: providers that bill per call
  (OpenRouter usage.cost applies cache discounts) can't bit-match a fresh
  registry estimate — re-estimate with cachedInputTokens and allow a 15%
  band; exact equality still required when nothing was billed.
- incident-responder: INCIDENT_TELEMETRY=<file.json> replaces canned
  telemetry; real-telemetry.json is built from actual run logs + git history.
- REPORT.md: real-task round results, including the honest finding that
  gpt-4o-mini cannot finish open-ended builds (use AUDIT_MODEL override).

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant