fix(cli): recover stale config locks#78
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f2c95a44a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| try { | ||
| await writeFile(path.join(lockDir, CONFIG_LOCK_OWNER_FILE), JSON.stringify(owner), { mode: 0o600, flag: "wx" }); | ||
| } catch (error) { | ||
| await rm(lockDir, { recursive: true, force: true }).catch(() => undefined); |
There was a problem hiding this comment.
Guard owner-write cleanup against successor locks
If this process is paused after mkdir(lockDir) but before owner.json is written for longer than the legacy grace period, another contender can classify the metadata-less directory as stale, remove it, and create a successor lock. When this original process resumes, writeFile(..., flag: "wx") can fail with EEXIST against the successor's owner file, and this cleanup removes the successor lock unconditionally, breaking mutual exclusion and allowing overlapping config writes. The cleanup needs to verify it is still deleting the directory it created before removing it.
Useful? React with 👍 / 👎.
| const claimToken = randomUUID(); | ||
| try { | ||
| await writeFile(claimPath, claimToken, { mode: 0o600, flag: "wx" }); | ||
| } catch (error) { |
There was a problem hiding this comment.
Expire abandoned reclamation claims
When a process is killed after creating .config.lock.reclaim-* but before the finally releases it, every later attempt to reclaim the same stale lock hits this deterministic claim path and returns false forever, so the dead config lock times out instead of recovering automatically. Since the change is specifically handling hard-killed writers, the claim file needs its own stale-owner handling or the stale lock remains unreclaimable after a killed reclaimer.
Useful? React with 👍 / 👎.
3f2c95a to
74fb19b
Compare
Review follow-upAddressed the abandoned reclamation-claim finding in commit
Verification: targeted config tests, full test suite, typecheck, lint, build, pack verification, clean-pack smoke test, and production dependency audit all pass. |
|
Opus review — needs-rework The lock protocol itself is carefully done (kernel-guarded reclaim serialization, token-checked release, re-inspect under guard closes the TOCTOU, strong regression matrix including the SIGKILL'd guard-holder test), and CI is green. Rejecting on platform coverage and proportionality, not on a correctness bug: 1. No automatic recovery on macOS/Windows — the filed failure stays permanent on primary platforms (blocking)
macOS is one of the two supported wallet/signing platforms. Issue #72's scenario — crash mid-write → every later A safe dead-check that covers both platforms needs no native code: same-hostname + 2. A native runtime dependency + ~330-line bespoke protocol is disproportionate for
The race the guard closes (two contenders observing the same dead owner; reclaim-vs-release) can be closed dependency-free with an atomic claim: Non-blocking notes
Rework direction: keep the v2 owner metadata, token-checked release, and the test matrix (un-skipped on macOS); replace kernel-flock + Linux-only identity with hostname + ESRCH liveness (optionally retaining the Linux start-ticks refinement) and an atomic rename-claim for deletion; drop |
GPT review — changes requestedReviewed head
The two older Codex findings are addressed at this head: ownerless locks are no longer auto-reclaimed, and the fd-scoped operation guard no longer leaves a permanent claim when a reclaimer dies. CI, CodeQL, build, lint, packaging checks, and existing tests pass. |
Replace the kernel-flock guard and Linux-only process-identity matrix with a dependency-free protocol that recovers on every supported platform: - owner liveness = same hostname + signal-0 ESRCH, so a dead owner is reclaimed on macOS and Linux alike; a reused PID or foreign host only degrades to the conservative manual-recovery guidance - reclamation claims the lock via atomic rename before deleting, so concurrent reclaimers cannot double-delete and a lock recreated between inspect and claim is put back, not destroyed - abandoned rename claims are inert (never the lock path) and swept by age on the next acquire - drop the fs-native-extensions runtime dependency, its type shim, and the operation-guard file; release keeps the token-checked refusal The dead-owner recovery, hard-killed holder, foreign-host, live-PID, and claim-sweep regressions now run on every platform instead of being Linux-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-upReworked in commit
Safety envelope note: a PID reused by another process or a foreign hostname degrades to the conservative manual-guidance path, never to deletion; a sibling container sharing both this config dir and this hostname across PID namespaces is documented as out of envelope (previously covered by the Linux identity matrix, at the cost of Linux-only recovery). Verification: |
Final reviewVerdict: REJECT — blocking mutual-exclusion race Reviewed head Blocker
The post-rename token check prevents deletion of a successor, but it happens only after
I reproduced this deterministically in a disposable worktree by adding pause hooks around the existing rename/token-check sequence. The observed result was The current 8-writer regression does not force this stale-observer/successor interleaving. Please use a reclamation protocol that cannot move an unverified successor out of the coordination path (or otherwise keeps reclamation serialized continuously), and add a deterministic three-party regression covering A → B displacement → C acquisition. Verification performed
No production config, wallet, paid call, or provider call was used. |
Review follow-upAddressed the deterministic stale-observer → live-successor → third-writer race in commit
Verification:
|
GPT ReviewerVerdict: PASS Reviewed head No actionable findings. Stale-lock reclamation is serialized and revalidated under the advisory guard, normal acquisition and release remain independent of native-guard failures, and the supplied validation passes. FindingsNone. Shared exact-snapshot validation
|
Opus ReviewerVerdict: PASS Reviewed head This PR replaces the pure-mkdir config lock with a crash-recovering protocol: versioned owner metadata (PID/hostname/createdAt/token), same-host I verified the fix is sound. Mutual exclusion rests on atomic Prior review threads are all resolved: Codex P1 (owner-write cleanup deleting a successor) is prevented by removing age-based ownerless reclamation; Codex P2 (abandoned claim files) is gone since the guard is kernel-released on descriptor close/SIGKILL; GPT P1 (native runtime failure stranding a write) cannot occur because normal acquire/release performs no native calls and the guard is used only for reclamation — import/tryLock failures preserve the stale lock with actionable manual guidance and leave the token-checked release intact. Token-checked release cannot delete a successor (throws instead). PID-reuse, foreign-host, live-PID, and unverifiable owners all degrade conservatively to manual recovery (the unsafe direction — declaring a live process dead — is impossible under ESRCH semantics). The guard fd lifecycle closes the handle on every non-acquired branch (no leaks). Observations considered and deemed non-actionable: the persistent guard file is a correctness requirement (unlinking it would reintroduce an inode-swap race), not a stray artifact; the native dependency is install-safe (no build/install scripts; runtime addon resolution with graceful degradation; 0 vulnerabilities) and justified over the proven-racy rename-claim alternative, scoped strictly to reclamation; one dead-owner test omits skipIf(win32), but Windows is outside the supported/CI platform matrix and the config-write paths require OWS bindings that also exclude it. No stdout/stderr or JSON error-contract regressions (errors propagate as normal Errors with actionable text; no direct console writes). Shared validation is fully green (typecheck, lint, 168 CLI + 14 core tests, build, 0-vuln audit). No actionable findings. FindingsNone. Shared exact-snapshot validation
|
Summary
Technical notes
process.kill(pid, 0)liveness treats onlyESRCHas proof of death; PID reuse degrades conservatively to manual recoveryfs-native-extensionsonly after observing a reclaimable dead owner.config.lock.guard, reclamation re-reads.config.lock/owner.jsonand removes the canonical lock only when the token is unchanged and still deadtryLock()runtime failures preserve the stale lock and surface manual recovery guidance0600on POSIX; the kernel lock is released on descriptor close/process deathVerification
SIGKILLblocks before kill and recovers after descriptor closetryLock()runtime failure preserves the lock without affecting normal releaseNo production config, wallet signing, provider call, or payment was used during verification.