Skip to content

/cso on Windows: a dead-owner mutation lease is never reclaimed, bricking every later run #2894

Closed

Description

@getabsolutehealth

Summary

On Windows, gstack-cso never reclaims a mutation lease whose owner process is no longer live. Because every launcher invocation is its own short-lived process, each operation leaves a dead-owner lease that fails the next call — including a brand-new start in the same repo scope. Net effect is roughly one lease-taking operation per run directory, so a multi-step assessment cannot complete.

Version: gstack v1.87.4.0. Windows 11, Git Bash (MINGW64), freshly built helper (VS 2022 Build Tools 17.14.41).

Symptom

{"ok":false,"error":{"code":"UNSAFE_PATH","message":"Run mutation lease decision is malformed"}}

Sometimes the first failure instead reports:

{"ok":false,"error":{"code":"PERSISTENCE_FAILED","message":"Run mutation lease decision changed before cleanup"}}

Both inspect, read, history, finish, resume, and start return this once a stale lease exists. doctor keeps working, which fits — it takes no lease.

Reproduction

gstack-cso start  --repo <repo>     # ok, creates run
gstack-cso inspect <run>            # ok
gstack-cso read    <run> some.file  # ok
gstack-cso read    <run> other.file # UNSAFE_PATH — lease malformed
gstack-cso start  --repo <repo>     # also UNSAFE_PATH: the whole repo scope is now stuck

Workaround (discards the run, so it does not enable a real multi-phase audit): confirm no live gstack-cso process, then rm -rf ~/.gstack/security/cso/<repoId>/<runId>. The next start succeeds immediately.

Evidence that it is liveness, not corruption

The .decision file is well-formed 265-byte JSON and carries an ownerCreatedAt that would disambiguate a recycled PID:

{"schemaVersion":1,"token":"…","kind":"ticket","ticket":"0000000000000001",
 "candidateDev":"…","candidateIno":"…","ownerPid":84108,
 "ownerCreatedAt":1789645916568,"publisherPid":84108,"createdAt":1789645916592}

Observed in both states, same error each time:

ownerPid state at failure
56436 recycled — PID now belongs to an unrelated conhost
84108 plain dead — no process with that PID

So it is not specifically PID reuse. Any non-live owner fails, and the stored ownerCreatedAt appears not to be consulted. Windows recycles PIDs aggressively, which makes the recycled case common there, but the dead case alone is enough to reproduce.

The error message is also misleading: "malformed" points at parse/corruption and sends you looking at the file, when the file parses fine and the real condition is an unreclaimed lease.

Impact

/cso cannot complete an assessment on Windows. It is not loudly broken though, which is the part worth flagging: a run finished early still reports

No supported findings in the assessed scope.

while every domain in the same report reads not assessed. To a skimming reader that is indistinguishable from a clean security review. In our case the weekly /cso had been a no-op for a week before anyone noticed.

Suggested fix

Treat a lease as reclaimable when the owner is not live, using the ownerCreatedAt already stored to reject a recycled PID rather than trusting the PID alone. Separately, a distinct error code for "stale lease reclaimed/blocked" would stop this reading as file corruption.

Related, lower priority

./setup correctly refuses to keep a CSO helper it cannot rebuild and deletes it — good security posture — but on a machine without the MSVC toolchain that silently converts a working /cso into a fail-closed one on the next upgrade. The summary line does say CSO unavailable: ... ($CSO_FAIL_REASON), but a previously-working install going dark is worth a louder signal.

Activity

  1. nattapol765-code commented on Sep 18, 2026

    @nattapol765-code

    Also hit this on Windows, gstack v1.87.4.0 — same symptoms, same workaround. One additional data point: the stale lease blocked start across three separate local clones that share one GitHub remote (different working directories, same repo). That suggests repoId may key off the remote URL rather than the local path, which could matter for the fix if it only checks PID liveness per-lease without considering that many clones can map to one repoId.

  2. capy-ai commented on Sep 23, 2026

    @capy-ai
    Contributor

    Thank you @getabsolutehealth for the Windows lease failure report and @nattapol765-code for confirming that the symptom can block clones sharing the same repository identity.

    #2942 landed a relevant underlying fix in b9706f3, v1.88.1.0: NTFS device/inode identities are no longer rounded through JavaScript numbers. This repairs the demonstrated malformed-decision/changed-before-cleanup path identified precisely in #2927. The exact-identity tests include recovery of a genuinely dead owner, and the compiled-helper NTFS lifecycle passed on native Windows.

    Keeping this broader issue open. Dead-owner recovery already existed; the demonstrated repair is exact filesystem identity, not a new Windows process-creation-time check. It does not establish safe recovery when a PID has been recycled, and ambiguous legacy records are still refused. The report's separate diagnostic/report-wording and toolchain-upgrade concerns were not addressed by this integration either. Thanks for the evidence; those remaining items should not be marked fixed just because the NTFS numeric defect is now covered.

  3. cawa1972 commented on Sep 23, 2026

    @cawa1972

    Data point after v1.88.1.0 (b9706f3) on Windows 11 Pro 10.0.26200 / NTFS, Git Bash:
    the helper was rebuilt by ./setup from 1.88.1.0 sources (gstack-cso 3.0.0 (helper ABI 3)), doctor reports static-snapshot: ready,
    and the first start now fails before any run is created:

    {"ok":false,"error":{"code":"UNSAFE_PATH","message":"Run mutation lease decision does not match its candidate owner"}}

    The repo's state namespace under ~/.gstack/security/cso/<repoId>/ is empty before and after (the 1.87.x run that hit #2927 had been moved
    out of the state root). One other namespace exists on this machine for a different repo (different remote), holding a run created by 1.87.4.0
    with its .mutation-lock-leases/*.decision. We did not delete it (per your note that legacy records stay blocked). If start consults leases
    outside its own repo scope, that record may be what it trips on; otherwise this looks like the owner/creation-time check on Windows.
    We made exactly one start attempt, per the skill's one-run invariant.

  4. capy-ai commented on Oct 3, 2026

    @capy-ai
    Contributor

    Fixed in v1.91.16.0 (#3014, ac20ef1): Windows and macOS leases now carry a process start-time identity, reclaim is atomic, and one unreadable lease left by an older gstack no longer blocks start or other runs. The native Windows lifecycle passes on the cso-windows-launcher CI job.

    Thanks for the report! If it still happens on v1.91.16.0 or later, reply here and we'll reopen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions