Skip to content

Hardening: crash-atomicity + lock findings from the 0.24.3 adversarial review #4

Description

@youdie006

From a cross-model adversarial review (GPT-5.6 code pass + ChatGPT Pro invariant pass) of the switch/keychain/store surface, six findings were fixed in 0.24.3. The remaining findings are all crash-window / non-transactional-write hardening — real but NARROW (each needs a crash or I/O failure inside a small window), and they share the same class as every file-swap switcher. Tracking them here rather than force-fitting a fix.

Crash-window / transactionality

  • Multi-resource apply is not crash-atomic (src/adapters/claude.rs apply): a SIGKILL/power-loss between the credential-file write, the Keychain write, and the .claude.json write leaves the machine with A's token and B's identity. No durable journal to finish/rollback on next start. Fix direction: write+fsync a WAL (prior bytes + exact Keychain service) before the first mutation; recover on startup.
  • Multi-blob profile save is not transactional (src/store.rs:save): a crash between the credentials blob and the oauth_account blob leaves a profile with B's token and A's identity (each blob is valid JSON, so use accepts the mismatch). Fix direction: write a full generation to a staging dir, fsync, atomically flip a pointer/manifest.
  • Rollback leaves a newly-created Keychain item (src/adapters/claude.rs apply rollback): when no prior Keychain token was read, the None => true branch leaves A in the Keychain while the config is rolled back to B. Fix direction: tri-state prior (absent / read-failed / present); on rollback of a created item, delete that exact item; abort before writing if the prior read failed.

restore / lock

  • A failed restore strands the requested backup (src/commands.rs restore): restore loads backup A, then backs up the live B (making B newest), then apply(A) fails — a retry now sees B as newest and exits "already active", never retrying A. Fix direction: back up B provisionally, promote only after A applies.
  • Interactive login re-acquires the store lock best-effort (src/commands.rs login, the store.lock().ok() after the interactive sign-in): a concurrent swapdex use during the sign-in can interleave; on Busy at re-lock, login proceeds unlocked. Fix direction: a per-tool live-credential lock (distinct from the store lock, which must stay released across the interactive wait) held across sign-in; never best-effort at the end.

All are NARROW (crash/concurrency-timing preconditions). Prioritize the two write-atomicity ones (apply WAL, save staging) if any are done — they are the ones that can produce a token/identity mismatch a later use would silently apply.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions