#2894·gstack

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

Author: getabsolutehealthCreated Sep 17, 2026Updated Sep 17, 2026

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.