fix(lock): treat compromise-check I/O failure as a lost lock - #134
Conversation
Sidecar interval called verifyStillHeld() without a rejection handler. Snapshot I/O errors leaked unhandled rejections and never fired onCompromised. Map verification failures to a lost lock and invoke the callback once. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: needs maintainer review before merge. Reviewed August 14, 2026, 12:55 PM ET / 16:55 UTC. ClawSweeper reviewWhat this changesThis PR treats an asynchronous sidecar-lock verification failure as a lost lock, invokes the existing compromise callback once, and documents and tests that behavior. Regression provenancePossible regression — probable (reproduction; reviewed change). No predecessor PR is attributed. Merge readinessThis PR repairs a real current-main failure in periodic asynchronous lock verification. The narrow fail-closed handling, focused regression test, documentation update, and built-package terminal proof support normal maintainer review. Priority: P2 Review scores
Verification
How this fits togetherSidecar locks create a lock file beside an application target and periodically compare it with the ownership snapshot. A mismatch or inability to verify ownership informs the application through its one-time compromise callback. flowchart LR
A[Application acquires lock] --> B[Sidecar lock and snapshot]
B --> C[Periodic compromise check]
C --> D{Snapshot verified?}
D -->|yes| C
D -->|mismatch or I/O failure| E[Stop timer]
E --> F[One-time compromise callback]
Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Land the narrow timer-level failure handling with its regression coverage so configured compromise detection reports unverifiable ownership instead of producing unhandled rejections. Do we have a high-confidence way to reproduce the issue? Yes. Current main leaves the periodic verifier rejection unhandled, and the supplied built-package Node comparison demonstrates the callback and rejection behavior before and after the patch. Is this the best way to solve the issue? Yes. Catching only the periodic verification failure preserves direct verifier rejection semantics while applying the repository’s fail-closed ownership policy to the asynchronous detector. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against f74884494505. LabelsLabel changes:
Label justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
|
What Problem This Solves
Fixes an issue where consumers using
acquireFileLock/createSidecarLockManagerwithcompromiseCheckIntervalMsandonCompromisedwould never learn that the lock could no longer be verified when the sidecar snapshot read failed with a filesystem I/O error (for exampleEIO). The interval promise had no rejection handler, so Node emitted an unhandled rejection on every tick and the compromise callback never ran.This is the async sidecar lock surface (periodic ownership check), not path confinement or archive extraction.
Why This Change Was Made
The interval now maps
verifyStillHeld()rejection to "not held" and firesonCompromisedonce, then stops the timer. That matches the existing fail-closed identity policy: if the library cannot confirm the sidecar still matches the acquisition snapshot, treat the lock as lost. PublicverifyStillHeld()still rejects so callers who check it themselves see the I/O error. No API shape change.User Impact
Holders that already register
onCompromisednow get that callback when a periodic snapshot read fails, instead of an unhandled rejection storm and a silent detector. Transient disk errors can produce a false-positive compromise signal; that is detection only, not automatic revocation of in-flight work. The callback still fires at most once.Evidence
Live
nodeagainst builtdist/file-lock.json origin/main vs this branch. Same script: acquire a lock with a 20ms compromise interval, then makeverifyStillHeld()reject withEIO.Before (origin/main
f748844):onCompromisednever ran. The interval kept throwing (dozens of unhandled rejections in one second).After (this branch):
Callback fired once in 21ms. Zero unhandled rejections.
CHANGELOG.mdupdated when release-relevantReal behavior proof
Behavior or issue addressed: Periodic sidecar compromise check dropped I/O failures as unhandled rejections and never called
onCompromised.Real environment tested: macOS, Node v22, package built from this branch and from origin/main
f748844under/tmp/fs-safe-f001and/tmp/fs-safe-main-proof.Exact steps or command run after this patch:
Evidence after fix: terminal output from the patched build:
Observed result after fix: The interval treats a rejected snapshot as a lost lock, fires
onCompromisedonce, and does not emit unhandled rejections.What was not tested: Synchronous
acquireFileLockSyncinterval (separate throw-in-timer path). Live antivirus-inducedEIOon a production volume.Origin
The interval was added in
b1132f91via #92 (2026-08-02). The.thenhad no rejection handler from the start.