Skip to content

fix(session-pid): bind the signed pid mapping to the process incarnation - #8467

Merged
bolichen97 merged 1 commit into
mainfrom
fix/session-pid-sig-start-token-8343
Sep 4, 2026
Merged

fix(session-pid): bind the signed pid mapping to the process incarnation#8467
bolichen97 merged 1 commit into
mainfrom
fix/session-pid-sig-start-token-8343

Conversation

@dwu96

@dwu96 dwu96 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Symptom

session_pid_<pid>.txt maps a pid number to a session key, and its HMAC sidecar signs only that number plus the key. Nothing in the file or the signature identifies the process incarnation that owned the pid — so once the OS recycles the pid, the mapping still verifies and answers for the new process with the previous owner's session key. The orphan sweep runs only at gateway start/shutdown, so a mid-run recycle stays misattributed until the next restart.

Scope, stated honestly: the mapping directory is same-uid, and the recycled process runs as the same user — this is a robustness/misattribution defect, not a privilege boundary. Fixing it prevents wrong-session attribution (state-mutating MCP tools, audit attribution), not an escalation.

Root cause

_compute_sig MAC'd exactly two fields ("<pid>:<session_key>"), and the .txt carried only the key — the pid number was the sole process identity anywhere in the contract (src/kiro_crew/session_pid_sig.py).

Fix

Bind the mapping to the process incarnation, following the in-tree precedent: session_pid.py's <gw>:<pid>:<start_token> dual-parse recycle guard (its sweep records the start token at spawn, dual-parses legacy vs guarded entries, treats a mismatch as "this PID now belongs to a different process", and treats an unreadable token as unknown — never a mismatch).

  • Publish (publish_session_pid): capture the process start token via platform_compat.get_process_start_id and append it as a second line of the .txt. A second line rather than a colon field — unlike the precedent's integer fields, the session key itself contains colons (dashboard:chat-7-…), so a colon split could not tell a legacy key from a key+token pair. Token source note: get_process_start_id is the single shared implementation (already consumed by apps/backend.py, instances/ssh_tunnel_manager.py, metrics/local_exporter.py); session_pid._pid_start_token is itself a one-line delegate to it, and importing session_pid from session_pid_sig would drag providers.base into every sandboxed resolver process, so this module calls the shared platform_compat implementation directly instead of the private delegate.
  • Sign: the MAC now covers the full published body ("<pid>:<body>"). A legacy body produces a byte-identical message to the old scheme, so every signed mapping written before this change still verifies — no migration, no tamper false-positive. Flipping only the token invalidates the MAC.
  • Read (both verify_session_pid and read_session_pid_txt): dual-parse legacy vs guarded forms. A recorded token that is readable live and different is positive evidence of a recycled pid → refuse. An absent recorded token (legacy file) or an unreadable live token (Windows, exited process) is unknown → resolve exactly as today. That asymmetry is the correctness argument and is written as comments at the guard.
  • Lenient reader refuses proven mismatches too, deliberately: call sites fall back from the strict resolver to the lenient reader (peer_resolve.py:103-105), so a mismatch surfaced only from the strict path would be silently recovered by the fallback and the stale attribution kept.
  • Unsigned-publish degrade preserved: SEL key unavailable → token-bearing .txt still published, stale sidecar removed, strict resolvers fail closed (existing + new tests).
  • Same-commit spec-doc update: docs/system-specs/modules/session.md sidecar-contract section (MAC coverage + recycle-guard bullet).

Out of scope, per the issue triage: making the orphan sweep periodic is a separate change with its own scheduling surface (see "Pattern harvest" below).

Verification

  • Red before green: the 5 behavior-changing tests fail against unmodified main (dabd83e91) — headline test_recycled_pid_refused_by_strict_resolver and test_recycled_pid_refused_by_lenient_reader both returned the old owner's key pre-fix. The 4 compatibility guards (legacy file, unknown-token, same-incarnation, sig-coverage) pass on both sides by design. Incarnation is simulated by controlling the token source, never by real pid recycling.
  • Mutation checks (each mutant applied via cp-aside/cp-back, run, restored, green re-confirmed):
    1. Token dropped from the MAC on both sides → killed by test_signature_covers_the_token (1 failed / 46 passed). Verify-side-only variant also run → killed by the round-trip tests.
    2. Absent token treated as mismatch → killed by test_legacy_tokenless_file_still_resolves (+2 existing lenient tests).
    3. Mismatch treated as unknown → killed by both recycle headline tests.
    4. Recycle check skipped in the lenient reader only → killed by test_recycled_pid_refused_by_lenient_reader while the strict headline stayed green (clean isolation).
  • Existing module: 38 pre-existing tests unchanged and green (the shared fixture pins the token probe to None so live host pids can't perturb exact-content assertions); 47/47 total.
  • Consumer modules (test_dashboard_peer_auth, test_identity_topology, test_mcp_core_audit_e, test_mcp_core_set_project, test_mcp_cron_caller_identity, test_resolve_session_key, test_sel): green except test_dashboard_peer_auth's 4 failures + 2 errors ("AF_UNIX path too long"), reproduced byte-identically on a pristine origin/main worktree — environmental.
  • Zero-regression proof: full backend suite on the branch vs a git worktree at origin/main, sorted failing-id sets diffed both directions — see the checked line below.
  • flake8 and mypy clean on touched files; black gate passes (both files baselined).

Zero-regression: branch 99 failed, 80092 passed, 2 errors vs origin/main worktree 99 failed, 80083 passed, 2 errors (pass-count delta = the 9 new tests); the sorted failing-id sets (101 ids each) are byte-identical both directions after normalizing one xdist log-interleave artifact (a Superseded default in stored config: diagnostic glued onto a summary line — present once in each log, on different tests, both of which failed in both runs). This repo's failing baseline is environmental; set identity is the proof, not "0 failures".

Pattern harvest

Rule candidate: a persisted pid-keyed record is only as valid as the pid number is stable — any pid map that outlives its process must record the incarnation (start token) and its signature must cover it, with mismatch=refuse / unknown=proceed asymmetry. Knowingly out-of-scope sibling: the orphan sweep still runs only at gateway start/shutdown, so a recycled mapping's files linger until then (the readers now refuse them, which removes the misattribution); attaching the sweep to a recurring task is a separate scheduling-surface change — sibling issue filed and linked in the comments.

Backend-only change (no UI surface); evidence is the red-first test set and mutation kills above — a still frame cannot show a resolver refusal.

Fixes #8343

The session_pid_<pid>.txt mapping and its HMAC sidecar bound only the pid
NUMBER, so a recycled pid kept verifying and answered for the new process
with the previous owner's session key until the next restart's orphan
sweep (issue #8343).

Publication now records the process start token
(platform_compat.get_process_start_id — the same incarnation identity
session_pid.py records in its <gw>:<pid>:<start_token> sweep entries) as a
second line of the .txt, the MAC covers the full published body, and both
readers refuse on a proven token mismatch. Absent (legacy) or unreadable
(Windows) tokens stay unknown, never a mismatch; the unsigned-publish
degrade path is preserved.

Fixes #8343
@dwu96
dwu96 requested a review from a team as a code owner September 4, 2026 14:26
@dwu96
dwu96 requested a review from CrysisDeu September 4, 2026 14:26
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of 65e3de45ab9f8a31c047ec6ce7737376ecb6751f — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All key claims verified. The get_process_start_id helper is the shared implementation (consumers in gatewayd.py, claim.py, metrics/sessions.py, session_pid.py), session_pid.py:115 is indeed a thin delegate, the lenient reader at peer_resolve.py:105 is a real resolution path callers reach with signed_only=False, and sibling pid-keyed records (session_pid.py sweep entries, mcp_gateway/claim.py pid_start_id, metrics/sessions.py live-token comparison) already carry the incarnation guard — this PR fixes the one remaining unguarded record. Here is the review:

First-Principles-Verdict: PASS

A real misattribution defect (#8343) fixed at its cause — the mapping's identity contract — reusing the existing shared incarnation helper, with zero new public surface.

What this change ships

Intent: stop a recycled OS pid from resolving to the previous owner's session key. FIX.

  1. Strict resolver refuses a mapping whose pid was recycled — justified (the reported defect)
  2. Lenient reader refuses proven recycles too — justified (callers reach it via signed_only=False, peer_resolve.py:105)
  3. .txt gains a second line carrying the process start token — justified mechanism
  4. MAC now covers the full body; legacy bodies verify byte-identically — justified, no migration
  5. Absent/unreadable token resolves as before — justified compatibility asymmetry
  6. Malformed 3+-line body now refused on both paths — undeclared corollary of the parse; unreachable via any publisher
  7. Publish degrades to legacy form for a \n-bearing session key — undeclared; canonical-encoding-before-MAC hygiene, derived
  8. Spec doc + module docstring updated same-commit — mandated by AGENTS.md

Lens-4/6 counts: incarnation identity already exists as platform_compat.get_process_start_id (5 consuming modules grepped) and the PR uses it directly rather than adding a second spelling; of 4 in-tree pid-keyed persisted records, 3 were already incarnation-guarded (session_pid.py:115, mcp_gateway/claim.py:83, metrics/sessions.py:540) — this fixes the last, so zero unfixed siblings remain beyond the sweep-timing change the author filed and deferred. No new config key, flag, or exported symbol; both private helpers have 2 in-module consumers each. Description and diff tell the same story throughout.

[FIRST-PRINCIPLES-REVIEWED] 65e3de4

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of 65e3de45ab9f8a31c047ec6ce7737376ecb6751f — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

Binds the mapping to the process incarnation — the actual root cause of #8343 — with byte-compatible legacy MACs and fail-closed-only-on-proof asymmetry.

Watch

get_process_start_id returns None on Windows, so publication degrades to the legacy form and the recycle guard never fires there — the platform where pid reuse is most aggressive keeps #8343's misattribution window ("An unreadable token (Windows, probe failure) degrades to the legacy single-line form"). Safe degrade to status quo and disclosed, but a Windows start-token implementation in platform_compat is the follow-up that finishes the fix.

[DESIGN-REVIEWED] 65e3de4

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 65e3de45ab9f8a31c047ec6ce7737376ecb6751f and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 65e3de4

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 65e3de45ab9f8a31c047ec6ce7737376ecb6751f: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 65e3de45ab9f8a31c047ec6ce7737376ecb6751f — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 65e3de4

Verdict parsed from the review's SHA-scoped output markers for commit 65e3de45ab9f8a31c047ec6ce7737376ecb6751f.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 65e3de45ab9f8a31c047ec6ce7737376ecb6751f: <one-sentence reason>

@dwu96

dwu96 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Advisory disposition — Design Watch item (head 65e3de4)

All four completed lanes PASS on this head (GPT 5.6 no findings, Opus 4.8 no findings, First Principles PASS, Design PASS). Design's PASS carried one Watch item; dispositioned here so nothing advisory is left unanswered:

Watch: get_process_start_id returns None on Windows, so the recycle guard never fires there — a Windows start-token implementation is the follow-up that finishes the fix.

ADOPTED as a tracked follow-up → #8473. Verified before filing: the Windows gap is real (the docstring's Windows arm returns None by design), and open PR #8039 does not close it (its platform_compat addition, live_thread_group_leaders, is Linux-/proc-only). Not folded into this PR deliberately: implementing a Windows process-times probe is a new platform_compat capability with its own ctypes/verification surface, and every consumer of the identity (this mapping, the session_pid.py sweep entries, mcp_gateway/claim.py, metrics/sessions.py) inherits it with zero changes once it lands — the dual-parse and unknown≠mismatch asymmetry shipped here are exactly the seam it plugs into. The degrade on Windows is the pre-existing status quo, disclosed in the module docstring and session.md.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 4, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Binds the pid mapping to the process incarnation the right way: the start token rides a second LINE (correct choice — session keys like dashboard:chat-7-… contain colons, so the sibling record's colon-field scheme could not disambiguate legacy from guarded), and the MAC now covers the whole .txt body so the token is signed while a legacy tokenless body produces a byte-identical message and keeps verifying without migration. The mismatch=refuse / absent-or-unreadable=unknown asymmetry is the load-bearing part and is right on both readers — refusing on the lenient read_session_pid_txt path too is necessary since peer_resolve falls back to it, which would otherwise silently restore the stale attribution. get_process_start_id swallows all exceptions and returns None, so the unsigned/Windows degrade paths cannot break publication, and the recycle check correctly runs AFTER the MAC so an unauthenticated token is never trusted.

@bolichen97
bolichen97 merged commit 7248a6b into main Sep 4, 2026
63 checks passed
@bolichen97
bolichen97 deleted the fix/session-pid-sig-start-token-8343 branch September 4, 2026 17:57
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 4, 2026
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.

The pid to session mapping and its HMAC bind only the pid number, so a recycled pid resolves to the previous owner's session key until the next restart

2 participants